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 }