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