github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/core/fetch.go (about)

     1  package core
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	"github.com/atlassian/git-lob/providers"
    12  	"github.com/atlassian/git-lob/util"
    13  )
    14  
    15  // Implementation of fetch
    16  func Fetch(provider providers.SyncProvider, remoteName string, refspecs []*GitRefSpec, dryRun, force bool,
    17  	callback util.ProgressCallback) error {
    18  	// We need to build a list of commits ranges at which we want to ensure binaries are present locally
    19  	// We can't build the list of binaries solely from the log, because  not all binaries needed may have been
    20  	// modified in the range. Therefore we need 'git ls-tree' at the base ancestor in each range, followed by
    21  	// a 'git log -G' query for subsequent LOB changes. This is faster than doing ls-tree for every individual commit
    22  	// and eliminating duplicates.
    23  
    24  	util.LogDebugf("Fetching from %v via %v\n", remoteName, provider.TypeID())
    25  
    26  	var fileLobsNeeded []*FileLOB
    27  	var fetchranges []*GitRefSpec
    28  	if len(refspecs) == 0 {
    29  		// No refs specified, use 'Recent' fetch algorithm
    30  		if util.GlobalOptions.Verbose {
    31  			callback(&util.ProgressCallbackData{util.ProgressCalculate, "Calculating recent commits...",
    32  				int64(0), int64(1), 0, 0})
    33  		}
    34  		// Get HEAD LOBs first
    35  		headfilelobs, earliestCommit, err := GetGitAllFileLOBsToCheckoutAtCommitAndRecent("HEAD", util.GlobalOptions.FetchCommitsPeriodHEAD,
    36  			util.GlobalOptions.FetchIncludePaths, util.GlobalOptions.FetchExcludePaths)
    37  		if err != nil {
    38  			return errors.New(fmt.Sprintf("Error determining recent HEAD commits: %v", err.Error()))
    39  		}
    40  		if util.GlobalOptions.Verbose {
    41  			callback(&util.ProgressCallbackData{util.ProgressCalculate, fmt.Sprintf(" * HEAD: %d binary references", len(headfilelobs)),
    42  				0, 0, 0, 0})
    43  		}
    44  		fileLobsNeeded = headfilelobs
    45  		headSHA, err := GitRefToFullSHA("HEAD")
    46  		if err != nil {
    47  			return errors.New(fmt.Sprintf("Error determining HEAD sha: %v", err.Error()))
    48  		}
    49  		fetchranges = append(fetchranges, &GitRefSpec{fmt.Sprintf("^%v", earliestCommit), "..", headSHA})
    50  		if util.GlobalOptions.FetchRefsPeriodDays > 0 {
    51  			// Find recent other refs (only include remote branches for this remote)
    52  			recentrefs, err := GetGitRecentRefs(util.GlobalOptions.FetchRefsPeriodDays, true, remoteName)
    53  			if err != nil {
    54  				return errors.New(fmt.Sprintf("Error determining recent refs: %v", err.Error()))
    55  			}
    56  			// Now each other ref, they should be in reverse date order from GetGitRecentRefs so we're doing
    57  			// things by priority, HEAD first then most recent
    58  			refSHAsDone := util.NewStringSet()
    59  			refSHAsDone.Add(headSHA)
    60  			for i, ref := range recentrefs {
    61  				// Don't duplicate work when >1 ref has the same SHA
    62  				// Most common with HEAD if not detached but also tags
    63  				if refSHAsDone.Contains(ref.CommitSHA) {
    64  					continue
    65  				}
    66  				refSHAsDone.Add(ref.CommitSHA)
    67  
    68  				recentreflobs, earliestCommit, err := GetGitAllFileLOBsToCheckoutAtCommitAndRecent(ref.Name, util.GlobalOptions.FetchCommitsPeriodOther,
    69  					util.GlobalOptions.FetchIncludePaths, util.GlobalOptions.FetchExcludePaths)
    70  				if err != nil {
    71  					return errors.New(fmt.Sprintf("Error determining recent commits on %v: %v", ref, err.Error()))
    72  				}
    73  				if util.GlobalOptions.Verbose {
    74  					callback(&util.ProgressCallbackData{util.ProgressCalculate, fmt.Sprintf(" * %v: %d binary references", ref, len(recentreflobs)),
    75  						int64(i), int64(len(refspecs)), 0, 0})
    76  				}
    77  				fileLobsNeeded = append(fileLobsNeeded, recentreflobs...)
    78  
    79  				fetchranges = append(fetchranges, &GitRefSpec{fmt.Sprintf("^%v", earliestCommit), "..", ref.CommitSHA})
    80  			}
    81  
    82  		}
    83  	} else {
    84  		// Get LOBs directly from specified refs/ranges
    85  		for i, refspec := range refspecs {
    86  			if util.GlobalOptions.Verbose {
    87  				callback(&util.ProgressCallbackData{util.ProgressCalculate, fmt.Sprintf("Calculating data to fetch for %v", refspec),
    88  					int64(i), int64(len(refspecs)), 0, 0})
    89  			}
    90  			reffileshas, err := GetGitAllFilesAndLOBsToCheckoutInRefSpec(refspec, util.GlobalOptions.FetchIncludePaths, util.GlobalOptions.FetchExcludePaths)
    91  			if err != nil {
    92  				return errors.New(fmt.Sprintf("Error determining LOBs to fetch for %v: %v", refspec, err.Error()))
    93  			}
    94  			if util.GlobalOptions.Verbose {
    95  				callback(&util.ProgressCallbackData{util.ProgressCalculate, fmt.Sprintf(" * %v: %d binary references", refspec, len(refspecs)),
    96  					int64(i), int64(len(refspecs)), 0, 0})
    97  			}
    98  			fileLobsNeeded = append(fileLobsNeeded, reffileshas...)
    99  
   100  			if refspec.IsRange() {
   101  				fetchranges = append(fetchranges, refspec)
   102  			} else {
   103  				fetchranges = append(fetchranges, &GitRefSpec{fmt.Sprintf("^%v", refspec.Ref1), "..", refspec.Ref1})
   104  			}
   105  		}
   106  	}
   107  
   108  	var commitsToMarkPushedAfterFetching []string
   109  	// Before we actually fetch anything, check if we can bulk mark things as pushed
   110  	// Common case - first fetch after clone, user hasn't done any local work
   111  	if !InitSuccessfullyPushedCacheIfAppropriate() {
   112  		// Otherwise, let's see whether we can move the pushed state forward as we fetch
   113  		// Remember, we can have 'gaps' in the history for LOBs if enough time passed since last fetch
   114  		// that the earliest fetch doesn't cover the whole period since the last fetch
   115  		// So the cases where we can move the push markers forward are:
   116  		// 1. There's nothing to push before the first commit we're fetching, OR
   117  		// 2. The intervening 'unpushed' commits already exist on the remote
   118  		for _, fetchrange := range fetchranges {
   119  			anyCommitsUnpushed := false
   120  			allUnpushedCommitsAreOnRemote := true
   121  			unpushedCallback := func(commit *CommitLOBRef) (quit bool, err error) {
   122  				anyCommitsUnpushed = true
   123  				for _, sha := range commit.LobSHAs {
   124  					// check remote
   125  					remoteerr := CheckRemoteLOBFilesForSHA(sha, provider, remoteName)
   126  					if remoteerr != nil {
   127  						// LOB doesn't exist on remote so this is genuinely unpushed
   128  						allUnpushedCommitsAreOnRemote = false
   129  						return true, remoteerr
   130  					}
   131  				}
   132  				return false, nil
   133  			}
   134  			// These are all ranges, Ref1 being exclusive so that's where we measure from
   135  			WalkGitCommitLOBsToPush(remoteName, fetchrange.Ref1, false, unpushedCallback)
   136  			if !anyCommitsUnpushed || allUnpushedCommitsAreOnRemote {
   137  				pushedsha := fetchrange.Ref2
   138  				if !GitRefIsFullSHA(pushedsha) {
   139  					// Was probably a manual ref, convert to SHA
   140  					pushedsha, _ = GitRefToFullSHA(pushedsha)
   141  				}
   142  				commitsToMarkPushedAfterFetching = append(commitsToMarkPushedAfterFetching, pushedsha)
   143  				util.LogDebugf("Will mark %v as pushed after fetch since there are no unpushed LOBs in ancestors that aren't on %v\n", fetchrange.Ref2, remoteName)
   144  			} else {
   145  				util.LogDebugf("%v will not be marked as pushed after fetch since there are unpushed LOBs in ancestors that aren't on %v\n", fetchrange.Ref2, remoteName)
   146  			}
   147  		}
   148  
   149  	}
   150  
   151  	fetchAnyNotFound := false
   152  
   153  	if len(fileLobsNeeded) == 0 {
   154  		callback(&util.ProgressCallbackData{util.ProgressCalculate, "No binaries to download.",
   155  			int64(len(refspecs)), int64(len(refspecs)), 0, 0})
   156  	} else {
   157  
   158  		// Duplicates are not eliminated by methods we call, for efficiency
   159  		// We need to remove them though because otherwise we can report much higher download requirements
   160  		// than necessary when multiple refs include the same SHA
   161  		// We use this opportunity to build a straight list of unduplicated LOB shas which is also map to a filename
   162  		// which we may use in smart servers to determine other versions to generate deltas on
   163  
   164  		lobsToDownload := ConvertFileLOBSliceToMap(fileLobsNeeded)
   165  		if !force {
   166  			// Eliminate any that are OK locally
   167  			// It's safe to delete as you iterate in Go! refreshing :)
   168  			for sha, _ := range lobsToDownload {
   169  				if !IsLOBMissing(sha, false) {
   170  					delete(lobsToDownload, sha)
   171  				}
   172  			}
   173  		}
   174  
   175  		if len(lobsToDownload) == 0 {
   176  			callback(&util.ProgressCallbackData{util.ProgressCalculate, "No binaries to download.",
   177  				int64(len(refspecs)), int64(len(refspecs)), 0, 0})
   178  			return nil
   179  		} else {
   180  			callback(&util.ProgressCallbackData{util.ProgressCalculate, fmt.Sprintf("%d binaries to download.", len(lobsToDownload)),
   181  				int64(len(refspecs)), int64(len(refspecs)), 0, 0})
   182  		}
   183  		if !dryRun {
   184  			fetchCallback := func(data *util.ProgressCallbackData) (abort bool) {
   185  				if data.Type == util.ProgressNotFound {
   186  					fetchAnyNotFound = true
   187  				}
   188  				// passthrough to external callback
   189  				return callback(data)
   190  			}
   191  
   192  			err := fetchLOBs(lobsToDownload, provider, remoteName, force, fetchCallback)
   193  			if err != nil {
   194  				return err
   195  			}
   196  
   197  		}
   198  	}
   199  
   200  	util.LogDebugf("Successfully fetched from %v via %v\n", remoteName, provider.TypeID())
   201  
   202  	// Now mark as pushed if appropriate
   203  	// If any files were not found on the remote, don't do this (we may get them locally later & need to push them)
   204  	if !fetchAnyNotFound && len(commitsToMarkPushedAfterFetching) > 0 {
   205  		for _, c := range commitsToMarkPushedAfterFetching {
   206  			err := MarkBinariesAsPushed(remoteName, c, "")
   207  			if err != nil {
   208  				util.LogErrorf("Error marking %v as pushed after fetch for %v: %v", c, remoteName, err.Error())
   209  			}
   210  		}
   211  		// resolve any redundant state that creates
   212  		CleanupPushState(remoteName)
   213  	}
   214  
   215  	return nil
   216  
   217  }
   218  
   219  // Internal method for fetching
   220  func fetchLOBs(lobshas map[string]string, provider providers.SyncProvider, remoteName string, force bool, callback util.ProgressCallback) error {
   221  	// Download metafiles first
   222  	// This will allow us to estimate the time required
   223  	callback(&util.ProgressCallbackData{util.ProgressCalculate, "Downloading metadata",
   224  		0, 0, 0, 0})
   225  	err := fetchMetadata(lobshas, provider, remoteName, force, callback)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	// So now we have all the metadata available locally, we can know what files to download
   231  
   232  	var filesTotalBytes int64
   233  	var files []string
   234  	var deltas []*LOBDelta
   235  	var deltaTotalBytes int64
   236  	var deltaSavings int64
   237  	smartProvider := providers.UpgradeToSmartSyncProvider(provider)
   238  
   239  	callback(&util.ProgressCallbackData{util.ProgressCalculate, "Calculating content files to download",
   240  		0, 0, 0, 0})
   241  	for sha, filename := range lobshas {
   242  		info, err := GetLOBInfo(sha)
   243  		if err != nil {
   244  			// If we could not get the lob data, it means that we could not download the meta file
   245  			// it's OK for this to happen since the remote may not have all the data we need (e.g.
   246  			// local branch with changes that actually came from somewhere else, or remote hasn't been
   247  			// fully updated yet)
   248  			// We notified earlier
   249  			continue
   250  		}
   251  		// If this is a smart provider, try to download deltas where appropriate
   252  		if info.Size > util.GlobalOptions.FetchDeltasAboveSize && smartProvider != nil {
   253  			// This doesn't download, just prepares and gets size
   254  			delta := prepareFetchDelta(sha, filename, smartProvider, remoteName)
   255  			if delta != nil {
   256  				deltas = append(deltas, delta)
   257  				deltaTotalBytes += delta.DeltaSize
   258  				deltaSavings += info.Size - (delta.DeltaSize + ApproximateMetadataSize)
   259  				// We'll do a delta for this so don't continue to determine files
   260  				continue
   261  			}
   262  		}
   263  		// fallback to basic file download
   264  		filesTotalBytes += info.Size
   265  		for i := 0; i < info.NumChunks; i++ {
   266  			// get relative filename for download purposes
   267  			files = append(files, GetLOBChunkRelativePath(sha, i))
   268  		}
   269  	}
   270  	totalBytes := filesTotalBytes + deltaTotalBytes
   271  	callback(&util.ProgressCallbackData{util.ProgressCalculate, fmt.Sprintf("Metadata done, downloading content (%v)", util.FormatSize(totalBytes)),
   272  		0, 0, 0, 0})
   273  	if deltaSavings > 0 {
   274  		callback(&util.ProgressCallbackData{util.ProgressCalculate, fmt.Sprintf("Saving %v by fetching deltas", util.FormatSize(deltaSavings)),
   275  			0, 0, 0, 0})
   276  	}
   277  
   278  	// Download content now
   279  	if smartProvider != nil && len(deltas) > 0 {
   280  		// First try deltas, if any fail fall back on regular download
   281  		failedDeltas := fetchDeltas(deltas, deltaTotalBytes, smartProvider, remoteName, force, callback)
   282  		if len(failedDeltas) > 0 {
   283  			for _, delta := range failedDeltas {
   284  				// Slightly costly but this should be rare for prepare to work and download not to
   285  				info, err := GetLOBInfo(delta.TargetSHA)
   286  				if err != nil {
   287  					return fmt.Errorf("LOB info for %v went missing, this should be impossible: %v", delta.TargetSHA, err.Error())
   288  				}
   289  				filesTotalBytes += info.Size
   290  				for i := 0; i < info.NumChunks; i++ {
   291  					// get relative filename for download purposes
   292  					files = append(files, GetLOBChunkRelativePath(info.SHA, i))
   293  				}
   294  			}
   295  		}
   296  	}
   297  	return fetchContentFiles(files, filesTotalBytes, provider, remoteName, force, callback)
   298  
   299  }
   300  
   301  func prepareFetchDelta(lobsha, filename string, provider providers.SmartSyncProvider, remoteName string) *LOBDelta {
   302  	othershas, err := GetGitAllLOBHistoryForFile(filename, lobsha)
   303  	if err != nil {
   304  		util.LogErrorf("Unable to prepare delta for %v(%v): %v\n", lobsha, filename, err.Error())
   305  		return nil
   306  	}
   307  	// This is all the possible base shas, but we can only use ones we have locally too
   308  	// Right now we're not trying to cope with ordered downloads where we might have newer ones part way through fetch (too fiddly)
   309  	var localbaseshas []string
   310  	for _, sha := range othershas {
   311  		if !IsLOBMissing(sha, false) {
   312  			localbaseshas = append(localbaseshas, sha)
   313  		}
   314  	}
   315  	if len(localbaseshas) == 0 {
   316  		// no base shas, cannot do this
   317  		return nil
   318  	}
   319  	// Now ask the server to pick a sha, generate a delta, cache it and tell us how big it is
   320  	sz, chosenbasesha, err := provider.PrepareDeltaForDownload(remoteName, lobsha, localbaseshas)
   321  	if err != nil {
   322  		util.LogErrorf("Unable to prepare delta %v(%v): %v\n", lobsha, filename, err.Error())
   323  		return nil
   324  	}
   325  	// No common base to use
   326  	if chosenbasesha == "" {
   327  		return nil
   328  	}
   329  	return &LOBDelta{
   330  		BaseSHA:   chosenbasesha,
   331  		TargetSHA: lobsha,
   332  		DeltaSize: sz,
   333  	}
   334  }
   335  
   336  // Internal method for fetching
   337  func fetchMetadata(lobshas map[string]string, provider providers.SyncProvider, remoteName string, force bool, callback util.ProgressCallback) error {
   338  	// Use average metafile bytes as estimate of download, usually < 100 bytes of JSON
   339  	metaTotalBytes := int64(len(lobshas) * ApproximateMetadataSize)
   340  	var metafilesDone int
   341  	metacallback := func(fileInProgress string, progressType util.ProgressCallbackType, bytesDone, totalBytes int64) (abort bool) {
   342  		// Don't bother to track partial completion, only 100 bytes each
   343  		if progressType == util.ProgressSkip || progressType == util.ProgressNotFound {
   344  			metafilesDone++
   345  			callback(&util.ProgressCallbackData{progressType, fileInProgress, totalBytes, totalBytes,
   346  				int64(metafilesDone * ApproximateMetadataSize), metaTotalBytes})
   347  			// Remote did not have this file
   348  		} else {
   349  			if bytesDone == totalBytes {
   350  				// finished
   351  				metafilesDone++
   352  				callback(&util.ProgressCallbackData{util.ProgressTransferBytes, fileInProgress, totalBytes, totalBytes,
   353  					int64(metafilesDone * ApproximateMetadataSize), metaTotalBytes})
   354  			}
   355  		}
   356  		return false
   357  	}
   358  	// Download all meta files
   359  	var metafilesToDownload []string
   360  	for sha, _ := range lobshas {
   361  		// Note get relative file name
   362  		metafilesToDownload = append(metafilesToDownload, GetLOBMetaRelativePath(sha))
   363  	}
   364  	// Download to shared if using shared area (we link later)
   365  	destDir := getFetchDestination()
   366  	err := provider.Download(remoteName, metafilesToDownload, destDir, force, metacallback)
   367  
   368  	// If shared store, link any metadata we downloaded into local
   369  	if IsUsingSharedStorage() {
   370  		for _, sha := range lobshas {
   371  			// filenames are relative (for download)
   372  			localfile := GetLocalLOBMetaPath(sha)
   373  			sharedfile := getSharedLOBMetaPath(sha)
   374  			if (force || !util.FileExists(localfile)) && util.FileExists(sharedfile) {
   375  				linkerr := linkSharedLOBFilename(sharedfile)
   376  				if linkerr != nil {
   377  					// we want to continue so don't return this
   378  					util.LogErrorf("Failed to link shared file %v into local repo: %v\n", sharedfile, linkerr.Error())
   379  				}
   380  			}
   381  		}
   382  	}
   383  	// Deal with errors afterwards so we linked partial successes
   384  	if err != nil {
   385  		return err
   386  	}
   387  
   388  	return nil
   389  }
   390  
   391  func getFetchDestination() string {
   392  	// Download to shared if using shared area (we link later)
   393  	if IsUsingSharedStorage() {
   394  		return GetSharedLOBRoot()
   395  	} else {
   396  		return GetLocalLOBRoot()
   397  	}
   398  }
   399  
   400  func fetchContentFiles(files []string, filesTotalBytes int64, provider providers.SyncProvider,
   401  	remoteName string, force bool, callback util.ProgressCallback) error {
   402  	var lastFilename string
   403  	var lastFileBytes int64
   404  	var bytesFromFilesDoneSoFar int64
   405  	contentcallback := func(fileInProgress string, progressType util.ProgressCallbackType, bytesDone, totalBytes int64) (abort bool) {
   406  
   407  		var ret bool
   408  		if lastFilename != fileInProgress && lastFilename != "" {
   409  			// we obviously never got a 100% call for previous file
   410  			bytesFromFilesDoneSoFar += lastFileBytes
   411  			ret = callback(&util.ProgressCallbackData{util.ProgressTransferBytes, lastFilename, lastFileBytes, lastFileBytes,
   412  				bytesFromFilesDoneSoFar, filesTotalBytes})
   413  			lastFilename = ""
   414  		}
   415  		if progressType == util.ProgressSkip || progressType == util.ProgressNotFound {
   416  			bytesFromFilesDoneSoFar += totalBytes
   417  			ret = callback(&util.ProgressCallbackData{progressType, fileInProgress, totalBytes, totalBytes,
   418  				bytesFromFilesDoneSoFar, filesTotalBytes})
   419  		} else {
   420  
   421  			if bytesDone == totalBytes {
   422  				// finished
   423  				bytesFromFilesDoneSoFar += totalBytes
   424  				ret = callback(&util.ProgressCallbackData{util.ProgressTransferBytes, fileInProgress, bytesDone, totalBytes,
   425  					bytesFromFilesDoneSoFar, filesTotalBytes})
   426  				lastFilename = ""
   427  			} else {
   428  				// partly progressed file
   429  				lastFilename = fileInProgress
   430  				lastFileBytes = totalBytes
   431  				ret = callback(&util.ProgressCallbackData{util.ProgressTransferBytes, fileInProgress, bytesDone, totalBytes,
   432  					bytesFromFilesDoneSoFar + bytesDone, filesTotalBytes})
   433  
   434  			}
   435  
   436  		}
   437  
   438  		return ret
   439  	}
   440  	destDir := getFetchDestination()
   441  	err := provider.Download(remoteName, files, destDir, force, contentcallback)
   442  	if err == nil && lastFilename != "" {
   443  		// we obviously never got a 100% progress call for final file
   444  		callback(&util.ProgressCallbackData{util.ProgressTransferBytes, lastFilename, lastFileBytes, lastFileBytes,
   445  			filesTotalBytes, filesTotalBytes})
   446  		lastFilename = ""
   447  	}
   448  	// Also if shared store, link meta into local
   449  	// Link any we successfully downloaded
   450  	if IsUsingSharedStorage() {
   451  		localroot := GetLocalLOBRoot()
   452  		sharedroot := GetSharedLOBRoot()
   453  		for _, relfile := range files {
   454  			// filenames are relative (for download)
   455  			localfile := filepath.Join(localroot, relfile)
   456  			sharedfile := filepath.Join(sharedroot, relfile)
   457  			if (force || !util.FileExists(localfile)) && util.FileExists(sharedfile) {
   458  				linkerr := linkSharedLOBFilename(sharedfile)
   459  				if linkerr != nil {
   460  					// we want to continue so don't return this
   461  					util.LogErrorf("Failed to link shared file %v into local repo: %v\n", sharedfile, linkerr.Error())
   462  				}
   463  			}
   464  		}
   465  	}
   466  
   467  	return err
   468  }
   469  
   470  // Fetch via deltas which have already been picked & prepared on the server. Any that fail for any reason are added
   471  // to the faileddeltas return list and will be re-tried using the standard download
   472  func fetchDeltas(deltas []*LOBDelta, deltaTotalBytes int64, provider providers.SmartSyncProvider, remoteName string,
   473  	force bool, callback util.ProgressCallback) (faileddeltas []*LOBDelta) {
   474  
   475  	var failed []*LOBDelta
   476  	var bytesDoneSoFar int64
   477  	for _, delta := range deltas {
   478  
   479  		err := fetchSingleDelta(delta, bytesDoneSoFar, deltaTotalBytes, provider, remoteName, force, callback)
   480  		bytesDoneSoFar += delta.DeltaSize
   481  		if err != nil {
   482  			failed = append(failed, delta)
   483  			msg := fmt.Sprintf("Error applying %v: %v. Falling back to non-delta download", getDeltaProgressDesc(delta), err.Error())
   484  			callback(&util.ProgressCallbackData{util.ProgressError, msg, delta.DeltaSize, delta.DeltaSize,
   485  				bytesDoneSoFar, deltaTotalBytes})
   486  		}
   487  
   488  	}
   489  	return failed
   490  
   491  }
   492  
   493  func getDeltaProgressDesc(delta *LOBDelta) string {
   494  	return fmt.Sprintf("Delta %v..%v", delta.BaseSHA[:7], delta.TargetSHA[:7])
   495  }
   496  
   497  func fetchSingleDelta(delta *LOBDelta, bytesSoFar int64, deltaTotalBytes int64, provider providers.SmartSyncProvider, remoteName string,
   498  	force bool, callback util.ProgressCallback) error {
   499  
   500  	// Description for progress
   501  	desc := getDeltaProgressDesc(delta)
   502  
   503  	// Initial 0% call
   504  	callback(&util.ProgressCallbackData{util.ProgressTransferBytes, desc, 0, delta.DeltaSize,
   505  		bytesSoFar, deltaTotalBytes})
   506  
   507  	// We could pipe download output directly into ApplyDelta via a goroutine
   508  	// But for simplicity of fail states, use a temp file
   509  	tempf, err := ioutil.TempFile("", "deltadownload")
   510  	if err != nil {
   511  		return err
   512  	}
   513  	tempfilename := tempf.Name()
   514  	defer os.Remove(tempfilename) // ensure always removed
   515  	defer tempf.Close()           // only used in panic cases, we close manually
   516  	localcallback := func(txt string, progressType util.ProgressCallbackType, bytesDone, totalBytes int64) (abort bool) {
   517  
   518  		var ret bool
   519  		if bytesDone != totalBytes {
   520  			// only do part progress in here, do final outside to ensure it always happens regardless
   521  			ret = callback(&util.ProgressCallbackData{util.ProgressTransferBytes, desc, bytesDone, totalBytes,
   522  				bytesSoFar + bytesDone, deltaTotalBytes})
   523  
   524  		}
   525  
   526  		return ret
   527  	}
   528  
   529  	err = provider.DownloadDelta(remoteName, delta.BaseSHA, delta.TargetSHA, tempf, localcallback)
   530  	tempf.Close() // Close so available to read back
   531  	if err != nil {
   532  		return err
   533  	}
   534  	// finished download OK, now apply
   535  	deltain, err := os.OpenFile(tempfilename, os.O_RDONLY, 0644)
   536  	if err != nil {
   537  		return err
   538  	}
   539  	defer deltain.Close()
   540  	// Apply to shared or local
   541  	err = ApplyLOBDeltaInBaseDir(getFetchDestination(), delta.BaseSHA, delta.TargetSHA, deltain)
   542  	if err != nil {
   543  		return err
   544  	}
   545  
   546  	// Also if downloading to shared store, link into local
   547  	if IsUsingSharedStorage() {
   548  		ok := recoverLocalLOBFilesFromSharedStore(delta.TargetSHA)
   549  		if !ok {
   550  			return fmt.Errorf("%v was applied to shared store but linking to local failed", desc)
   551  		}
   552  	}
   553  
   554  	// yay, call final 100%
   555  	callback(&util.ProgressCallbackData{util.ProgressTransferBytes, desc, delta.DeltaSize, delta.DeltaSize,
   556  		bytesSoFar + delta.DeltaSize, deltaTotalBytes})
   557  
   558  	return nil
   559  
   560  }
   561  
   562  // Fetch the files required for a single LOB
   563  func FetchSingle(lobsha string, provider providers.SyncProvider, remoteName string, force bool, callback util.ProgressCallback) error {
   564  
   565  	var lobToDownload map[string]string
   566  	if force || IsLOBMissing(lobsha, false) {
   567  		// We don't know the filename, this is forced
   568  		lobToDownload[lobsha] = ""
   569  	}
   570  
   571  	if len(lobToDownload) > 0 {
   572  		return fetchLOBs(lobToDownload, provider, remoteName, force, callback)
   573  	} else {
   574  		return nil
   575  	}
   576  }
   577  
   578  // Auto-fetch a single LOB from the default locations
   579  // If the required files are not found this won't cause an error
   580  func AutoFetch(lobsha string, reportProgress bool) error {
   581  	remoteName := GetGitDefaultRemoteForPull()
   582  	util.LogDebugf("Trying to auto-fetch %v from %v\n", lobsha, remoteName)
   583  	// check the remote config to make sure it's valid
   584  	provider, err := providers.GetProviderForRemote(remoteName)
   585  	if err != nil {
   586  		return err
   587  	}
   588  	if err = provider.ValidateConfig(remoteName); err != nil {
   589  		return err
   590  	}
   591  
   592  	var fetcherr error
   593  	if reportProgress {
   594  		// We need to run this in a goroutine to report progress deterministically
   595  		// 100 items in the queue should be good enough, this means that it won't block
   596  		callbackChan := make(chan *util.ProgressCallbackData, 100)
   597  		go func(lobsha string, provider providers.SyncProvider, remoteName string, progresschan chan<- *util.ProgressCallbackData) {
   598  
   599  			// Progress callback just passes the result back to the channel
   600  			progress := func(data *util.ProgressCallbackData) (abort bool) {
   601  				progresschan <- data
   602  
   603  				return false
   604  			}
   605  
   606  			err := FetchSingle(lobsha, provider, remoteName, false, progress)
   607  
   608  			close(progresschan)
   609  
   610  			if err != nil {
   611  				fetcherr = err
   612  			}
   613  
   614  		}(lobsha, provider, remoteName, callbackChan)
   615  
   616  		// Report progress on operation every 0.5s
   617  		util.ReportProgressToConsole(callbackChan, "Fetch", time.Millisecond*500)
   618  		// Because no final newline from report progress
   619  		util.LogConsole("")
   620  	} else {
   621  		// no progress, just do it
   622  		fetcherr = FetchSingle(lobsha, provider, remoteName, false, func(data *util.ProgressCallbackData) (abort bool) { return false })
   623  	}
   624  
   625  	if fetcherr == nil {
   626  		util.LogDebugf("Successfully fetched %v from %v\n", lobsha, remoteName)
   627  	} else {
   628  		util.LogDebugf("Failed to auto fetch %v from %v: %v\n", lobsha, remoteName, fetcherr)
   629  	}
   630  
   631  	return fetcherr
   632  }