gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/filesystem/siafile/metadata.go (about) 1 package siafile 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "os" 8 "path/filepath" 9 "time" 10 11 "gitlab.com/NebulousLabs/errors" 12 "gitlab.com/NebulousLabs/writeaheadlog" 13 14 "gitlab.com/SkynetLabs/skyd/build" 15 "gitlab.com/SkynetLabs/skyd/skymodules" 16 "go.sia.tech/siad/crypto" 17 "go.sia.tech/siad/persist" 18 "go.sia.tech/siad/types" 19 ) 20 21 type ( 22 // SiafileUID is a unique identifier for siafile which is used to track 23 // siafiles even after renaming them. 24 SiafileUID string 25 26 // Metadata is the metadata of a SiaFile and is JSON encoded. 27 // Note: Methods which update the metadata and can potentially fail after 28 // doing so and before persisting the change should use backup() and 29 // restore() to restore the metadata before returning the error. Also 30 // changes to Metadata require backup() and restore() to be updated as well. 31 Metadata struct { 32 UniqueID SiafileUID `json:"uniqueid"` // unique identifier for file 33 34 StaticPagesPerChunk uint8 `json:"pagesperchunk"` // number of pages reserved for storing a chunk. 35 StaticVersion [16]byte `json:"version"` // version of the sia file format used 36 FileSize int64 `json:"filesize"` // total size of the file 37 StaticPieceSize uint64 `json:"piecesize"` // size of a single piece of the file 38 LocalPath string `json:"localpath"` // file to the local copy of the file used for repairing 39 40 // Fields for encryption 41 StaticMasterKey []byte `json:"masterkey"` // masterkey used to encrypt pieces 42 StaticMasterKeyType crypto.CipherType `json:"masterkeytype"` 43 StaticSharingKey []byte `json:"sharingkey"` // key used to encrypt shared pieces 44 StaticSharingKeyType crypto.CipherType `json:"sharingkeytype"` 45 46 // The following fields are the usual unix timestamps of files. 47 ModTime time.Time `json:"modtime"` // time of last content modification 48 ChangeTime time.Time `json:"changetime"` // time of last metadata modification 49 AccessTime time.Time `json:"accesstime"` // time of last access 50 CreateTime time.Time `json:"createtime"` // time of file creation 51 52 // Cached fields. These fields are cached fields and are only meant to be used 53 // to create FileInfos for file related API endpoints. There is no guarantee 54 // that these fields are up-to-date. Neither in memory nor on disk. Updates to 55 // these fields aren't persisted immediately. Instead they will only be 56 // persisted whenever another method persists the metadata or when the SiaFile 57 // is closed. 58 // 59 // CachedRedundancy is the redundancy of the file on the network and is 60 // updated within the 'Redundancy' method which is periodically called by the 61 // repair code. 62 // 63 // CachedUserRedundancy is the redundancy of the file on the network as 64 // visible to the user and is updated within the 'Redundancy' method which is 65 // periodically called by the repair code. 66 // 67 // CachedHealth is the health of the file on the network and is also 68 // periodically updated by the health check loop whenever 'Health' is called. 69 // 70 // CachedStuckHealth is the health of the stuck chunks of the file. It is 71 // updated by the health check loop. CachedExpiration is the lowest height at 72 // which any of the file's contracts will expire. Also updated periodically by 73 // the health check loop whenever 'Health' is called. 74 // 75 // CachedUploadedBytes is the number of bytes of the file that have been 76 // uploaded to the network so far. Is updated every time a piece is added to 77 // the siafile. 78 // 79 // CachedUploadProgress is the upload progress of the file and is updated 80 // every time a piece is added to the siafile. 81 CachedRedundancy float64 `json:"cachedredundancy"` 82 CachedRepairBytes uint64 `json:"cachedrepairbytes"` 83 CachedUserRedundancy float64 `json:"cacheduserredundancy"` 84 CachedHealth float64 `json:"cachedhealth"` 85 CachedStuckBytes uint64 `json:"cachedstuckbytes"` 86 CachedStuckHealth float64 `json:"cachedstuckhealth"` 87 CachedExpiration types.BlockHeight `json:"cachedexpiration"` 88 CachedUploadedBytes uint64 `json:"cacheduploadedbytes"` 89 CachedUploadProgress float64 `json:"cacheduploadprogress"` 90 91 // Repair loop fields 92 // 93 // Finished indicates if the file ever finished uploading. A file is 94 // considered to have finished uploading if the health was ever < 1. 95 // 96 // LastHealthCheckTime is the timestamp of the last time the SiaFile's 97 // health was checked by Health() 98 // 99 // LazyUpload indicates if the file was uploaded with LazyPinning 100 // 101 // Lost indicates if the node views the file as lost 102 // 103 // NumStuckChunks is the number of all the SiaFile's chunks that have 104 // been marked as stuck by the repair loop. This doesn't include a potential 105 // partial chunk at the end of the file though. Use 'numStuckChunks()' for 106 // that instead. 107 Finished bool `json:"finished"` 108 LastHealthCheckTime time.Time `json:"lasthealthchecktime"` 109 LazyUpload bool `json:"lazyupload"` 110 Lost bool `json:"lost"` 111 NumStuckChunks uint64 `json:"numstuckchunks"` 112 113 // File ownership/permission fields. 114 Mode os.FileMode `json:"mode"` // unix filemode of the sia file - uint32 115 UserID int32 `json:"userid"` // id of the user who owns the file 116 GroupID int32 `json:"groupid"` // id of the group that owns the file 117 118 // The following fields are the offsets for data that is written to disk 119 // after the pubKeyTable. We reserve a generous amount of space for the 120 // table and extra fields, but we need to remember those offsets in case we 121 // need to resize later on. 122 // 123 // chunkOffset is the offset of the first chunk, forced to be a factor of 124 // 4096, default 4kib 125 // 126 // pubKeyTableOffset is the offset of the publicKeyTable within the 127 // file. 128 // 129 ChunkOffset int64 `json:"chunkoffset"` 130 PubKeyTableOffset int64 `json:"pubkeytableoffset"` 131 132 // erasure code settings. 133 // 134 // StaticErasureCodeType specifies the algorithm used for erasure coding 135 // chunks. Available types are: 136 // 0 - Invalid / Missing Code 137 // 1 - Reed Solomon Code 138 // 139 // erasureCodeParams specifies possible parameters for a certain 140 // StaticErasureCodeType. Currently params will be parsed as follows: 141 // Reed Solomon Code - 4 bytes dataPieces / 4 bytes parityPieces 142 // 143 StaticErasureCodeType [4]byte `json:"erasurecodetype"` 144 StaticErasureCodeParams [8]byte `json:"erasurecodeparams"` 145 staticErasureCode skymodules.ErasureCoder // not persisted, exists for convenience 146 147 // Skylink tracking. If this siafile is known to have sectors of any 148 // skyfiles, those skyfiles will be listed here. It should be noted that 149 // a single siafile can be responsible for tracking many skyfiles. 150 Skylinks []string `json:"skylinks"` 151 } 152 153 // BubbledMetadata is the metadata of a siafile that gets bubbled 154 BubbledMetadata struct { 155 CreateTime time.Time 156 Finished bool 157 Health float64 158 LastHealthCheckTime time.Time 159 LazyUpload bool 160 Lost bool 161 ModTime time.Time 162 NumSkylinks uint64 163 NumStuckChunks uint64 164 OnDisk bool 165 Redundancy float64 166 RepairBytes uint64 167 Size uint64 168 StuckBytes uint64 169 StuckHealth float64 170 UID SiafileUID 171 } 172 ) 173 174 // AccessTime returns the AccessTime timestamp of the file. 175 func (sf *SiaFile) AccessTime() time.Time { 176 sf.mu.RLock() 177 defer sf.mu.RUnlock() 178 return sf.staticMetadata.AccessTime 179 } 180 181 // AddSkylink will add a skylink to the SiaFile. 182 func (sf *SiaFile) AddSkylink(s skymodules.Skylink) (err error) { 183 sf.mu.Lock() 184 defer sf.mu.Unlock() 185 // backup the changed metadata before changing it. Revert the change on 186 // error. 187 defer func(backup Metadata) { 188 if err != nil { 189 sf.staticMetadata.restore(backup) 190 } 191 }(sf.staticMetadata.backup()) 192 sf.staticMetadata.Skylinks = append(sf.staticMetadata.Skylinks, s.String()) 193 194 // Save changes to metadata to disk. 195 return sf.saveMetadata() 196 } 197 198 // ChangeTime returns the ChangeTime timestamp of the file. 199 func (sf *SiaFile) ChangeTime() time.Time { 200 sf.mu.RLock() 201 defer sf.mu.RUnlock() 202 return sf.staticMetadata.ChangeTime 203 } 204 205 // CreateTime returns the CreateTime timestamp of the file. 206 func (sf *SiaFile) CreateTime() time.Time { 207 sf.mu.RLock() 208 defer sf.mu.RUnlock() 209 return sf.staticMetadata.CreateTime 210 } 211 212 // ChunkSize returns the size of a single chunk of the file. 213 func (sf *SiaFile) ChunkSize() uint64 { 214 return sf.staticChunkSize() 215 } 216 217 // Finished returns whether or not the file finished uploading 218 func (sf *SiaFile) Finished() bool { 219 sf.mu.RLock() 220 defer sf.mu.RUnlock() 221 return sf.finished() 222 } 223 224 // finished returns whether or not the file finished uploading 225 func (sf *SiaFile) finished() bool { 226 return sf.staticMetadata.Finished 227 } 228 229 // LastHealthCheckTime returns the LastHealthCheckTime timestamp of the file 230 func (sf *SiaFile) LastHealthCheckTime() time.Time { 231 sf.mu.RLock() 232 defer sf.mu.RUnlock() 233 return sf.staticMetadata.LastHealthCheckTime 234 } 235 236 // LazyUpload indicates whether a siafile is intended to be uploaded lazily. 237 // Meaning that it can be uploaded from the network only during repairs. 238 func (sf *SiaFile) LazyUpload() bool { 239 sf.mu.RLock() 240 defer sf.mu.RUnlock() 241 return sf.staticMetadata.LazyUpload 242 } 243 244 // LocalPath returns the path of the local data of the file. 245 func (sf *SiaFile) LocalPath() string { 246 sf.mu.RLock() 247 defer sf.mu.RUnlock() 248 return sf.staticMetadata.LocalPath 249 } 250 251 // MasterKey returns the masterkey used to encrypt the file. 252 func (sf *SiaFile) MasterKey() crypto.CipherKey { 253 return sf.staticMasterKey() 254 } 255 256 // Metadata returns the SiaFile's metadata, resolving any fields related to 257 // partial chunks. 258 func (sf *SiaFile) Metadata() Metadata { 259 sf.mu.RLock() 260 defer sf.mu.RUnlock() 261 md := sf.staticMetadata 262 md.NumStuckChunks = sf.numStuckChunks() 263 md.Finished = sf.finished() 264 return md 265 } 266 267 // Mode returns the FileMode of the SiaFile. 268 func (sf *SiaFile) Mode() os.FileMode { 269 sf.mu.RLock() 270 defer sf.mu.RUnlock() 271 return sf.staticMetadata.Mode 272 } 273 274 // ModTime returns the ModTime timestamp of the file. 275 func (sf *SiaFile) ModTime() time.Time { 276 sf.mu.RLock() 277 defer sf.mu.RUnlock() 278 return sf.staticMetadata.ModTime 279 } 280 281 // NumStuckChunks returns the Number of Stuck Chunks recorded in the file's 282 // metadata 283 func (sf *SiaFile) NumStuckChunks() uint64 { 284 sf.mu.RLock() 285 defer sf.mu.RUnlock() 286 return sf.numStuckChunks() 287 } 288 289 // PieceSize returns the size of a single piece of the file. 290 func (sf *SiaFile) PieceSize() uint64 { 291 return sf.staticMetadata.StaticPieceSize 292 } 293 294 // Rename changes the name of the file to a new one. To guarantee that renaming 295 // the file is atomic across all operating systems, we create a wal transaction 296 // that moves over all the chunks one-by-one and deletes the src file. 297 func (sf *SiaFile) Rename(newSiaFilePath string) error { 298 sf.mu.Lock() 299 defer sf.mu.Unlock() 300 return sf.rename(newSiaFilePath) 301 } 302 303 // backup creates a deep-copy of a Metadata. 304 func (md Metadata) backup() (b Metadata) { 305 // Copy the static fields first. They are shallow copies since they are not 306 // allowed to change. 307 b.StaticPagesPerChunk = md.StaticPagesPerChunk 308 b.StaticVersion = md.StaticVersion 309 b.StaticPieceSize = md.StaticPieceSize 310 b.StaticMasterKey = md.StaticMasterKey 311 b.StaticMasterKeyType = md.StaticMasterKeyType 312 b.StaticSharingKey = md.StaticSharingKey 313 b.StaticSharingKeyType = md.StaticSharingKeyType 314 b.StaticErasureCodeType = md.StaticErasureCodeType 315 b.StaticErasureCodeParams = md.StaticErasureCodeParams 316 b.staticErasureCode = md.staticErasureCode 317 318 // Deep copy the remaining fields. For the sake of completion and safety we 319 // also copy the native types one-by-one even though they could be cloned 320 // together with the static types by a simple assignment like b = md 321 b.UniqueID = md.UniqueID 322 b.FileSize = md.FileSize 323 b.LocalPath = md.LocalPath 324 b.ModTime = md.ModTime 325 b.ChangeTime = md.ChangeTime 326 b.AccessTime = md.AccessTime 327 b.CreateTime = md.CreateTime 328 b.CachedRepairBytes = md.CachedRepairBytes 329 b.CachedStuckBytes = md.CachedStuckBytes 330 b.CachedRedundancy = md.CachedRedundancy 331 b.CachedUserRedundancy = md.CachedUserRedundancy 332 b.CachedHealth = md.CachedHealth 333 b.CachedStuckHealth = md.CachedStuckHealth 334 b.CachedExpiration = md.CachedExpiration 335 b.CachedUploadedBytes = md.CachedUploadedBytes 336 b.CachedUploadProgress = md.CachedUploadProgress 337 b.Finished = md.Finished 338 b.LastHealthCheckTime = md.LastHealthCheckTime 339 b.LazyUpload = md.LazyUpload 340 b.Lost = md.Lost 341 b.NumStuckChunks = md.NumStuckChunks 342 b.Mode = md.Mode 343 b.UserID = md.UserID 344 b.GroupID = md.GroupID 345 b.ChunkOffset = md.ChunkOffset 346 b.PubKeyTableOffset = md.PubKeyTableOffset 347 // Special handling for slice since reflect.DeepEqual is false when 348 // comparing empty slice to nil. 349 if md.Skylinks == nil { 350 b.Skylinks = nil 351 } else { 352 // Being extra explicit about capacity and length here. 353 b.Skylinks = make([]string, len(md.Skylinks), cap(md.Skylinks)) 354 copy(b.Skylinks, md.Skylinks) 355 } 356 // If the backup was successful it should match the original. 357 if build.Release == "testing" && !md.equals(b) { 358 fmt.Println("md:\n", md) 359 fmt.Println("b:\n", b) 360 build.Critical("backup: copy doesn't match original") 361 } 362 return 363 } 364 365 // restore restores the metadata from a backup created with the backup() method. 366 func (md *Metadata) restore(b Metadata) { 367 md.UniqueID = b.UniqueID 368 md.FileSize = b.FileSize 369 md.LocalPath = b.LocalPath 370 md.ModTime = b.ModTime 371 md.ChangeTime = b.ChangeTime 372 md.AccessTime = b.AccessTime 373 md.CreateTime = b.CreateTime 374 md.CachedRepairBytes = b.CachedRepairBytes 375 md.CachedStuckBytes = b.CachedStuckBytes 376 md.CachedRedundancy = b.CachedRedundancy 377 md.CachedUserRedundancy = b.CachedUserRedundancy 378 md.CachedHealth = b.CachedHealth 379 md.CachedStuckHealth = b.CachedStuckHealth 380 md.CachedExpiration = b.CachedExpiration 381 md.CachedUploadedBytes = b.CachedUploadedBytes 382 md.CachedUploadProgress = b.CachedUploadProgress 383 md.Finished = b.Finished 384 md.LastHealthCheckTime = b.LastHealthCheckTime 385 b.LazyUpload = md.LazyUpload 386 b.Lost = md.Lost 387 md.NumStuckChunks = b.NumStuckChunks 388 md.Mode = b.Mode 389 md.UserID = b.UserID 390 md.GroupID = b.GroupID 391 md.ChunkOffset = b.ChunkOffset 392 md.PubKeyTableOffset = b.PubKeyTableOffset 393 md.Skylinks = b.Skylinks 394 // If the backup was successful it should match the backup. 395 if build.Release == "testing" && !md.equals(b) { 396 fmt.Println("md:\n", md) 397 fmt.Println("b:\n", b) 398 build.Critical("restore: copy doesn't match original") 399 } 400 } 401 402 // equal compares the two structs for equality by serializing them and comparing 403 // the serialized representations. 404 // 405 // WARNING: Do not use in production! 406 func (md *Metadata) equals(b Metadata) bool { 407 if build.Release != "testing" { 408 build.Critical("Metadata.equals used in non-testing code!") 409 } 410 mdBytes, err := json.Marshal(md) 411 if err != nil { 412 build.Critical(fmt.Sprintf("failed to marshal: %s. Problematic entity: %+v\n", err.Error(), md)) 413 } 414 bBytes, err := json.Marshal(b) 415 if err != nil { 416 build.Critical(fmt.Sprintf("failed to marshal: %s. Problematic entity: %+v\n", err.Error(), b)) 417 } 418 return bytes.Equal(mdBytes, bBytes) 419 } 420 421 // rename changes the name of the file to a new one. To guarantee that renaming 422 // the file is atomic across all operating systems, we create a wal transaction 423 // that moves over all the chunks one-by-one and deletes the src file. 424 func (sf *SiaFile) rename(newSiaFilePath string) (err error) { 425 if sf.deleted { 426 return errors.New("can't rename deleted siafile") 427 } 428 // backup the changed metadata before changing it. Revert the change on 429 // error. 430 oldPath := sf.siaFilePath 431 defer func(backup Metadata) { 432 if err != nil { 433 sf.staticMetadata.restore(backup) 434 sf.siaFilePath = oldPath 435 } 436 }(sf.staticMetadata.backup()) 437 // Check if file exists at new location. 438 if _, err := os.Stat(newSiaFilePath); err == nil { 439 return ErrPathOverload 440 } 441 // Create path to renamed location. 442 dir, _ := filepath.Split(newSiaFilePath) 443 err = os.MkdirAll(dir, 0700) 444 if err != nil { 445 return err 446 } 447 // Create the delete update before changing the path to the new one. 448 updates := []writeaheadlog.Update{sf.createDeleteUpdate()} 449 // Load all the chunks. 450 chunks := make([]chunk, 0, sf.numChunks) 451 err = sf.iterateChunksReadonly(func(chunk chunk) error { 452 chunks = append(chunks, chunk) 453 return nil 454 }) 455 if err != nil { 456 return err 457 } 458 // Rename file in memory. 459 sf.siaFilePath = newSiaFilePath 460 // Update the ChangeTime because the metadata changed. 461 sf.staticMetadata.ChangeTime = time.Now() 462 // Write the header to the new location. 463 headerUpdate, err := sf.saveHeaderUpdates() 464 if err != nil { 465 return err 466 } 467 updates = append(updates, headerUpdate...) 468 // Write the chunks to the new location. 469 for _, chunk := range chunks { 470 updates = append(updates, sf.saveChunkUpdate(chunk)) 471 } 472 // Apply updates. 473 return createAndApplyTransaction(sf.wal, updates...) 474 } 475 476 // SetMode sets the filemode of the sia file. 477 func (sf *SiaFile) SetMode(mode os.FileMode) (err error) { 478 sf.mu.Lock() 479 defer sf.mu.Unlock() 480 // backup the changed metadata before changing it. Revert the change on 481 // error. 482 defer func(backup Metadata) { 483 if err != nil { 484 sf.staticMetadata.restore(backup) 485 } 486 }(sf.staticMetadata.backup()) 487 sf.staticMetadata.Mode = mode 488 sf.staticMetadata.ChangeTime = time.Now() 489 490 // Save changes to metadata to disk. 491 return sf.saveMetadata() 492 } 493 494 // SetLastHealthCheckTime sets the LastHealthCheckTime in memory to the current 495 // time but does not update and write to disk. 496 // 497 // NOTE: This call should be used in conjunction with a method that saves the 498 // SiaFile metadata 499 func (sf *SiaFile) SetLastHealthCheckTime() { 500 sf.mu.Lock() 501 defer sf.mu.Unlock() 502 sf.staticMetadata.LastHealthCheckTime = time.Now() 503 } 504 505 // SetLocalPath changes the local path of the file which is used to repair 506 // the file from disk. 507 func (sf *SiaFile) SetLocalPath(path string) (err error) { 508 sf.mu.Lock() 509 defer sf.mu.Unlock() 510 // backup the changed metadata before changing it. Revert the change on 511 // error. 512 defer func(backup Metadata) { 513 if err != nil { 514 sf.staticMetadata.restore(backup) 515 } 516 }(sf.staticMetadata.backup()) 517 518 sf.staticMetadata.LocalPath = path 519 520 // Save changes to metadata to disk. 521 return sf.saveMetadata() 522 } 523 524 // SetLost sets the Lost flag for the siafile. 525 func (sf *SiaFile) SetLost(lost bool) (err error) { 526 sf.mu.Lock() 527 defer sf.mu.Unlock() 528 // backup the changed metadata before changing it. Revert the change on 529 // error. 530 defer func(backup Metadata) { 531 if err != nil { 532 sf.staticMetadata.restore(backup) 533 } 534 }(sf.staticMetadata.backup()) 535 536 sf.staticMetadata.Lost = lost 537 538 // Save changes to metadata to disk. 539 return sf.saveMetadata() 540 } 541 542 // Size returns the file's size. 543 func (sf *SiaFile) Size() uint64 { 544 sf.mu.RLock() 545 defer sf.mu.RUnlock() 546 return uint64(sf.staticMetadata.FileSize) 547 } 548 549 // UpdateUniqueID creates a new random uid for the SiaFile. 550 func (sf *SiaFile) UpdateUniqueID() { 551 sf.staticMetadata.UniqueID = uniqueID() 552 } 553 554 // UpdateAccessTime updates the AccessTime timestamp to the current time. 555 func (sf *SiaFile) UpdateAccessTime() (err error) { 556 sf.mu.Lock() 557 defer sf.mu.Unlock() 558 // backup the changed metadata before changing it. Revert the change on 559 // error. 560 defer func(backup Metadata) { 561 if err != nil { 562 sf.staticMetadata.restore(backup) 563 } 564 }(sf.staticMetadata.backup()) 565 sf.staticMetadata.AccessTime = time.Now() 566 567 // Save changes to metadata to disk. 568 return sf.saveMetadata() 569 } 570 571 // numStuckChunks returns the number of stuck chunks recorded in the file's 572 // metadata. 573 func (sf *SiaFile) numStuckChunks() uint64 { 574 return sf.staticMetadata.NumStuckChunks 575 } 576 577 // staticChunkSize returns the size of a single chunk of the file. 578 func (sf *SiaFile) staticChunkSize() uint64 { 579 return sf.staticMetadata.StaticPieceSize * uint64(sf.staticMetadata.staticErasureCode.MinPieces()) 580 } 581 582 // staticMasterKey returns the masterkey used to encrypt the file. 583 func (sf *SiaFile) staticMasterKey() crypto.CipherKey { 584 sk, err := crypto.NewSiaKey(sf.staticMetadata.StaticMasterKeyType, sf.staticMetadata.StaticMasterKey) 585 if err != nil { 586 // This should never happen since the constructor of the SiaFile takes 587 // a CipherKey as an argument which guarantees that it is already a 588 // valid key. 589 panic(errors.AddContext(err, "failed to create masterkey of siafile")) 590 } 591 return sk 592 } 593 594 // onDisk is a helper for indicated if a siafile is ondisk, which means it has a 595 // localPath. 596 func (sf *SiaFile) onDisk() bool { 597 return sf.staticMetadata.LocalPath != "" 598 } 599 600 // uniqueID creates a random unique SiafileUID. 601 func uniqueID() SiafileUID { 602 return SiafileUID(persist.UID()) 603 }