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 }