gitlab.com/jokerrs1/Sia@v1.3.2/modules/renter/files.go (about)

     1  package renter
     2  
     3  import (
     4  	"errors"
     5  	"math"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  
    10  	"github.com/NebulousLabs/Sia/build"
    11  	"github.com/NebulousLabs/Sia/crypto"
    12  	"github.com/NebulousLabs/Sia/modules"
    13  	"github.com/NebulousLabs/Sia/persist"
    14  	"github.com/NebulousLabs/Sia/types"
    15  )
    16  
    17  var (
    18  	// ErrEmptyFilename is an error when filename is empty
    19  	ErrEmptyFilename = errors.New("filename must be a nonempty string")
    20  	// ErrPathOverload is an error when a file already exists at that location
    21  	ErrPathOverload = errors.New("a file already exists at that location")
    22  	// ErrUnknownPath is an error when a file cannot be found with the given path
    23  	ErrUnknownPath = errors.New("no file known with that path")
    24  )
    25  
    26  // A file is a single file that has been uploaded to the network. Files are
    27  // split into equal-length chunks, which are then erasure-coded into pieces.
    28  // Each piece is separately encrypted, using a key derived from the file's
    29  // master key. The pieces are uploaded to hosts in groups, such that one file
    30  // contract covers many pieces.
    31  type file struct {
    32  	name        string
    33  	size        uint64 // Static - can be accessed without lock.
    34  	contracts   map[types.FileContractID]fileContract
    35  	masterKey   crypto.TwofishKey    // Static - can be accessed without lock.
    36  	erasureCode modules.ErasureCoder // Static - can be accessed without lock.
    37  	pieceSize   uint64               // Static - can be accessed without lock.
    38  	mode        uint32               // actually an os.FileMode
    39  
    40  	staticUID string // A UID assigned to the file when it gets created.
    41  
    42  	mu sync.RWMutex
    43  }
    44  
    45  // A fileContract is a contract covering an arbitrary number of file pieces.
    46  // Chunk/Piece metadata is used to split the raw contract data appropriately.
    47  type fileContract struct {
    48  	ID     types.FileContractID
    49  	IP     modules.NetAddress
    50  	Pieces []pieceData
    51  
    52  	WindowStart types.BlockHeight
    53  }
    54  
    55  // pieceData contains the metadata necessary to request a piece from a
    56  // fetcher.
    57  //
    58  // TODO: Add an 'Unavailable' flag that can be set if the host loses the piece.
    59  // Some TODOs exist in 'repair.go' related to this field.
    60  type pieceData struct {
    61  	Chunk      uint64      // which chunk the piece belongs to
    62  	Piece      uint64      // the index of the piece in the chunk
    63  	MerkleRoot crypto.Hash // the Merkle root of the piece
    64  }
    65  
    66  // deriveKey derives the key used to encrypt and decrypt a specific file piece.
    67  func deriveKey(masterKey crypto.TwofishKey, chunkIndex, pieceIndex uint64) crypto.TwofishKey {
    68  	return crypto.TwofishKey(crypto.HashAll(masterKey, chunkIndex, pieceIndex))
    69  }
    70  
    71  // staticChunkSize returns the size of one chunk.
    72  func (f *file) staticChunkSize() uint64 {
    73  	return f.pieceSize * uint64(f.erasureCode.MinPieces())
    74  }
    75  
    76  // numChunks returns the number of chunks that f was split into.
    77  func (f *file) numChunks() uint64 {
    78  	// empty files still need at least one chunk
    79  	if f.size == 0 {
    80  		return 1
    81  	}
    82  	n := f.size / f.staticChunkSize()
    83  	// last chunk will be padded, unless chunkSize divides file evenly.
    84  	if f.size%f.staticChunkSize() != 0 {
    85  		n++
    86  	}
    87  	return n
    88  }
    89  
    90  // available indicates whether the file is ready to be downloaded.
    91  func (f *file) available(contractStatus func(types.FileContractID) (offline bool, goodForRenew bool)) bool {
    92  	chunkPieces := make([]int, f.numChunks())
    93  	for _, fc := range f.contracts {
    94  		if offline, _ := contractStatus(fc.ID); offline {
    95  			continue
    96  		}
    97  		for _, p := range fc.Pieces {
    98  			chunkPieces[p.Chunk]++
    99  		}
   100  	}
   101  	for _, n := range chunkPieces {
   102  		if n < f.erasureCode.MinPieces() {
   103  			return false
   104  		}
   105  	}
   106  	return true
   107  }
   108  
   109  // uploadedBytes indicates how many bytes of the file have been uploaded via
   110  // current file contracts. Note that this includes padding and redundancy, so
   111  // uploadedBytes can return a value much larger than the file's original filesize.
   112  func (f *file) uploadedBytes() uint64 {
   113  	var uploaded uint64
   114  	for _, fc := range f.contracts {
   115  		// Note: we need to multiply by SectorSize here instead of
   116  		// f.pieceSize because the actual bytes uploaded include overhead
   117  		// from TwoFish encryption
   118  		uploaded += uint64(len(fc.Pieces)) * modules.SectorSize
   119  	}
   120  	return uploaded
   121  }
   122  
   123  // uploadProgress indicates what percentage of the file (plus redundancy) has
   124  // been uploaded. Note that a file may be Available long before UploadProgress
   125  // reaches 100%, and UploadProgress may report a value greater than 100%.
   126  func (f *file) uploadProgress() float64 {
   127  	uploaded := f.uploadedBytes()
   128  	desired := modules.SectorSize * uint64(f.erasureCode.NumPieces()) * f.numChunks()
   129  
   130  	return math.Min(100*(float64(uploaded)/float64(desired)), 100)
   131  }
   132  
   133  // redundancy returns the redundancy of the least redundant chunk. A file
   134  // becomes available when this redundancy is >= 1. Assumes that every piece is
   135  // unique within a file contract. -1 is returned if the file has size 0. It
   136  // takes one argument, a map of offline contracts for this file.
   137  func (f *file) redundancy(contractStatus func(types.FileContractID) (bool, bool)) float64 {
   138  	if f.size == 0 {
   139  		return -1
   140  	}
   141  	piecesPerChunk := make([]int, f.numChunks())
   142  	piecesPerChunkNoRenew := make([]int, f.numChunks())
   143  	// If the file has non-0 size then the number of chunks should also be
   144  	// non-0. Therefore the f.size == 0 conditional block above must appear
   145  	// before this check.
   146  	if len(piecesPerChunk) == 0 {
   147  		build.Critical("cannot get redundancy of a file with 0 chunks")
   148  		return -1
   149  	}
   150  	for _, fc := range f.contracts {
   151  		offline, goodForRenew := contractStatus(fc.ID)
   152  
   153  		// do not count pieces from the contract if the contract is offline
   154  		if offline {
   155  			continue
   156  		}
   157  		for _, p := range fc.Pieces {
   158  			if goodForRenew {
   159  				piecesPerChunk[p.Chunk]++
   160  			}
   161  			piecesPerChunkNoRenew[p.Chunk]++
   162  		}
   163  	}
   164  	// Find the chunk with the least finished pieces counting only pieces of
   165  	// contracts that are goodForRenew.
   166  	minPieces := piecesPerChunk[0]
   167  	for _, numPieces := range piecesPerChunk {
   168  		if numPieces < minPieces {
   169  			minPieces = numPieces
   170  		}
   171  	}
   172  	// Find the chunk with the least finished pieces including pieces from
   173  	// contracts that are not good for renewal.
   174  	minPiecesNoRenew := piecesPerChunkNoRenew[0]
   175  	for _, numPieces := range piecesPerChunkNoRenew {
   176  		if numPieces < minPiecesNoRenew {
   177  			minPiecesNoRenew = numPieces
   178  		}
   179  	}
   180  	// If the redundancy is smaller than 1x we return the redundancy that
   181  	// includes contracts that are not good for renewal. The reason for this is
   182  	// a better user experience. If the renter operates correctly, redundancy
   183  	// should never go above numPieces / minPieces and redundancyNoRenew should
   184  	// never go below 1.
   185  	redundancy := float64(minPieces) / float64(f.erasureCode.MinPieces())
   186  	redundancyNoRenew := float64(minPiecesNoRenew) / float64(f.erasureCode.MinPieces())
   187  	if redundancy < 1 {
   188  		return redundancyNoRenew
   189  	}
   190  	return redundancy
   191  }
   192  
   193  // expiration returns the lowest height at which any of the file's contracts
   194  // will expire.
   195  func (f *file) expiration() types.BlockHeight {
   196  	if len(f.contracts) == 0 {
   197  		return 0
   198  	}
   199  	lowest := ^types.BlockHeight(0)
   200  	for _, fc := range f.contracts {
   201  		if fc.WindowStart < lowest {
   202  			lowest = fc.WindowStart
   203  		}
   204  	}
   205  	return lowest
   206  }
   207  
   208  // newFile creates a new file object.
   209  func newFile(name string, code modules.ErasureCoder, pieceSize, fileSize uint64) *file {
   210  	return &file{
   211  		name:        name,
   212  		size:        fileSize,
   213  		contracts:   make(map[types.FileContractID]fileContract),
   214  		masterKey:   crypto.GenerateTwofishKey(),
   215  		erasureCode: code,
   216  		pieceSize:   pieceSize,
   217  
   218  		staticUID: persist.RandomSuffix(),
   219  	}
   220  }
   221  
   222  // DeleteFile removes a file entry from the renter and deletes its data from
   223  // the hosts it is stored on.
   224  //
   225  // TODO: The data is not cleared from any contracts where the host is not
   226  // immediately online.
   227  func (r *Renter) DeleteFile(nickname string) error {
   228  	lockID := r.mu.Lock()
   229  	f, exists := r.files[nickname]
   230  	if !exists {
   231  		r.mu.Unlock(lockID)
   232  		return ErrUnknownPath
   233  	}
   234  	delete(r.files, nickname)
   235  	delete(r.tracking, nickname)
   236  
   237  	err := persist.RemoveFile(filepath.Join(r.persistDir, f.name+ShareExtension))
   238  	if err != nil {
   239  		r.log.Println("WARN: couldn't remove file :", err)
   240  	}
   241  
   242  	r.saveSync()
   243  	r.mu.Unlock(lockID)
   244  
   245  	// delete the file's associated contract data.
   246  	f.mu.Lock()
   247  	defer f.mu.Unlock()
   248  
   249  	// TODO: delete the sectors of the file as well.
   250  
   251  	return nil
   252  }
   253  
   254  // FileList returns all of the files that the renter has.
   255  func (r *Renter) FileList() []modules.FileInfo {
   256  	var files []*file
   257  	lockID := r.mu.RLock()
   258  	for _, f := range r.files {
   259  		files = append(files, f)
   260  	}
   261  	r.mu.RUnlock(lockID)
   262  
   263  	contractStatus := func(id types.FileContractID) (offline bool, goodForRenew bool) {
   264  		id = r.hostContractor.ResolveID(id)
   265  		cu, ok := r.hostContractor.ContractUtility(id)
   266  		offline = r.hostContractor.IsOffline(id)
   267  		goodForRenew = ok && cu.GoodForRenew
   268  		return
   269  	}
   270  
   271  	var fileList []modules.FileInfo
   272  	for _, f := range files {
   273  		lockID := r.mu.RLock()
   274  		f.mu.RLock()
   275  		renewing := true
   276  		var localPath string
   277  		tf, exists := r.tracking[f.name]
   278  		if exists {
   279  			localPath = tf.RepairPath
   280  		}
   281  		fileList = append(fileList, modules.FileInfo{
   282  			SiaPath:        f.name,
   283  			LocalPath:      localPath,
   284  			Filesize:       f.size,
   285  			Renewing:       renewing,
   286  			Available:      f.available(contractStatus),
   287  			Redundancy:     f.redundancy(contractStatus),
   288  			UploadedBytes:  f.uploadedBytes(),
   289  			UploadProgress: f.uploadProgress(),
   290  			Expiration:     f.expiration(),
   291  		})
   292  		f.mu.RUnlock()
   293  		r.mu.RUnlock(lockID)
   294  	}
   295  	return fileList
   296  }
   297  
   298  // RenameFile takes an existing file and changes the nickname. The original
   299  // file must exist, and there must not be any file that already has the
   300  // replacement nickname.
   301  func (r *Renter) RenameFile(currentName, newName string) error {
   302  	lockID := r.mu.Lock()
   303  	defer r.mu.Unlock(lockID)
   304  
   305  	err := validateSiapath(newName)
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	// Check that currentName exists and newName doesn't.
   311  	file, exists := r.files[currentName]
   312  	if !exists {
   313  		return ErrUnknownPath
   314  	}
   315  	_, exists = r.files[newName]
   316  	if exists {
   317  		return ErrPathOverload
   318  	}
   319  
   320  	// Modify the file and save it to disk.
   321  	file.mu.Lock()
   322  	file.name = newName
   323  	err = r.saveFile(file)
   324  	file.mu.Unlock()
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	// Update the entries in the renter.
   330  	delete(r.files, currentName)
   331  	r.files[newName] = file
   332  	if t, ok := r.tracking[currentName]; ok {
   333  		delete(r.tracking, currentName)
   334  		r.tracking[newName] = t
   335  	}
   336  	err = r.saveSync()
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	// Delete the old .sia file.
   342  	oldPath := filepath.Join(r.persistDir, currentName+ShareExtension)
   343  	return os.RemoveAll(oldPath)
   344  }