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  }