gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/snapshot.go (about)

     1  package renter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"sort"
    11  	"sync"
    12  	"time"
    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  	"go.sia.tech/siad/crypto"
    19  	"go.sia.tech/siad/modules"
    20  	"go.sia.tech/siad/types"
    21  
    22  	"gitlab.com/NebulousLabs/errors"
    23  	"gitlab.com/NebulousLabs/fastrand"
    24  )
    25  
    26  var (
    27  	// snapshotKeySpecifier is the specifier used for deriving the secret used
    28  	// to encrypt a snapshot from the RenterSeed.
    29  	snapshotKeySpecifier = types.NewSpecifier("snapshot")
    30  
    31  	// snapshotTableSpecifier is the specifier used to identify a snapshot entry
    32  	// table stored in a sector.
    33  	snapshotTableSpecifier = types.NewSpecifier("SnapshotTable")
    34  )
    35  
    36  var (
    37  	// errEmptyContract is returned when we are trying to read from an empty
    38  	// contract, this can be the case when downloading a snapshot from a host
    39  	// that does not have one yet.
    40  	errEmptyContract = errors.New("empty contract")
    41  
    42  	// maxSnapshotUploadTime defines the total amount of time that the renter
    43  	// will allocate to complete an upload of a snapshot .sia file to all hosts.
    44  	// This is done with each host in parallel, and the .sia file is not
    45  	// expected to exceed a few megabytes even for very large renters.
    46  	maxSnapshotUploadTime = build.Select(build.Var{
    47  		Standard: time.Minute * 15,
    48  		Dev:      time.Minute * 3,
    49  		Testing:  time.Minute,
    50  	}).(time.Duration)
    51  )
    52  
    53  // A snapshotEntry is an entry within the snapshot table, identifying both the
    54  // snapshot metadata and the other sectors on the host storing the snapshot
    55  // data.
    56  type snapshotEntry struct {
    57  	Name         [96]byte
    58  	UID          [16]byte
    59  	CreationDate types.Timestamp
    60  	Size         uint64         // size of snapshot .sia file
    61  	DataSectors  [4]crypto.Hash // pointers to sectors containing snapshot .sia file
    62  }
    63  
    64  // calcSnapshotUploadProgress calculates the upload progress of a snapshot.
    65  func calcSnapshotUploadProgress(fileUploadProgress float64, dotSiaUploadProgress float64) float64 {
    66  	return 0.8*fileUploadProgress + 0.2*dotSiaUploadProgress
    67  }
    68  
    69  // UploadedBackups returns the backups that the renter can download, along with
    70  // a list of which contracts are storing all known backups.
    71  func (r *Renter) UploadedBackups() ([]skymodules.UploadedBackup, []types.SiaPublicKey, error) {
    72  	if err := r.tg.Add(); err != nil {
    73  		return nil, nil, err
    74  	}
    75  	defer r.tg.Done()
    76  	id := r.mu.RLock()
    77  	defer r.mu.RUnlock(id)
    78  	backups := append([]skymodules.UploadedBackup(nil), r.persist.UploadedBackups...)
    79  	hosts := make([]types.SiaPublicKey, 0, len(r.persist.SyncedContracts))
    80  	for _, c := range r.staticHostContractor.Contracts() {
    81  		for _, id := range r.persist.SyncedContracts {
    82  			if c.ID == id {
    83  				hosts = append(hosts, c.HostPublicKey)
    84  				break
    85  			}
    86  		}
    87  	}
    88  	return backups, hosts, nil
    89  }
    90  
    91  // BackupsOnHost returns the backups stored on a particular host. This operation
    92  // can take multiple minutes if the renter is performing many other operations
    93  // on this host, however this operation is given high priority over other types
    94  // of operations.
    95  func (r *Renter) BackupsOnHost(hostKey types.SiaPublicKey) ([]skymodules.UploadedBackup, error) {
    96  	if err := r.tg.Add(); err != nil {
    97  		return nil, err
    98  	}
    99  	defer r.tg.Done()
   100  
   101  	// Find the relevant worker.
   102  	w, err := r.staticWorkerPool.callWorker(hostKey)
   103  	if err != nil {
   104  		return nil, errors.AddContext(err, "host not found in the worker table")
   105  	}
   106  
   107  	return w.FetchBackups(r.tg.StopCtx())
   108  }
   109  
   110  // UploadBackup creates a backup of the renter which is uploaded to the sia
   111  // network as a snapshot and can be retrieved using only the seed.
   112  func (r *Renter) UploadBackup(src, name string) error {
   113  	if err := r.tg.Add(); err != nil {
   114  		return err
   115  	}
   116  	defer r.tg.Done()
   117  	return r.managedUploadBackup(src, name)
   118  }
   119  
   120  // managedUploadBackup creates a backup of the renter which is uploaded to the
   121  // sia network as a snapshot and can be retrieved using only the seed.
   122  func (r *Renter) managedUploadBackup(src, name string) error {
   123  	if len(name) > 96 {
   124  		return errors.New("name is too long")
   125  	}
   126  
   127  	// Check if snapshot already exists.
   128  	if r.managedSnapshotExists(name) {
   129  		s := fmt.Sprintf("snapshot with name '%s' already exists", name)
   130  		return errors.AddContext(filesystem.ErrExists, s)
   131  	}
   132  
   133  	// Open the backup for uploading.
   134  	backup, err := os.Open(src)
   135  	if err != nil {
   136  		return errors.AddContext(err, "failed to open backup for uploading")
   137  	}
   138  	defer func() {
   139  		err = errors.Compose(err, backup.Close())
   140  	}()
   141  
   142  	// Prepare the siapath.
   143  	sp, err := skymodules.BackupFolder.Join(name)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	// Create upload params with high redundancy.
   148  	allowance := r.staticHostContractor.Allowance()
   149  	dataPieces := allowance.Hosts / 10
   150  	if dataPieces == 0 {
   151  		dataPieces = 1
   152  	}
   153  	parityPieces := allowance.Hosts - dataPieces
   154  	ec, err := skymodules.NewRSSubCode(int(dataPieces), int(parityPieces), crypto.SegmentSize)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	up := skymodules.FileUploadParams{
   159  		SiaPath:     sp,
   160  		ErasureCode: ec,
   161  		Force:       false,
   162  
   163  		CipherType: crypto.TypeDefaultRenter,
   164  	}
   165  	// Begin uploading the backup. When the upload finishes, the backup .sia
   166  	// file will be uploaded by r.threadedSynchronizeSnapshots and then deleted.
   167  	fileNode, err := r.callUploadStreamFromReader(r.tg.StopCtx(), up, backup)
   168  	if err != nil {
   169  		return errors.AddContext(err, "failed to upload backup")
   170  	}
   171  	err = fileNode.Close()
   172  	if err != nil {
   173  		return errors.AddContext(err, "unable to close fileNode while uploading a backup")
   174  	}
   175  	// Save initial snapshot entry.
   176  	meta := skymodules.UploadedBackup{
   177  		Name:           name,
   178  		CreationDate:   types.CurrentTimestamp(),
   179  		Size:           0,
   180  		UploadProgress: 0,
   181  	}
   182  	fastrand.Read(meta.UID[:])
   183  	if err := r.managedSaveSnapshot(meta); err != nil {
   184  		return err
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  // DownloadBackup downloads the specified backup.
   191  func (r *Renter) DownloadBackup(dst string, name string) (err error) {
   192  	if err := r.tg.Add(); err != nil {
   193  		return err
   194  	}
   195  	defer r.tg.Done()
   196  	// Open the destination.
   197  	dstFile, err := os.Create(dst)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	defer func() {
   202  		err = errors.Compose(err, dstFile.Close())
   203  	}()
   204  	// search for backup
   205  	if len(name) > 96 {
   206  		return errors.New("no record of a backup with that name")
   207  	}
   208  	var uid [16]byte
   209  	var found bool
   210  	id := r.mu.RLock()
   211  	for _, b := range r.persist.UploadedBackups {
   212  		if b.Name == name {
   213  			uid = b.UID
   214  			found = true
   215  			break
   216  		}
   217  	}
   218  	r.mu.RUnlock(id)
   219  	if !found {
   220  		return errors.New("no record of a backup with that name")
   221  	}
   222  	// Download snapshot's .sia file.
   223  	_, dotSia, err := r.managedDownloadSnapshot(uid)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	// Store it in the backup file set.
   228  	backupSiaPath, err := skymodules.BackupFolder.Join(name)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	if err := r.staticFileSystem.WriteFile(backupSiaPath, dotSia, 0666); err != nil {
   233  		return err
   234  	}
   235  	// Load the .sia file.
   236  	siaPath, err := skymodules.BackupFolder.Join(name)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	entry, err := r.staticFileSystem.OpenSiaFile(siaPath)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	defer func() {
   245  		err = errors.Compose(err, entry.Close())
   246  	}()
   247  	// Use .sia file to download snapshot.
   248  	snap, err := entry.Snapshot(siaPath)
   249  	if err != nil {
   250  		return err
   251  	}
   252  	s := r.managedStreamer(snap, false)
   253  	_, err = io.Copy(dstFile, s)
   254  	return errors.Compose(err, s.Close())
   255  }
   256  
   257  // managedSnapshotExists returns true if a snapshot with a given name already
   258  // exists.
   259  func (r *Renter) managedSnapshotExists(name string) bool {
   260  	id := r.mu.Lock()
   261  	defer r.mu.Unlock(id)
   262  	for _, ub := range r.persist.UploadedBackups {
   263  		if ub.Name == name {
   264  			return true
   265  		}
   266  	}
   267  	return false
   268  }
   269  
   270  // managedSaveSnapshot saves snapshot metadata to disk.
   271  func (r *Renter) managedSaveSnapshot(meta skymodules.UploadedBackup) error {
   272  	id := r.mu.Lock()
   273  	defer r.mu.Unlock(id)
   274  	// Check whether we've already saved this snapshot.
   275  	for i, ub := range r.persist.UploadedBackups {
   276  		if ub.UID == meta.UID {
   277  			if ub == meta {
   278  				// nothing changed
   279  				return nil
   280  			}
   281  			// something changed; overwrite existing entry
   282  			r.persist.UploadedBackups[i] = meta
   283  			return r.saveSync()
   284  		}
   285  	}
   286  	// Append the new snapshot.
   287  	r.persist.UploadedBackups = append(r.persist.UploadedBackups, meta)
   288  	// Trim the set of snapshots if necessary. Hosts can only store a finite
   289  	// number of snapshots, so if we exceed that number, we kick out the oldest
   290  	// snapshot. We check the size by encoding a slice of snapshotEntrys and
   291  	// removing elements from the slice until the encoded size fits on the host.
   292  	entryTable := make([]snapshotEntry, len(r.persist.UploadedBackups))
   293  	for len(encoding.Marshal(entryTable)) > int(modules.SectorSize) {
   294  		entryTable = entryTable[1:]
   295  	}
   296  	// Sort by CreationDate (youngest-to-oldest) and remove excess elements from
   297  	// the end.
   298  	sort.Slice(r.persist.UploadedBackups, func(i, j int) bool {
   299  		return r.persist.UploadedBackups[i].CreationDate > r.persist.UploadedBackups[j].CreationDate
   300  	})
   301  	r.persist.UploadedBackups = r.persist.UploadedBackups[:len(entryTable)]
   302  	// Save the result.
   303  	if err := r.saveSync(); err != nil {
   304  		return err
   305  	}
   306  	return nil
   307  }
   308  
   309  // managedDownloadSnapshotTable will fetch the snapshot table from the host.
   310  func (r *Renter) managedDownloadSnapshotTable(host *worker) ([]snapshotEntry, error) {
   311  	// Get the wallet seed.
   312  	ws, _, err := r.staticWallet.PrimarySeed()
   313  	if err != nil {
   314  		return nil, errors.AddContext(err, "failed to get wallet's primary seed")
   315  	}
   316  	// Derive the renter seed and wipe the memory once we are done using it.
   317  	rs := skymodules.DeriveRenterSeed(ws)
   318  	defer fastrand.Read(rs[:])
   319  	// Derive the secret and wipe it afterwards.
   320  	secret := crypto.HashAll(rs, snapshotKeySpecifier)
   321  	defer fastrand.Read(secret[:])
   322  
   323  	// Create an empty entryTable
   324  	var entryTable []snapshotEntry
   325  
   326  	// Fetch the contract and see if it's empty, if that is the case return an
   327  	// empty entryTable and appropriate error.
   328  	contract, ok := r.staticHostContractor.ContractByPublicKey(host.staticHostPubKey)
   329  	if !ok {
   330  		return nil, errors.New("failed to host contract")
   331  	}
   332  	if contract.Size() == 0 {
   333  		return entryTable, errEmptyContract
   334  	}
   335  
   336  	// Download the table of snapshots that the host is storing.
   337  	tableSector, err := host.ReadOffset(r.tg.StopCtx(), categorySnapshotDownload, 0, modules.SectorSize)
   338  	if err != nil {
   339  		return nil, errors.AddContext(err, "unable to perform a download by index on this contract")
   340  	}
   341  
   342  	// decrypt the table
   343  	c, _ := crypto.NewSiaKey(crypto.TypeThreefish, secret[:])
   344  	encTable, err := c.DecryptBytesInPlace(tableSector, 0)
   345  	if err != nil || !bytes.Equal(encTable[:16], snapshotTableSpecifier[:]) {
   346  		// either the first sector was not an entry table, or it got corrupted
   347  		// somehow; either way, it's not retrievable, so we'll treat this as
   348  		// equivalent to having no entry table at all. This is not an error; it
   349  		// just means that when we upload a snapshot, we'll have to create a new
   350  		// table.
   351  		return nil, errors.AddContext(err, "error decrypting bytes")
   352  	}
   353  
   354  	if err := encoding.Unmarshal(encTable[16:], &entryTable); err != nil {
   355  		return nil, errors.AddContext(err, "error unmarshaling the entry table")
   356  	}
   357  	return entryTable, nil
   358  }
   359  
   360  // managedUploadSnapshot uploads a snapshot .sia file to all hosts.
   361  func (r *Renter) managedUploadSnapshot(meta skymodules.UploadedBackup, dotSia []byte) error {
   362  	// Grab all of the workers that are good for upload.
   363  	var total int
   364  	workers := r.staticWorkerPool.callWorkers()
   365  	for i := 0; i < len(workers); i++ {
   366  		if workers[i].staticCache().staticContractUtility.GoodForUpload {
   367  			workers[total] = workers[i]
   368  			total++
   369  		}
   370  	}
   371  	workers = workers[:total]
   372  
   373  	// Submit a job to each worker. Make sure the response channel has enough
   374  	// room in the buffer for all results, this way workers are not being
   375  	// blocked when returning their results.
   376  	maxWait, cancel := context.WithTimeout(r.tg.StopCtx(), maxSnapshotUploadTime)
   377  	defer cancel()
   378  	responseChan := make(chan *jobUploadSnapshotResponse, len(workers))
   379  	queued := 0
   380  	for _, w := range workers {
   381  		job := &jobUploadSnapshot{
   382  			staticSiaFileData: dotSia,
   383  
   384  			staticResponseChan: responseChan,
   385  
   386  			jobGeneric: newJobGeneric(maxWait, w.staticJobUploadSnapshotQueue, meta),
   387  		}
   388  
   389  		// If a job is not added correctly, count this as a failed response.
   390  		if w.staticJobUploadSnapshotQueue.callAdd(job) {
   391  			queued++
   392  		}
   393  	}
   394  
   395  	// Iteratively grab the responses from the workers.
   396  	responses := 0
   397  	successes := 0
   398  
   399  LOOP:
   400  	for responses < queued {
   401  		var resp *jobUploadSnapshotResponse
   402  		select {
   403  		case resp = <-responseChan:
   404  		case <-maxWait.Done():
   405  			break LOOP
   406  		}
   407  		responses++
   408  
   409  		// Update the progress.
   410  		pct := 100 * float64(responses) / float64(total)
   411  		meta.UploadProgress = calcSnapshotUploadProgress(100, pct)
   412  		err := r.managedSaveSnapshot(meta)
   413  		if err != nil {
   414  			r.staticLog.Println("Error saving snapshot during upload:", err)
   415  			continue
   416  		}
   417  
   418  		// Log any error.
   419  		if resp.staticErr != nil {
   420  			r.staticLog.Debugln("snapshot upload failed:", resp.staticErr)
   421  			continue
   422  		}
   423  		successes++
   424  	}
   425  
   426  	// Check for shutdown.
   427  	select {
   428  	case <-r.tg.StopChan():
   429  		return errors.New("renter is shutting down")
   430  	default:
   431  	}
   432  
   433  	// Check if there were too few successes to count this as a successful
   434  	// backup. A 1/3 success rate is really quite arbitrary, picked because it
   435  	// ~feels~ like that should be enough to give the user security, but really
   436  	// who knows. Like really we should probably be looking at the total number
   437  	// of hosts in the allowance and comparing against that.
   438  	if successes < total/3 {
   439  		r.staticLog.Printf("Unable to save snapshot effectively, wanted %v but only got %v successful snapshot backups", total, successes)
   440  		return fmt.Errorf("needed at least %v successes, only got %v", total/3, successes)
   441  	}
   442  
   443  	// Save the final version of the snapshot. Represent the progress at 100%
   444  	// even though not every host may have our snapshot. Really we only need 1
   445  	// working host to do a full recovery.
   446  	meta.UploadProgress = calcSnapshotUploadProgress(100, 100)
   447  	if err := r.managedSaveSnapshot(meta); err != nil {
   448  		return errors.AddContext(err, "error saving snapshot after upload completed")
   449  	}
   450  	return nil
   451  }
   452  
   453  // managedDownloadSnapshot downloads and returns the specified snapshot.
   454  func (r *Renter) managedDownloadSnapshot(uid [16]byte) (ub skymodules.UploadedBackup, dotSia []byte, err error) {
   455  	if err := r.tg.Add(); err != nil {
   456  		return skymodules.UploadedBackup{}, nil, err
   457  	}
   458  	defer r.tg.Done()
   459  
   460  	// Get the wallet seed.
   461  	ws, _, err := r.staticWallet.PrimarySeed()
   462  	if err != nil {
   463  		return skymodules.UploadedBackup{}, nil, errors.AddContext(err, "failed to get wallet's primary seed")
   464  	}
   465  	// Derive the renter seed and wipe the memory once we are done using it.
   466  	rs := skymodules.DeriveRenterSeed(ws)
   467  	defer fastrand.Read(rs[:])
   468  	// Derive the secret and wipe it afterwards.
   469  	secret := crypto.HashAll(rs, snapshotKeySpecifier)
   470  	defer fastrand.Read(secret[:])
   471  
   472  	// try downloading from each host in serial, prioritizing the hosts that are
   473  	// GoodForUpload, then GoodForRenew
   474  	contracts := r.staticHostContractor.Contracts()
   475  	sort.Slice(contracts, func(i, j int) bool {
   476  		if contracts[i].Utility.GoodForUpload == contracts[j].Utility.GoodForUpload {
   477  			return contracts[i].Utility.GoodForRenew && !contracts[j].Utility.GoodForRenew
   478  		}
   479  		return contracts[i].Utility.GoodForUpload && !contracts[j].Utility.GoodForUpload
   480  	})
   481  	for i := range contracts {
   482  		err := func() (err error) {
   483  			w, err := r.staticWorkerPool.callWorker(contracts[i].HostPublicKey)
   484  			if err != nil {
   485  				return err
   486  			}
   487  			// download the snapshot table
   488  			entryTable, err := w.DownloadSnapshotTable(r.tg.StopCtx())
   489  			if err != nil {
   490  				return err
   491  			}
   492  
   493  			// search for the desired snapshot
   494  			var entry *snapshotEntry
   495  			for j := range entryTable {
   496  				if entryTable[j].UID == uid {
   497  					entry = &entryTable[j]
   498  					break
   499  				}
   500  			}
   501  			if entry == nil {
   502  				return errors.New("entry table does not contain snapshot")
   503  			}
   504  			// download the entry
   505  			dotSia = nil
   506  			for _, root := range entry.DataSectors {
   507  				data, err := w.ReadSector(r.tg.StopCtx(), categorySnapshotDownload, root, 0, modules.SectorSize)
   508  				if err != nil {
   509  					return err
   510  				}
   511  				dotSia = append(dotSia, data...)
   512  				if uint64(len(dotSia)) >= entry.Size {
   513  					dotSia = dotSia[:entry.Size]
   514  					break
   515  				}
   516  			}
   517  			ub = skymodules.UploadedBackup{
   518  				Name:           string(bytes.TrimRight(entry.Name[:], types.RuneToString(0))),
   519  				UID:            entry.UID,
   520  				CreationDate:   entry.CreationDate,
   521  				Size:           entry.Size,
   522  				UploadProgress: 100,
   523  			}
   524  			return nil
   525  		}()
   526  		if err != nil {
   527  			r.staticLog.Printf("Downloading backup from host %v failed: %v", contracts[i].HostPublicKey, err)
   528  			continue
   529  		}
   530  		return ub, dotSia, nil
   531  	}
   532  	return skymodules.UploadedBackup{}, nil, errors.New("could not download backup from any host")
   533  }
   534  
   535  // threadedSynchronizeSnapshots continuously scans hosts to ensure that all
   536  // current hosts are storing all known snapshots.
   537  func (r *Renter) threadedSynchronizeSnapshots() {
   538  	if err := r.tg.Add(); err != nil {
   539  		return
   540  	}
   541  	defer r.tg.Done()
   542  	// calcOverlap takes a host's entry table and the set of known snapshots,
   543  	// and calculates which snapshots the host is missing and which snapshots it
   544  	// has that we don't.
   545  	calcOverlap := func(entryTable []snapshotEntry, known map[[16]byte]struct{}) (unknown []skymodules.UploadedBackup, missing [][16]byte) {
   546  		missingMap := make(map[[16]byte]struct{}, len(known))
   547  		for uid := range known {
   548  			missingMap[uid] = struct{}{}
   549  		}
   550  		for _, e := range entryTable {
   551  			if _, ok := known[e.UID]; !ok {
   552  				unknown = append(unknown, skymodules.UploadedBackup{
   553  					Name:           string(bytes.TrimRight(e.Name[:], types.RuneToString(0))),
   554  					UID:            e.UID,
   555  					CreationDate:   e.CreationDate,
   556  					Size:           e.Size,
   557  					UploadProgress: 100,
   558  				})
   559  			}
   560  			delete(missingMap, e.UID)
   561  		}
   562  		for uid := range missingMap {
   563  			missing = append(missing, uid)
   564  		}
   565  		return
   566  	}
   567  
   568  	// Build a set of which contracts are synced.
   569  	syncedContracts := make(map[types.FileContractID]struct{})
   570  	id := r.mu.RLock()
   571  	for _, fcid := range r.persist.SyncedContracts {
   572  		syncedContracts[fcid] = struct{}{}
   573  	}
   574  	r.mu.RUnlock(id)
   575  
   576  	for {
   577  		// Can't do anything if the wallet is locked.
   578  		if unlocked, _ := r.staticWallet.Unlocked(); !unlocked {
   579  			select {
   580  			case <-time.After(snapshotSyncSleepDuration):
   581  			case <-r.tg.StopChan():
   582  				return
   583  			}
   584  			continue
   585  		}
   586  		r.staticWorkerPool.callUpdate(r)
   587  
   588  		// First, process any snapshot siafiles that may have finished uploading.
   589  		root := skymodules.BackupFolder
   590  		var mu sync.Mutex
   591  		flf := func(info skymodules.FileInfo) {
   592  			// Make sure we only look at a single info at a time.
   593  			mu.Lock()
   594  			defer mu.Unlock()
   595  
   596  			// locate corresponding entry
   597  			id = r.mu.RLock()
   598  			var meta skymodules.UploadedBackup
   599  			found := false
   600  			for _, meta = range r.persist.UploadedBackups {
   601  				sp, _ := info.SiaPath.Rebase(skymodules.BackupFolder, skymodules.RootSiaPath())
   602  				if meta.Name == sp.String() {
   603  					found = true
   604  					break
   605  				}
   606  			}
   607  			r.mu.RUnlock(id)
   608  			if !found {
   609  				r.staticLog.Println("Could not locate entry for file in backup set")
   610  				return
   611  			}
   612  
   613  			// record current UploadProgress
   614  			meta.UploadProgress = calcSnapshotUploadProgress(info.UploadProgress, 0)
   615  			if err := r.managedSaveSnapshot(meta); err != nil {
   616  				r.staticLog.Println("Could not save upload progress:", err)
   617  				return
   618  			}
   619  
   620  			if skymodules.NeedsRepair(info.Health) {
   621  				// not ready for upload yet
   622  				return
   623  			}
   624  			r.staticLog.Println("Uploading snapshot", info.SiaPath)
   625  			err := func() error {
   626  				// Grab the entry for the uploaded backup's siafile.
   627  				entry, err := r.staticFileSystem.OpenSiaFile(info.SiaPath)
   628  				if err != nil {
   629  					return errors.AddContext(err, "failed to get entry for snapshot")
   630  				}
   631  				// Read the siafile from disk.
   632  				sr, err := entry.SnapshotReader()
   633  				if err != nil {
   634  					return errors.Compose(err, entry.Close())
   635  				}
   636  				// NOTE: The snapshot reader needs to be closed before
   637  				// entry.Close is called, and unfortunately also before
   638  				// managedUploadSnapshot is called, because the snapshot reader
   639  				// holds a lock on the underlying dotsia, which is also actively
   640  				// a part of the repair system. The repair system locks the
   641  				// renter then tries to grab a lock on the siafile, while
   642  				// 'managedUploadSnapshot' can independently try to grab a lock
   643  				// on the renter while holding the snapshot.
   644  
   645  				// TODO: Calling ReadAll here is not really acceptable because
   646  				// for larger renters, the snapshot can be very large. For a
   647  				// user with 20 TB of data, the snapshot is going to be 2 GB
   648  				// large, which on its own exceeds our goal for the total memory
   649  				// consumption of the renter, and it's not even that much data.
   650  				//
   651  				// Need to work around the snapshot reader lock issue as well.
   652  				dotSia, err := ioutil.ReadAll(sr)
   653  				if err := sr.Close(); err != nil {
   654  					return errors.Compose(err, entry.Close())
   655  				}
   656  
   657  				// Upload the snapshot to the network.
   658  				meta.UploadProgress = calcSnapshotUploadProgress(100, 0)
   659  				meta.Size = uint64(len(dotSia))
   660  				if err := r.managedUploadSnapshot(meta, dotSia); err != nil {
   661  					return errors.Compose(err, entry.Close())
   662  				}
   663  
   664  				// Close out the entry.
   665  				err = entry.Close()
   666  				if err != nil {
   667  					return err
   668  				}
   669  
   670  				// Delete the local siafile.
   671  				if err := r.staticFileSystem.DeleteFile(info.SiaPath); err != nil {
   672  					return err
   673  				}
   674  				return nil
   675  			}()
   676  			if err != nil {
   677  				r.staticLog.Println("Failed to upload snapshot .sia:", err)
   678  			}
   679  		}
   680  		offlineMap, goodForRenewMap, contractsMap, _ := r.callRenterContractsAndUtilities()
   681  		err := r.staticFileSystem.List(root, true, offlineMap, goodForRenewMap, contractsMap, flf, func(skymodules.DirectoryInfo) {})
   682  		if err != nil {
   683  			r.staticLog.Println("Could not get un-uploaded snapshots:", err)
   684  		}
   685  
   686  		// Build a set of the snapshots we already have.
   687  		known := make(map[[16]byte]struct{})
   688  		id := r.mu.RLock()
   689  		for _, ub := range r.persist.UploadedBackups {
   690  			if ub.UploadProgress == 100 {
   691  				known[ub.UID] = struct{}{}
   692  			} else {
   693  				// signal r.threadedUploadAndRepair to keep uploading the snapshot
   694  				select {
   695  				case r.staticUploadHeap.repairNeeded <- struct{}{}:
   696  				default:
   697  				}
   698  			}
   699  		}
   700  		r.mu.RUnlock(id)
   701  
   702  		// Select an unsynchronized host.
   703  		contracts := r.staticHostContractor.Contracts()
   704  		var found bool
   705  		var c skymodules.RenterContract
   706  		for _, c = range contracts {
   707  			// ignore bad contracts
   708  			if !c.Utility.GoodForRenew || !c.Utility.GoodForUpload {
   709  				continue
   710  			}
   711  			if _, ok := syncedContracts[c.ID]; !ok {
   712  				found = true
   713  				break
   714  			}
   715  		}
   716  		if !found {
   717  			// No unsychronized hosts; drop any irrelevant contracts, then sleep
   718  			// for a while before trying again
   719  			if len(contracts) != 0 {
   720  				syncedContracts = make(map[types.FileContractID]struct{})
   721  				for _, c := range contracts {
   722  					if !c.Utility.GoodForRenew || !c.Utility.GoodForUpload {
   723  						continue
   724  					}
   725  					syncedContracts[c.ID] = struct{}{}
   726  				}
   727  			}
   728  			select {
   729  			case <-time.After(snapshotSyncSleepDuration):
   730  			case <-r.tg.StopChan():
   731  				return
   732  			}
   733  			continue
   734  		}
   735  
   736  		// Synchronize the host.
   737  		r.staticLog.Debugln("Synchronizing snapshots on host", c.HostPublicKey)
   738  		err = func() (err error) {
   739  			// Get the right worker for the host.
   740  			w, err := r.staticWorkerPool.callWorker(c.HostPublicKey)
   741  			if err != nil {
   742  				return err
   743  			}
   744  
   745  			// download the snapshot table
   746  			entryTable, err := w.DownloadSnapshotTable(r.tg.StopCtx())
   747  			if err != nil {
   748  				return err
   749  			}
   750  
   751  			// Calculate which snapshots the host doesn't have, and which
   752  			// snapshots it does have that we haven't seen before.
   753  			unknown, missing := calcOverlap(entryTable, known)
   754  
   755  			// If *any* snapshots are new, mark all other hosts as not
   756  			// synchronized.
   757  			if len(unknown) != 0 {
   758  				for fcid := range syncedContracts {
   759  					delete(syncedContracts, fcid)
   760  				}
   761  				// Record the new snapshots; they'll be replicated to the other
   762  				// hosts in subsequent iterations of the synchronization loop.
   763  				for _, ub := range unknown {
   764  					known[ub.UID] = struct{}{}
   765  					if err := r.managedSaveSnapshot(ub); err != nil {
   766  						return err
   767  					}
   768  					r.staticLog.Printf("Located new snapshot %q on host %v", ub.Name, c.HostPublicKey)
   769  				}
   770  			}
   771  
   772  			// Upload any snapshots that the host is missing.
   773  			//
   774  			// TODO: instead of returning immediately upon encountering an
   775  			// error, we should probably continue trying to upload the other
   776  			// snapshots.
   777  			for _, uid := range missing {
   778  				ub, dotSia, err := r.managedDownloadSnapshot(uid)
   779  				if err != nil {
   780  					// TODO: if snapshot can't be found on any host, delete it
   781  					return err
   782  				}
   783  				if err := w.UploadSnapshot(r.tg.StopCtx(), ub, dotSia); err != nil {
   784  					return err
   785  				}
   786  				r.staticLog.Printf("Replicated missing snapshot %q to host %v", ub.Name, c.HostPublicKey)
   787  			}
   788  			return nil
   789  		}()
   790  		if err != nil {
   791  			r.staticLog.Debugln("Failed to synchronize snapshots on host:", err)
   792  			// sleep for a bit to prevent retrying the same host repeatedly in a
   793  			// tight loop
   794  			select {
   795  			case <-time.After(snapshotSyncSleepDuration):
   796  			case <-r.tg.StopChan():
   797  				return
   798  			}
   799  			continue
   800  		}
   801  		// Mark the contract as synchronized.
   802  		syncedContracts[c.ID] = struct{}{}
   803  		// Commit the set of synchronized hosts.
   804  		id = r.mu.Lock()
   805  		r.persist.SyncedContracts = r.persist.SyncedContracts[:0]
   806  		for fcid := range syncedContracts {
   807  			r.persist.SyncedContracts = append(r.persist.SyncedContracts, fcid)
   808  		}
   809  		if err := r.saveSync(); err != nil {
   810  			r.staticLog.Println("Failed to update set of synced hosts:", err)
   811  		}
   812  		r.mu.Unlock(id)
   813  	}
   814  }