gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/persist_compat.go (about)

     1  package renter
     2  
     3  import (
     4  	"compress/gzip"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  
    12  	"gitlab.com/NebulousLabs/errors"
    13  
    14  	"gitlab.com/NebulousLabs/encoding"
    15  	"gitlab.com/SkynetLabs/skyd/build"
    16  	"gitlab.com/SkynetLabs/skyd/skymodules"
    17  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem"
    18  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem/siafile"
    19  	"go.sia.tech/siad/crypto"
    20  	"go.sia.tech/siad/modules"
    21  	"go.sia.tech/siad/persist"
    22  	"go.sia.tech/siad/types"
    23  )
    24  
    25  // v137Persistence is the persistence struct of a renter that doesn't use the
    26  // new SiaFile format yet.
    27  type v137Persistence struct {
    28  	MaxDownloadSpeed int64
    29  	MaxUploadSpeed   int64
    30  	StreamCacheSize  uint64
    31  	Tracking         map[string]v137TrackedFile
    32  }
    33  
    34  // v137TrackedFile is the tracking information stored about a file on a legacy
    35  // renter.
    36  type v137TrackedFile struct {
    37  	RepairPath string
    38  }
    39  
    40  // The v1.3.7 in-memory file format.
    41  //
    42  // A file is a single file that has been uploaded to the network. Files are
    43  // split into equal-length chunks, which are then erasure-coded into pieces.
    44  // Each piece is separately encrypted, using a key derived from the file's
    45  // master key. The pieces are uploaded to hosts in groups, such that one file
    46  // contract covers many pieces.
    47  type file struct {
    48  	name        string
    49  	size        uint64 // Static - can be accessed without lock.
    50  	contracts   map[types.FileContractID]fileContract
    51  	masterKey   [crypto.EntropySize]byte // Static - can be accessed without lock.
    52  	erasureCode skymodules.ErasureCoder  // Static - can be accessed without lock.
    53  	pieceSize   uint64                   // Static - can be accessed without lock.
    54  	mode        uint32                   // actually an os.FileMode
    55  	deleted     bool                     // indicates if the file has been deleted.
    56  
    57  	staticUID string // A UID assigned to the file when it gets created.
    58  }
    59  
    60  // The v1.3.7 in-memory format for a contract used by the v1.3.7 file format.
    61  //
    62  // A fileContract is a contract covering an arbitrary number of file pieces.
    63  // Chunk/Piece metadata is used to split the raw contract data appropriately.
    64  type fileContract struct {
    65  	ID     types.FileContractID
    66  	IP     modules.NetAddress
    67  	Pieces []pieceData
    68  
    69  	WindowStart types.BlockHeight
    70  }
    71  
    72  // The v1.3.7 in-memory format for a piece used by the v1.3.7 file format.
    73  //
    74  // pieceData contains the metadata necessary to request a piece from a
    75  // fetcher.
    76  //
    77  // TODO: Add an 'Unavailable' flag that can be set if the host loses the piece.
    78  // Some TODOs exist in 'repair.go' related to this field.
    79  type pieceData struct {
    80  	Chunk      uint64      // which chunk the piece belongs to
    81  	Piece      uint64      // the index of the piece in the chunk
    82  	MerkleRoot crypto.Hash // the Merkle root of the piece
    83  }
    84  
    85  // numChunks returns the number of chunks that f was split into.
    86  func (f *file) numChunks() uint64 {
    87  	// empty files still need at least one chunk
    88  	if f.size == 0 {
    89  		return 1
    90  	}
    91  	n := f.size / f.staticChunkSize()
    92  	// last chunk will be padded, unless chunkSize divides file evenly.
    93  	if f.size%f.staticChunkSize() != 0 {
    94  		n++
    95  	}
    96  	return n
    97  }
    98  
    99  // staticChunkSize returns the size of one chunk.
   100  func (f *file) staticChunkSize() uint64 {
   101  	return f.pieceSize * uint64(f.erasureCode.MinPieces())
   102  }
   103  
   104  // MarshalSia implements the encoding.SiaMarshaller interface, writing the
   105  // file data to w.
   106  func (f *file) MarshalSia(w io.Writer) error {
   107  	enc := encoding.NewEncoder(w)
   108  
   109  	// encode easy fields
   110  	err := enc.EncodeAll(
   111  		f.name,
   112  		f.size,
   113  		f.masterKey,
   114  		f.pieceSize,
   115  		f.mode,
   116  	)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	// COMPATv0.4.3 - encode the bytesUploaded and chunksUploaded fields
   121  	// TODO: the resulting .sia file may confuse old clients.
   122  	err = enc.EncodeAll(f.pieceSize*f.numChunks()*uint64(f.erasureCode.NumPieces()), f.numChunks())
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	// encode erasureCode
   128  	switch code := f.erasureCode.(type) {
   129  	case *skymodules.RSCode:
   130  		err = enc.EncodeAll(
   131  			"Reed-Solomon",
   132  			uint64(code.MinPieces()),
   133  			uint64(code.NumPieces()-code.MinPieces()),
   134  		)
   135  		if err != nil {
   136  			return err
   137  		}
   138  	default:
   139  		if build.DEBUG {
   140  			panic("unknown erasure code")
   141  		}
   142  		return errors.New("unknown erasure code")
   143  	}
   144  	// encode contracts
   145  	if err := enc.Encode(uint64(len(f.contracts))); err != nil {
   146  		return err
   147  	}
   148  	for _, c := range f.contracts {
   149  		if err := enc.Encode(c); err != nil {
   150  			return err
   151  		}
   152  	}
   153  	return nil
   154  }
   155  
   156  // UnmarshalSia implements the encoding.SiaUnmarshaler interface,
   157  // reconstructing a file from the encoded bytes read from r.
   158  func (f *file) UnmarshalSia(r io.Reader) error {
   159  	dec := encoding.NewDecoder(r, 100e6)
   160  
   161  	// COMPATv0.4.3 - decode bytesUploaded and chunksUploaded into dummy vars.
   162  	var bytesUploaded, chunksUploaded uint64
   163  
   164  	// Decode easy fields.
   165  	err := dec.DecodeAll(
   166  		&f.name,
   167  		&f.size,
   168  		&f.masterKey,
   169  		&f.pieceSize,
   170  		&f.mode,
   171  		&bytesUploaded,
   172  		&chunksUploaded,
   173  	)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	f.staticUID = persist.RandomSuffix()
   178  
   179  	// Decode erasure coder.
   180  	var codeType string
   181  	if err := dec.Decode(&codeType); err != nil {
   182  		return err
   183  	}
   184  	switch codeType {
   185  	case "Reed-Solomon":
   186  		var nData, nParity uint64
   187  		err = dec.DecodeAll(
   188  			&nData,
   189  			&nParity,
   190  		)
   191  		if err != nil {
   192  			return err
   193  		}
   194  		rsc, err := skymodules.NewRSCode(int(nData), int(nParity))
   195  		if err != nil {
   196  			return err
   197  		}
   198  		f.erasureCode = rsc
   199  	default:
   200  		return errors.New("unrecognized erasure code type: " + codeType)
   201  	}
   202  
   203  	// Decode contracts.
   204  	var nContracts uint64
   205  	if err := dec.Decode(&nContracts); err != nil {
   206  		return err
   207  	}
   208  	f.contracts = make(map[types.FileContractID]fileContract)
   209  	var contract fileContract
   210  	for i := uint64(0); i < nContracts; i++ {
   211  		if err := dec.Decode(&contract); err != nil {
   212  			return err
   213  		}
   214  		f.contracts[contract.ID] = contract
   215  	}
   216  	return nil
   217  }
   218  
   219  // loadSiaFiles walks through the directory searching for siafiles and loading
   220  // them into memory.
   221  func (r *Renter) compatV137ConvertSiaFiles(tracking map[string]v137TrackedFile, oldContracts []skymodules.RenterContract) error {
   222  	// Recursively convert all files found in renter directory.
   223  	err := filepath.Walk(r.persistDir, func(path string, info os.FileInfo, err error) error {
   224  		// This error is non-nil if filepath.Walk couldn't stat a file or
   225  		// folder.
   226  		if err != nil {
   227  			r.staticLog.Println("WARN: could not stat file or folder during walk:", err)
   228  			return nil
   229  		}
   230  
   231  		// Skip folders and non-sia files.
   232  		if info.IsDir() || filepath.Ext(path) != skymodules.SiaFileExtension {
   233  			return nil
   234  		}
   235  
   236  		// Check if file was already converted.
   237  		_, err = siafile.LoadSiaFile(path, r.staticWAL)
   238  		if err == nil {
   239  			return nil
   240  		}
   241  
   242  		// Open the file.
   243  		file, err := os.Open(path)
   244  		if err != nil {
   245  			return errors.AddContext(err, "unable to open file for conversion"+path)
   246  		}
   247  
   248  		// Load the file contents into the renter.
   249  		_, err = r.compatV137loadSiaFilesFromReader(file, tracking, oldContracts)
   250  		if err != nil {
   251  			err = errors.AddContext(err, "unable to load v137 siafiles from reader")
   252  			return errors.Compose(err, file.Close())
   253  		}
   254  
   255  		// Close the file and delete it since it was converted.
   256  		if err := file.Close(); err != nil {
   257  			return err
   258  		}
   259  		return os.Remove(path)
   260  	})
   261  	if err != nil {
   262  		return err
   263  	}
   264  	// Cleanup folders in the renter subdir.
   265  	fis, err := ioutil.ReadDir(r.persistDir)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	for _, fi := range fis {
   270  		// Ignore files.
   271  		if !fi.IsDir() {
   272  			continue
   273  		}
   274  		// Skip siafiles and contracts folders.
   275  		if fi.Name() == skymodules.FileSystemRoot || fi.Name() == "contracts" {
   276  			continue
   277  		}
   278  		// Delete the folder.
   279  		if err := os.RemoveAll(filepath.Join(r.persistDir, fi.Name())); err != nil {
   280  			return err
   281  		}
   282  	}
   283  	return nil
   284  }
   285  
   286  // v137FileToSiaFile converts a legacy file to a SiaFile. Fields that can't be
   287  // populated using the legacy file remain blank.
   288  func (r *Renter) v137FileToSiaFile(f *file, repairPath string, oldContracts []skymodules.RenterContract) (*filesystem.FileNode, error) {
   289  	// Create a mapping of contract ids to host keys.
   290  	contracts := r.staticHostContractor.Contracts()
   291  	idToPk := make(map[types.FileContractID]types.SiaPublicKey)
   292  	for _, c := range contracts {
   293  		idToPk[c.ID] = c.HostPublicKey
   294  	}
   295  	// Add old contracts to the mapping too.
   296  	for _, c := range oldContracts {
   297  		idToPk[c.ID] = c.HostPublicKey
   298  	}
   299  
   300  	fileData := siafile.FileData{
   301  		Name:        f.name,
   302  		FileSize:    f.size,
   303  		MasterKey:   f.masterKey,
   304  		ErasureCode: f.erasureCode,
   305  		RepairPath:  repairPath,
   306  		PieceSize:   f.pieceSize,
   307  		Mode:        os.FileMode(f.mode),
   308  		Deleted:     f.deleted,
   309  		UID:         siafile.SiafileUID(f.staticUID),
   310  	}
   311  	chunks := make([]siafile.FileChunk, f.numChunks())
   312  	for i := 0; i < len(chunks); i++ {
   313  		chunks[i].Pieces = make([][]siafile.Piece, f.erasureCode.NumPieces())
   314  	}
   315  	for _, contract := range f.contracts {
   316  		pk, exists := idToPk[contract.ID]
   317  		if !exists {
   318  			r.staticLog.Printf("Couldn't find pubKey for contract %v with WindowStart %v",
   319  				contract.ID, contract.WindowStart)
   320  			continue
   321  		}
   322  
   323  		for _, piece := range contract.Pieces {
   324  			// Make sure we don't add the same piece on the same host multiple
   325  			// times.
   326  			duplicate := false
   327  			for _, p := range chunks[piece.Chunk].Pieces[piece.Piece] {
   328  				if p.HostPubKey.Equals(pk) {
   329  					duplicate = true
   330  					break
   331  				}
   332  			}
   333  			if duplicate {
   334  				continue
   335  			}
   336  			chunks[piece.Chunk].Pieces[piece.Piece] = append(chunks[piece.Chunk].Pieces[piece.Piece], siafile.Piece{
   337  				HostPubKey: pk,
   338  				MerkleRoot: piece.MerkleRoot,
   339  			})
   340  		}
   341  	}
   342  	fileData.Chunks = chunks
   343  	return r.staticFileSystem.NewSiaFileFromLegacyData(fileData)
   344  }
   345  
   346  // compatV137LoadSiaFilesFromReader reads .sia data from reader and registers
   347  // the contained files in the renter. It returns the nicknames of the loaded
   348  // files.
   349  func (r *Renter) compatV137loadSiaFilesFromReader(reader io.Reader, tracking map[string]v137TrackedFile, oldContracts []skymodules.RenterContract) ([]string, error) {
   350  	// read header
   351  	var header [15]byte
   352  	var version string
   353  	var numFiles uint64
   354  	err := encoding.NewDecoder(reader, encoding.DefaultAllocLimit).DecodeAll(
   355  		&header,
   356  		&version,
   357  		&numFiles,
   358  	)
   359  	if err != nil {
   360  		return nil, errors.AddContext(err, "unable to read header")
   361  	} else if header != shareHeader {
   362  		return nil, ErrBadFile
   363  	} else if version != shareVersion {
   364  		return nil, ErrIncompatible
   365  	}
   366  
   367  	// Create decompressor.
   368  	unzip, err := gzip.NewReader(reader)
   369  	if err != nil {
   370  		return nil, errors.AddContext(err, "unable to create gzip decompressor")
   371  	}
   372  	dec := encoding.NewDecoder(unzip, 100e6)
   373  
   374  	// Read each file.
   375  	files := make([]*file, numFiles)
   376  	for i := range files {
   377  		files[i] = new(file)
   378  		err := dec.Decode(files[i])
   379  		if err != nil {
   380  			return nil, errors.AddContext(err, "unable to decode file")
   381  		}
   382  
   383  		// Make sure the file's name does not conflict with existing files.
   384  		dupCount := 0
   385  		origName := files[i].name
   386  		for {
   387  			siaPath, err := skymodules.NewSiaPath(files[i].name)
   388  			if err != nil {
   389  				return nil, err
   390  			}
   391  			exists, _ := r.staticFileSystem.FileExists(siaPath)
   392  			if !exists {
   393  				break
   394  			}
   395  			dupCount++
   396  			files[i].name = origName + "_" + strconv.Itoa(dupCount)
   397  		}
   398  	}
   399  
   400  	// Add files to renter.
   401  	names := make([]string, numFiles)
   402  	for i, f := range files {
   403  		// Figure out the repair path.
   404  		var repairPath string
   405  		tf, ok := tracking[f.name]
   406  		if ok {
   407  			repairPath = tf.RepairPath
   408  		}
   409  		// v137FileToSiaFile adds siafile to the SiaFileSet so it does not need to
   410  		// be returned here
   411  		entry, err := r.v137FileToSiaFile(f, repairPath, oldContracts)
   412  		if err != nil {
   413  			return nil, errors.AddContext(err, fmt.Sprintf("unable to transform old file %v to new file", repairPath))
   414  		}
   415  		if entry.NumChunks() < 1 {
   416  			return nil, errors.AddContext(err, "new file has invalid number of chunks")
   417  		}
   418  		names[i] = f.name
   419  		err = entry.Close()
   420  		if err != nil {
   421  			return nil, errors.AddContext(err, "failed to close file")
   422  		}
   423  	}
   424  	return names, err
   425  }
   426  
   427  // convertPersistVersionFrom140To142 upgrades a legacy persist file to the next
   428  // version, converting the old filesystem to the new one.
   429  func (r *Renter) convertPersistVersionFrom140To142(path string) error {
   430  	metadata := persist.Metadata{
   431  		Header:  settingsMetadata.Header,
   432  		Version: persistVersion140,
   433  	}
   434  	var p persistence
   435  	err := persist.LoadJSON(metadata, &p, path)
   436  	if err != nil {
   437  		return errors.AddContext(err, "could not load json")
   438  	}
   439  	// Rename siafiles folder to fs/home/user and snapshots to fs/snapshots.
   440  	fsRoot := filepath.Join(r.persistDir, skymodules.FileSystemRoot)
   441  	newHomePath := skymodules.HomeFolder.SiaDirSysPath(fsRoot)
   442  	newSiaFilesPath := skymodules.UserFolder.SiaDirSysPath(fsRoot)
   443  	newSnapshotsPath := skymodules.BackupFolder.SiaDirSysPath(fsRoot)
   444  	if err := os.MkdirAll(newHomePath, 0700); err != nil {
   445  		return errors.AddContext(err, "failed to create new home dir")
   446  	}
   447  	if err := os.Rename(filepath.Join(r.persistDir, "siafiles"), newSiaFilesPath); err != nil && !os.IsNotExist(err) {
   448  		return errors.AddContext(err, "failed to rename legacy siafiles folder")
   449  	}
   450  	if err := os.Rename(filepath.Join(r.persistDir, "snapshots"), newSnapshotsPath); err != nil && !os.IsNotExist(err) {
   451  		return errors.AddContext(err, "failed to rename legacy snapshots dir")
   452  	}
   453  	// Save metadata with updated version
   454  	metadata.Version = persistVersion142
   455  	err = persist.SaveJSON(metadata, p, path)
   456  	if err != nil {
   457  		return errors.AddContext(err, "could not save json")
   458  	}
   459  	return nil
   460  }
   461  
   462  // convertPersistVersionFrom133To140 upgrades a legacy persist file to the next
   463  // version, converting legacy SiaFiles in the process.
   464  func (r *Renter) convertPersistVersionFrom133To140(path string, oldContracts []skymodules.RenterContract) error {
   465  	metadata := persist.Metadata{
   466  		Header:  settingsMetadata.Header,
   467  		Version: persistVersion133,
   468  	}
   469  	p := v137Persistence{
   470  		Tracking: make(map[string]v137TrackedFile),
   471  	}
   472  
   473  	err := persist.LoadJSON(metadata, &p, path)
   474  	if err != nil {
   475  		return errors.AddContext(err, "could not load json")
   476  	}
   477  	metadata.Version = persistVersion140
   478  	// Load potential legacy SiaFiles.
   479  	if err := r.compatV137ConvertSiaFiles(p.Tracking, oldContracts); err != nil {
   480  		return errors.AddContext(err, "conversion from v137 failed")
   481  	}
   482  	err = persist.SaveJSON(metadata, p, path)
   483  	if err != nil {
   484  		return errors.AddContext(err, "could not save json")
   485  	}
   486  	return nil
   487  }
   488  
   489  // convertPersistVersionFrom040to133 upgrades a legacy persist file to the next
   490  // version, adding new fields with their default values.
   491  func convertPersistVersionFrom040To133(path string) error {
   492  	metadata := persist.Metadata{
   493  		Header:  settingsMetadata.Header,
   494  		Version: persistVersion040,
   495  	}
   496  	p := persistence{}
   497  
   498  	err := persist.LoadJSON(metadata, &p, path)
   499  	if err != nil {
   500  		return err
   501  	}
   502  	metadata.Version = persistVersion133
   503  	p.MaxDownloadSpeed = DefaultMaxDownloadSpeed
   504  	p.MaxUploadSpeed = DefaultMaxUploadSpeed
   505  	return persist.SaveJSON(metadata, p, path)
   506  }