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 }