gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/filesystem/siadir/persist.go (about) 1 package siadir 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "reflect" 11 "time" 12 13 "gitlab.com/NebulousLabs/errors" 14 "go.sia.tech/siad/crypto" 15 "go.sia.tech/siad/modules" 16 17 "gitlab.com/SkynetLabs/skyd/build" 18 "gitlab.com/SkynetLabs/skyd/skymodules" 19 ) 20 21 const ( 22 // SiaDirExtension is the name of the metadata file for the sia directory 23 SiaDirExtension = ".siadir" 24 25 // DefaultDirHealth is the default health for the directory and the fall 26 // back value when there is an error. This is to protect against falsely 27 // trying to repair directories that had a read error 28 DefaultDirHealth = float64(0) 29 30 // DefaultDirRedundancy is the default redundancy for the directory and the 31 // fall back value when there is an error. This is to protect against 32 // falsely trying to repair directories that had a read error 33 DefaultDirRedundancy = float64(-1) 34 35 // metadataVersion is the version of the metadata 36 metadataVersion = "1.0" 37 ) 38 39 var ( 40 // ErrDeleted is the error returned if the siadir is deleted 41 ErrDeleted = errors.New("siadir is deleted") 42 43 // ErrCorruptFile is the error returned if the siadir is believed to be 44 // corrupt 45 ErrCorruptFile = errors.New(".siadir file is potentially corrupt") 46 47 // ErrInvalidChecksum is the error returned if the siadir checksum is invalid 48 ErrInvalidChecksum = errors.New(".siadir has invalid checksum") 49 ) 50 51 // New creates a new directory in the renter directory and makes sure there is a 52 // metadata file in the directory and creates one as needed. This method will 53 // also make sure that all the parent directories are created and have metadata 54 // files as well and will return the SiaDir containing the information for the 55 // directory that matches the siaPath provided 56 // 57 // NOTE: the fullPath is expected to include the rootPath. The rootPath is used 58 // to determine when to stop recursively creating siadir metadata. 59 func New(fullPath, rootPath string, mode os.FileMode) (*SiaDir, error) { 60 // Create path to directory and ensure path contains all metadata 61 deps := modules.ProdDependencies 62 err := createDirMetadataAll(fullPath, rootPath, mode, deps) 63 if err != nil { 64 return nil, errors.AddContext(err, "unable to create metadatas for parent directories") 65 } 66 67 // Create metadata for directory 68 md, err := createDirMetadata(fullPath, mode) 69 if err != nil { 70 return nil, errors.AddContext(err, "unable to create metadata for directory") 71 } 72 73 // Create SiaDir 74 sd := &SiaDir{ 75 metadata: md, 76 deps: deps, 77 path: fullPath, 78 } 79 80 return sd, sd.saveDir() 81 } 82 83 // LoadSiaDir loads the directory metadata from disk 84 func LoadSiaDir(path string, deps modules.Dependencies) (sd *SiaDir, err error) { 85 sd = &SiaDir{ 86 deps: deps, 87 path: path, 88 } 89 sd.metadata, err = callLoadSiaDirMetadata(filepath.Join(path, modules.SiaDirExtension), modules.ProdDependencies) 90 if errors.Contains(err, ErrInvalidChecksum) || errors.Contains(err, ErrCorruptFile) { 91 // If there was an error on load related to the checksum or a corrupt file, 92 // return a newly initialized metadata and try and fix the corruption by 93 // re-saving the metadata. This is OK because siadir persistence is not ACID 94 // and all metadata information can be recalculated. 95 sd.metadata, err = newMetadata() 96 if err != nil { 97 return nil, errors.AddContext(err, "unable to initialize new metadata") 98 } 99 err = sd.saveDir() 100 } 101 return sd, err 102 } 103 104 // Delete removes the directory from disk and marks it as deleted. Once the 105 // directory is deleted, attempting to access the directory will return an 106 // error. 107 func (sd *SiaDir) Delete() error { 108 sd.mu.Lock() 109 defer sd.mu.Unlock() 110 111 // Check if the SiaDir is already deleted 112 if sd.deleted { 113 return nil 114 } 115 116 // Delete the siadir 117 err := os.RemoveAll(sd.path) 118 if err != nil { 119 return errors.AddContext(err, "unable to delete siadir") 120 } 121 sd.deleted = true 122 return nil 123 } 124 125 // Rename renames the SiaDir to targetPath. 126 func (sd *SiaDir) Rename(targetPath string) error { 127 sd.mu.Lock() 128 defer sd.mu.Unlock() 129 130 // Check if Deleted 131 if sd.deleted { 132 return errors.AddContext(ErrDeleted, "cannot rename a deleted SiaDir") 133 } 134 return sd.rename(targetPath) 135 } 136 137 // SetPath sets the path field of the dir. 138 func (sd *SiaDir) SetPath(targetPath string) error { 139 sd.mu.Lock() 140 defer sd.mu.Unlock() 141 // Check if Deleted 142 if sd.deleted { 143 return errors.AddContext(ErrDeleted, "cannot set the path of a deleted SiaDir") 144 } 145 sd.path = targetPath 146 return nil 147 } 148 149 // UpdateLastHealthCheckTime updates the SiaDir LastHealthCheckTime and 150 // AggregateLastHealthCheckTime and saves the changes to disk 151 func (sd *SiaDir) UpdateLastHealthCheckTime(aggregateLastHealthCheckTime, lastHealthCheckTime time.Time) error { 152 sd.mu.Lock() 153 defer sd.mu.Unlock() 154 md := sd.metadata 155 md.AggregateLastHealthCheckTime = aggregateLastHealthCheckTime 156 md.LastHealthCheckTime = lastHealthCheckTime 157 return sd.updateMetadata(md) 158 } 159 160 // UpdateMetadata updates the SiaDir metadata on disk 161 func (sd *SiaDir) UpdateMetadata(metadata Metadata) error { 162 sd.mu.Lock() 163 defer sd.mu.Unlock() 164 return sd.updateMetadata(metadata) 165 } 166 167 // rename renames the SiaDir to targetPath. 168 func (sd *SiaDir) rename(targetPath string) error { 169 err := os.Rename(sd.path, targetPath) 170 if err != nil { 171 return err 172 } 173 sd.path = targetPath 174 return nil 175 } 176 177 // saveDir saves the SiaDir's metadata to disk. 178 func (sd *SiaDir) saveDir() (err error) { 179 // Check if Deleted 180 if sd.deleted { 181 return errors.AddContext(ErrDeleted, "cannot save a deleted SiaDir") 182 } 183 return saveDir(sd.path, sd.metadata, sd.deps) 184 } 185 186 // updateMetadata updates the SiaDir metadata on disk 187 func (sd *SiaDir) updateMetadata(metadata Metadata) error { 188 // Check if the directory is deleted 189 if sd.deleted { 190 return errors.AddContext(ErrDeleted, "cannot update the metadata for a deleted directory") 191 } 192 193 // Update metadata 194 sd.metadata.AggregateHealth = metadata.AggregateHealth 195 sd.metadata.AggregateLastHealthCheckTime = metadata.AggregateLastHealthCheckTime 196 sd.metadata.AggregateMinRedundancy = metadata.AggregateMinRedundancy 197 sd.metadata.AggregateModTime = metadata.AggregateModTime 198 sd.metadata.AggregateNumFiles = metadata.AggregateNumFiles 199 sd.metadata.AggregateNumLostFiles = metadata.AggregateNumLostFiles 200 sd.metadata.AggregateNumStuckChunks = metadata.AggregateNumStuckChunks 201 sd.metadata.AggregateNumSubDirs = metadata.AggregateNumSubDirs 202 sd.metadata.AggregateNumUnfinishedFiles = metadata.AggregateNumUnfinishedFiles 203 sd.metadata.AggregateRemoteHealth = metadata.AggregateRemoteHealth 204 sd.metadata.AggregateRepairSize = metadata.AggregateRepairSize 205 sd.metadata.AggregateSize = metadata.AggregateSize 206 sd.metadata.AggregateStuckHealth = metadata.AggregateStuckHealth 207 sd.metadata.AggregateStuckSize = metadata.AggregateStuckSize 208 209 sd.metadata.AggregateSkynetFiles = metadata.AggregateSkynetFiles 210 sd.metadata.AggregateSkynetSize = metadata.AggregateSkynetSize 211 212 sd.metadata.Health = metadata.Health 213 sd.metadata.LastHealthCheckTime = metadata.LastHealthCheckTime 214 sd.metadata.MinRedundancy = metadata.MinRedundancy 215 sd.metadata.ModTime = metadata.ModTime 216 sd.metadata.Mode = metadata.Mode 217 sd.metadata.NumFiles = metadata.NumFiles 218 sd.metadata.NumLostFiles = metadata.NumLostFiles 219 sd.metadata.NumStuckChunks = metadata.NumStuckChunks 220 sd.metadata.NumSubDirs = metadata.NumSubDirs 221 sd.metadata.NumUnfinishedFiles = metadata.NumUnfinishedFiles 222 sd.metadata.RemoteHealth = metadata.RemoteHealth 223 sd.metadata.RepairSize = metadata.RepairSize 224 sd.metadata.Size = metadata.Size 225 sd.metadata.StuckHealth = metadata.StuckHealth 226 sd.metadata.StuckSize = metadata.StuckSize 227 228 sd.metadata.SkynetFiles = metadata.SkynetFiles 229 sd.metadata.SkynetSize = metadata.SkynetSize 230 231 // NOTE: We're setting the version manually here because we are saving the 232 // metadata to disk using the most recent code. If the metadata used to have 233 // an older version, it'll have the most recent version following this 234 // update. 235 sd.metadata.Version = metadataVersion 236 metadata.Version = metadataVersion 237 238 // Testing check to ensure new fields aren't missed 239 if build.Release == "testing" && !reflect.DeepEqual(sd.metadata, metadata) { 240 str := fmt.Sprintf(`Input metadata not equal to set metadata 241 metadata 242 %v 243 sd.metadata 244 %v`, metadata, sd.metadata) 245 build.Critical(str) 246 } 247 248 // Sanity check that siadir is on disk 249 _, err := os.Stat(sd.path) 250 if os.IsNotExist(err) { 251 build.Critical("UpdateMetadata called on a SiaDir that does not exist on disk") 252 err = os.MkdirAll(filepath.Dir(sd.path), skymodules.DefaultDirPerm) 253 if err != nil { 254 return errors.AddContext(err, "unable to create missing siadir directory on disk") 255 } 256 } 257 258 return sd.saveDir() 259 } 260 261 // callLoadSiaDirMetadata loads the directory metadata from disk. 262 func callLoadSiaDirMetadata(path string, deps modules.Dependencies) (md Metadata, err error) { 263 // Open the file. 264 file, err := deps.Open(path) 265 if err != nil { 266 return Metadata{}, err 267 } 268 defer func() { 269 err = errors.Compose(err, file.Close()) 270 }() 271 272 // Read the file 273 fileBytes, err := ioutil.ReadAll(file) 274 if err != nil { 275 return Metadata{}, errors.AddContext(err, "unable to read bytes from file") 276 } 277 278 // Verify there is enough data for a checksum 279 if len(fileBytes) < crypto.HashSize { 280 return Metadata{}, ErrCorruptFile 281 } 282 283 // Verify checksum 284 checksum := fileBytes[:crypto.HashSize] 285 mdBytes := fileBytes[crypto.HashSize:] 286 fileChecksum := crypto.HashBytes(mdBytes) 287 if !bytes.Equal(checksum, fileChecksum[:]) { 288 return Metadata{}, ErrInvalidChecksum 289 } 290 291 // Parse the json object. 292 err = json.Unmarshal(mdBytes, &md) 293 if err != nil { 294 return Metadata{}, errors.AddContext(err, "unable to unmarshal metadata") 295 } 296 297 // CompatV1420 check if filemode is set. If not use the default. It's fine 298 // not to persist it right away since it will either be persisted anyway or 299 // we just set the values again the next time we load it and hope that it 300 // gets persisted then. 301 if md.Version == "" && md.Mode == 0 { 302 md.Mode = modules.DefaultDirPerm 303 md.Version = metadataVersion 304 } 305 return 306 } 307 308 // createDirMetadata makes sure there is a metadata file in the directory and 309 // creates one as needed 310 func createDirMetadata(path string, mode os.FileMode) (Metadata, error) { 311 // Check if metadata file exists 312 mdPath := filepath.Join(path, modules.SiaDirExtension) 313 _, err := os.Stat(mdPath) 314 if err == nil { 315 return Metadata{}, os.ErrExist 316 } else if !os.IsNotExist(err) { 317 return Metadata{}, err 318 } 319 320 md, err := newMetadata() 321 if err != nil { 322 return Metadata{}, errors.AddContext(err, "unable to initialize new metadata") 323 } 324 md.Mode = mode 325 return md, nil 326 } 327 328 // createDirMetadataAll creates a path on disk to the provided siaPath and make 329 // sure that all the parent directories have metadata files. 330 func createDirMetadataAll(dirPath, rootPath string, mode os.FileMode, deps modules.Dependencies) error { 331 // Create path to directory 332 if err := os.MkdirAll(dirPath, modules.DefaultDirPerm); err != nil { 333 return err 334 } 335 336 // Create metadata 337 for dirPath != rootPath { 338 dirPath = filepath.Dir(dirPath) 339 if dirPath == string(filepath.Separator) || dirPath == "." { 340 dirPath = rootPath 341 } 342 md, err := createDirMetadata(dirPath, mode) 343 if err != nil && !errors.Contains(err, os.ErrExist) { 344 return errors.AddContext(err, "unable to create metadata") 345 } 346 if !errors.Contains(err, os.ErrExist) { 347 // Save metadata if the file doesn't already exist 348 err = saveDir(dirPath, md, deps) 349 if err != nil { 350 return errors.AddContext(err, "unable to saveDir") 351 } 352 } 353 } 354 return nil 355 } 356 357 // newMetadata returns an initialized Metadata with all default values. 358 func newMetadata() (Metadata, error) { 359 // Initialize metadata, set Health and StuckHealth to DefaultDirHealth so 360 // empty directories won't be viewed as being the most in need. Initialize 361 // ModTimes. 362 now := time.Now() 363 md := Metadata{ 364 AggregateHealth: DefaultDirHealth, 365 AggregateMinRedundancy: DefaultDirRedundancy, 366 AggregateModTime: now, 367 AggregateRemoteHealth: DefaultDirHealth, 368 AggregateStuckHealth: DefaultDirHealth, 369 370 Health: DefaultDirHealth, 371 MinRedundancy: DefaultDirRedundancy, 372 Mode: modules.DefaultDirPerm, 373 ModTime: now, 374 RemoteHealth: DefaultDirHealth, 375 StuckHealth: DefaultDirHealth, 376 Version: metadataVersion, 377 } 378 return md, VerifyMetadataInit(md) 379 } 380 381 // saveDir saves the metadata to disk at the provided path. 382 func saveDir(path string, md Metadata, deps modules.Dependencies) (err error) { 383 // Open .siadir file 384 f, err := deps.OpenFile(filepath.Join(path, SiaDirExtension), os.O_RDWR|os.O_CREATE, modules.DefaultFilePerm) 385 if err != nil { 386 return errors.AddContext(err, "unable to open file") 387 } 388 defer func() { 389 err = errors.Compose(err, f.Close()) 390 }() 391 392 // Marshal metadata 393 data, err := json.Marshal(md) 394 if err != nil { 395 return errors.AddContext(err, "unable to marshal metadata") 396 } 397 398 // Generate checksum 399 checksum := crypto.HashBytes(data) 400 401 // Write the checksum to the file 402 _, err = f.WriteAt(checksum[:], 0) 403 if err != nil { 404 return errors.AddContext(err, "unable to write checksum") 405 } 406 407 // Write the metadata to disk 408 checksumLen := int64(len(checksum)) 409 _, err = f.WriteAt(data, checksumLen) 410 if err != nil { 411 return errors.AddContext(err, "unable to write data to disk") 412 } 413 414 // Truncate the file to clear any corrupt or lingering data 415 truncateLen := checksumLen + int64(len(data)) 416 err = f.Truncate(truncateLen) 417 if err != nil { 418 return errors.AddContext(err, "unable to truncate file") 419 } 420 return nil 421 }