github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/store/store_download.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  // Package store has support to use the Ubuntu Store for querying and downloading of snaps, and the related services.
    21  package store
    22  
    23  import (
    24  	"context"
    25  	"crypto"
    26  	"errors"
    27  	"fmt"
    28  	"io"
    29  	"net/http"
    30  	"net/url"
    31  	"os"
    32  	"os/exec"
    33  	"path/filepath"
    34  	"strconv"
    35  	"strings"
    36  	"sync"
    37  	"time"
    38  
    39  	"github.com/juju/ratelimit"
    40  	"gopkg.in/retry.v1"
    41  
    42  	"github.com/snapcore/snapd/dirs"
    43  	"github.com/snapcore/snapd/httputil"
    44  	"github.com/snapcore/snapd/i18n"
    45  	"github.com/snapcore/snapd/logger"
    46  	"github.com/snapcore/snapd/osutil"
    47  	"github.com/snapcore/snapd/overlord/auth"
    48  	"github.com/snapcore/snapd/progress"
    49  	"github.com/snapcore/snapd/snap"
    50  	"github.com/snapcore/snapd/snapdtool"
    51  )
    52  
    53  var downloadRetryStrategy = retry.LimitCount(7, retry.LimitTime(90*time.Second,
    54  	retry.Exponential{
    55  		Initial: 500 * time.Millisecond,
    56  		Factor:  2.5,
    57  	},
    58  ))
    59  
    60  var downloadSpeedMeasureWindow = 5 * time.Minute
    61  
    62  // minimum average download speed (bytes/sec), measured over downloadSpeedMeasureWindow.
    63  var downloadSpeedMin = float64(4096)
    64  
    65  func init() {
    66  	if v := os.Getenv("SNAPD_MIN_DOWNLOAD_SPEED"); v != "" {
    67  		if speed, err := strconv.Atoi(v); err == nil {
    68  			downloadSpeedMin = float64(speed)
    69  		} else {
    70  			logger.Noticef("Cannot parse SNAPD_MIN_DOWNLOAD_SPEED as number")
    71  		}
    72  	}
    73  	if v := os.Getenv("SNAPD_DOWNLOAD_MEAS_WINDOW"); v != "" {
    74  		if win, err := time.ParseDuration(v); err == nil {
    75  			downloadSpeedMeasureWindow = win
    76  		} else {
    77  			logger.Noticef("Cannot parse SNAPD_DOWNLOAD_MEAS_WINDOW as time.Duration")
    78  		}
    79  	}
    80  }
    81  
    82  // Deltas enabled by default on classic, but allow opting in or out on both classic and core.
    83  func useDeltas() bool {
    84  	// only xdelta3 is supported for now, so check the binary exists here
    85  	// TODO: have a per-format checker instead
    86  	if _, err := getXdelta3Cmd(); err != nil {
    87  		return false
    88  	}
    89  
    90  	return osutil.GetenvBool("SNAPD_USE_DELTAS_EXPERIMENTAL", true)
    91  }
    92  
    93  func (s *Store) cdnHeader() (string, error) {
    94  	if s.noCDN {
    95  		return "none", nil
    96  	}
    97  
    98  	if s.dauthCtx == nil {
    99  		return "", nil
   100  	}
   101  
   102  	// set Snap-CDN from cloud instance information
   103  	// if available
   104  
   105  	// TODO: do we want a more complex retry strategy
   106  	// where we first to send this header and if the
   107  	// operation fails that way to even get the connection
   108  	// then we retry without sending this?
   109  
   110  	cloudInfo, err := s.dauthCtx.CloudInfo()
   111  	if err != nil {
   112  		return "", err
   113  	}
   114  
   115  	if cloudInfo != nil {
   116  		cdnParams := []string{fmt.Sprintf("cloud-name=%q", cloudInfo.Name)}
   117  		if cloudInfo.Region != "" {
   118  			cdnParams = append(cdnParams, fmt.Sprintf("region=%q", cloudInfo.Region))
   119  		}
   120  		if cloudInfo.AvailabilityZone != "" {
   121  			cdnParams = append(cdnParams, fmt.Sprintf("availability-zone=%q", cloudInfo.AvailabilityZone))
   122  		}
   123  
   124  		return strings.Join(cdnParams, " "), nil
   125  	}
   126  
   127  	return "", nil
   128  }
   129  
   130  type HashError struct {
   131  	name           string
   132  	sha3_384       string
   133  	targetSha3_384 string
   134  }
   135  
   136  func (e HashError) Error() string {
   137  	return fmt.Sprintf("sha3-384 mismatch for %q: got %s but expected %s", e.name, e.sha3_384, e.targetSha3_384)
   138  }
   139  
   140  type DownloadOptions struct {
   141  	RateLimit           int64
   142  	IsAutoRefresh       bool
   143  	LeavePartialOnError bool
   144  }
   145  
   146  // Download downloads the snap addressed by download info and returns its
   147  // filename.
   148  // The file is saved in temporary storage, and should be removed
   149  // after use to prevent the disk from running out of space.
   150  func (s *Store) Download(ctx context.Context, name string, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error {
   151  	if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
   152  		return err
   153  	}
   154  
   155  	if err := s.cacher.Get(downloadInfo.Sha3_384, targetPath); err == nil {
   156  		logger.Debugf("Cache hit for SHA3_384 …%.5s.", downloadInfo.Sha3_384)
   157  		return nil
   158  	}
   159  
   160  	if useDeltas() {
   161  		logger.Debugf("Available deltas returned by store: %v", downloadInfo.Deltas)
   162  
   163  		if len(downloadInfo.Deltas) == 1 {
   164  			err := s.downloadAndApplyDelta(name, targetPath, downloadInfo, pbar, user, dlOpts)
   165  			if err == nil {
   166  				return nil
   167  			}
   168  			// We revert to normal downloads if there is any error.
   169  			logger.Noticef("Cannot download or apply deltas for %s: %v", name, err)
   170  		}
   171  	}
   172  
   173  	partialPath := targetPath + ".partial"
   174  	w, err := os.OpenFile(partialPath, os.O_RDWR|os.O_CREATE, 0600)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	resume, err := w.Seek(0, os.SEEK_END)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	defer func() {
   183  		fi, _ := w.Stat()
   184  		if cerr := w.Close(); cerr != nil && err == nil {
   185  			err = cerr
   186  		}
   187  		if err == nil {
   188  			return
   189  		}
   190  		if dlOpts == nil || !dlOpts.LeavePartialOnError || fi == nil || fi.Size() == 0 {
   191  			os.Remove(w.Name())
   192  		}
   193  	}()
   194  	if resume > 0 {
   195  		logger.Debugf("Resuming download of %q at %d.", partialPath, resume)
   196  	} else {
   197  		logger.Debugf("Starting download of %q.", partialPath)
   198  	}
   199  
   200  	authAvail, err := s.authAvailable(user)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	url := downloadInfo.AnonDownloadURL
   206  	if url == "" || authAvail {
   207  		url = downloadInfo.DownloadURL
   208  	}
   209  
   210  	if downloadInfo.Size == 0 || resume < downloadInfo.Size {
   211  		err = download(ctx, name, downloadInfo.Sha3_384, url, user, s, w, resume, pbar, dlOpts)
   212  		if err != nil {
   213  			logger.Debugf("download of %q failed: %#v", url, err)
   214  		}
   215  	} else {
   216  		// we're done! check the hash though
   217  		h := crypto.SHA3_384.New()
   218  		if _, err := w.Seek(0, os.SEEK_SET); err != nil {
   219  			return err
   220  		}
   221  		if _, err := io.Copy(h, w); err != nil {
   222  			return err
   223  		}
   224  		actualSha3 := fmt.Sprintf("%x", h.Sum(nil))
   225  		if downloadInfo.Sha3_384 != actualSha3 {
   226  			err = HashError{name, actualSha3, downloadInfo.Sha3_384}
   227  		}
   228  	}
   229  	// If hashsum is incorrect retry once
   230  	if _, ok := err.(HashError); ok {
   231  		logger.Debugf("Hashsum error on download: %v", err.Error())
   232  		logger.Debugf("Truncating and trying again from scratch.")
   233  		err = w.Truncate(0)
   234  		if err != nil {
   235  			return err
   236  		}
   237  		_, err = w.Seek(0, os.SEEK_SET)
   238  		if err != nil {
   239  			return err
   240  		}
   241  		err = download(ctx, name, downloadInfo.Sha3_384, url, user, s, w, 0, pbar, nil)
   242  		if err != nil {
   243  			logger.Debugf("download of %q failed: %#v", url, err)
   244  		}
   245  	}
   246  
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	if err := os.Rename(w.Name(), targetPath); err != nil {
   252  		return err
   253  	}
   254  
   255  	if err := w.Sync(); err != nil {
   256  		return err
   257  	}
   258  
   259  	return s.cacher.Put(downloadInfo.Sha3_384, targetPath)
   260  }
   261  
   262  func downloadReqOpts(storeURL *url.URL, cdnHeader string, opts *DownloadOptions) *requestOptions {
   263  	reqOptions := requestOptions{
   264  		Method:       "GET",
   265  		URL:          storeURL,
   266  		ExtraHeaders: map[string]string{},
   267  		// FIXME: use the new headers? with
   268  		// APILevel: apiV2Endps,
   269  	}
   270  	if cdnHeader != "" {
   271  		reqOptions.ExtraHeaders["Snap-CDN"] = cdnHeader
   272  	}
   273  	if opts != nil && opts.IsAutoRefresh {
   274  		reqOptions.ExtraHeaders["Snap-Refresh-Reason"] = "scheduled"
   275  	}
   276  
   277  	return &reqOptions
   278  }
   279  
   280  type transferSpeedError struct {
   281  	Speed float64
   282  }
   283  
   284  func (e *transferSpeedError) Error() string {
   285  	return fmt.Sprintf("download too slow: %.2f bytes/sec", e.Speed)
   286  }
   287  
   288  // implements io.Writer interface
   289  // XXX: move to osutil?
   290  type TransferSpeedMonitoringWriter struct {
   291  	mu sync.Mutex
   292  
   293  	measureTimeWindow   time.Duration
   294  	minDownloadSpeedBps float64
   295  
   296  	ctx context.Context
   297  
   298  	// internal state
   299  	start   time.Time
   300  	written int
   301  	cancel  func()
   302  	err     error
   303  
   304  	// for testing
   305  	measuredWindows int
   306  }
   307  
   308  // NewTransferSpeedMonitoringWriterAndContext returns an io.Writer that measures
   309  // write speed in measureTimeWindow windows and cancels the operation if
   310  // minDownloadSpeedBps is not achieved.
   311  // Monitor() must be called to start actual measurement.
   312  func NewTransferSpeedMonitoringWriterAndContext(origCtx context.Context, measureTimeWindow time.Duration, minDownloadSpeedBps float64) (*TransferSpeedMonitoringWriter, context.Context) {
   313  	ctx, cancel := context.WithCancel(origCtx)
   314  	w := &TransferSpeedMonitoringWriter{
   315  		measureTimeWindow:   measureTimeWindow,
   316  		minDownloadSpeedBps: minDownloadSpeedBps,
   317  		ctx:                 ctx,
   318  		cancel:              cancel,
   319  	}
   320  	return w, ctx
   321  }
   322  
   323  func (w *TransferSpeedMonitoringWriter) reset() {
   324  	w.mu.Lock()
   325  	defer w.mu.Unlock()
   326  	w.written = 0
   327  	w.start = time.Now()
   328  	w.measuredWindows++
   329  }
   330  
   331  // checkSpeed measures the transfer rate since last reset() call.
   332  // The caller must call reset() over the desired time windows.
   333  func (w *TransferSpeedMonitoringWriter) checkSpeed(min float64) bool {
   334  	w.mu.Lock()
   335  	defer w.mu.Unlock()
   336  	d := time.Now().Sub(w.start)
   337  	// should never happen since checkSpeed is done after measureTimeWindow
   338  	if d.Seconds() == 0 {
   339  		return true
   340  	}
   341  	s := float64(w.written) / d.Seconds()
   342  	ok := s >= min
   343  	if !ok {
   344  		w.err = &transferSpeedError{Speed: s}
   345  	}
   346  	return ok
   347  }
   348  
   349  // Monitor starts a new measurement for write operations and returns a quit
   350  // channel that should be closed by the caller to finish the measurement.
   351  func (w *TransferSpeedMonitoringWriter) Monitor() (quit chan bool) {
   352  	quit = make(chan bool)
   353  	w.reset()
   354  	go func() {
   355  		for {
   356  			select {
   357  			case <-time.After(w.measureTimeWindow):
   358  				if !w.checkSpeed(w.minDownloadSpeedBps) {
   359  					w.cancel()
   360  					return
   361  				}
   362  				// reset the measurement every downloadSpeedMeasureWindow,
   363  				// we want average speed per second over the mesure time window,
   364  				// otherwise a large download with initial good download
   365  				// speed could get stuck at the end of the download, and it
   366  				// would take long time for overall average to "catch up".
   367  				w.reset()
   368  			case <-quit:
   369  				return
   370  			}
   371  		}
   372  	}()
   373  	return quit
   374  }
   375  
   376  func (w *TransferSpeedMonitoringWriter) Write(p []byte) (n int, err error) {
   377  	w.mu.Lock()
   378  	defer w.mu.Unlock()
   379  	w.written += len(p)
   380  	return len(p), nil
   381  }
   382  
   383  // Err returns the transferSpeedError if encountered when measurement was run.
   384  func (w *TransferSpeedMonitoringWriter) Err() error {
   385  	return w.err
   386  }
   387  
   388  var ratelimitReader = ratelimit.Reader
   389  
   390  var download = downloadImpl
   391  
   392  // download writes an http.Request showing a progress.Meter
   393  func downloadImpl(ctx context.Context, name, sha3_384, downloadURL string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *DownloadOptions) error {
   394  	if dlOpts == nil {
   395  		dlOpts = &DownloadOptions{}
   396  	}
   397  
   398  	storeURL, err := url.Parse(downloadURL)
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	cdnHeader, err := s.cdnHeader()
   404  	if err != nil {
   405  		return err
   406  	}
   407  
   408  	tc, downloadCtx := NewTransferSpeedMonitoringWriterAndContext(ctx, downloadSpeedMeasureWindow, downloadSpeedMin)
   409  
   410  	var finalErr error
   411  	var dlSize float64
   412  	startTime := time.Now()
   413  	for attempt := retry.Start(downloadRetryStrategy, nil); attempt.Next(); {
   414  		reqOptions := downloadReqOpts(storeURL, cdnHeader, dlOpts)
   415  
   416  		httputil.MaybeLogRetryAttempt(reqOptions.URL.String(), attempt, startTime)
   417  
   418  		h := crypto.SHA3_384.New()
   419  
   420  		if resume > 0 {
   421  			reqOptions.ExtraHeaders["Range"] = fmt.Sprintf("bytes=%d-", resume)
   422  			// seed the sha3 with the already local file
   423  			if _, err := w.Seek(0, os.SEEK_SET); err != nil {
   424  				return err
   425  			}
   426  			n, err := io.Copy(h, w)
   427  			if err != nil {
   428  				return err
   429  			}
   430  			if n != resume {
   431  				return fmt.Errorf("resume offset wrong: %d != %d", resume, n)
   432  			}
   433  		}
   434  
   435  		if cancelled(downloadCtx) {
   436  			return fmt.Errorf("The download has been cancelled: %s", downloadCtx.Err())
   437  		}
   438  		var resp *http.Response
   439  		cli := s.newHTTPClient(nil)
   440  		resp, finalErr = s.doRequest(downloadCtx, cli, reqOptions, user)
   441  		if cancelled(downloadCtx) {
   442  			return fmt.Errorf("The download has been cancelled: %s", downloadCtx.Err())
   443  		}
   444  		if finalErr != nil {
   445  			if httputil.ShouldRetryAttempt(attempt, finalErr) {
   446  				continue
   447  			}
   448  			break
   449  		}
   450  		if resume > 0 && resp.StatusCode != 206 {
   451  			logger.Debugf("server does not support resume")
   452  			if _, err := w.Seek(0, os.SEEK_SET); err != nil {
   453  				return err
   454  			}
   455  			h = crypto.SHA3_384.New()
   456  			resume = 0
   457  		}
   458  		if httputil.ShouldRetryHttpResponse(attempt, resp) {
   459  			resp.Body.Close()
   460  			continue
   461  		}
   462  
   463  		defer resp.Body.Close()
   464  
   465  		switch resp.StatusCode {
   466  		case 200, 206: // OK, Partial Content
   467  		case 402: // Payment Required
   468  
   469  			return fmt.Errorf("please buy %s before installing it.", name)
   470  		default:
   471  			return &DownloadError{Code: resp.StatusCode, URL: resp.Request.URL}
   472  		}
   473  
   474  		if pbar == nil {
   475  			pbar = progress.Null
   476  		}
   477  		dlSize = float64(resp.ContentLength)
   478  		pbar.Start(name, dlSize)
   479  		mw := io.MultiWriter(w, h, pbar, tc)
   480  		var limiter io.Reader
   481  		limiter = resp.Body
   482  		if limit := dlOpts.RateLimit; limit > 0 {
   483  			bucket := ratelimit.NewBucketWithRate(float64(limit), 2*limit)
   484  			limiter = ratelimitReader(resp.Body, bucket)
   485  		}
   486  
   487  		stopMonitorCh := tc.Monitor()
   488  		_, finalErr = io.Copy(mw, limiter)
   489  		close(stopMonitorCh)
   490  		pbar.Finished()
   491  
   492  		if err := tc.Err(); err != nil {
   493  			return err
   494  		}
   495  		if cancelled(downloadCtx) {
   496  			// cancelled for other reason that download timeout (which would
   497  			// be caught by tc.Err() above).
   498  			return fmt.Errorf("The download has been cancelled: %s", downloadCtx.Err())
   499  		}
   500  
   501  		if finalErr != nil {
   502  			if httputil.ShouldRetryAttempt(attempt, finalErr) {
   503  				// error while downloading should resume
   504  				var seekerr error
   505  				resume, seekerr = w.Seek(0, os.SEEK_END)
   506  				if seekerr == nil {
   507  					continue
   508  				}
   509  				// if seek failed, then don't retry end return the original error
   510  			}
   511  			break
   512  		}
   513  
   514  		actualSha3 := fmt.Sprintf("%x", h.Sum(nil))
   515  		if sha3_384 != "" && sha3_384 != actualSha3 {
   516  			finalErr = HashError{name, actualSha3, sha3_384}
   517  		}
   518  		break
   519  	}
   520  	if finalErr == nil {
   521  		// not using quantity.FormatFoo as this is just for debug
   522  		dt := time.Since(startTime)
   523  		r := dlSize / dt.Seconds()
   524  		var p rune
   525  		for _, p = range " kMGTPEZY" {
   526  			if r < 1000 {
   527  				break
   528  			}
   529  			r /= 1000
   530  		}
   531  
   532  		logger.Debugf("Download succeeded in %.03fs (%.0f%cB/s).", dt.Seconds(), r, p)
   533  	}
   534  	return finalErr
   535  }
   536  
   537  // DownloadStream will copy the snap from the request to the io.Reader
   538  func (s *Store) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, resume int64, user *auth.UserState) (io.ReadCloser, int, error) {
   539  	// XXX: coverage of this is rather poor
   540  	if path := s.cacher.GetPath(downloadInfo.Sha3_384); path != "" {
   541  		logger.Debugf("Cache hit for SHA3_384 …%.5s.", downloadInfo.Sha3_384)
   542  		file, err := os.OpenFile(path, os.O_RDONLY, 0600)
   543  		if err != nil {
   544  			return nil, 0, err
   545  		}
   546  		if resume == 0 {
   547  			return file, 200, nil
   548  		}
   549  		_, err = file.Seek(resume, os.SEEK_SET)
   550  		if err != nil {
   551  			return nil, 0, err
   552  		}
   553  		return file, 206, nil
   554  	}
   555  
   556  	authAvail, err := s.authAvailable(user)
   557  	if err != nil {
   558  		return nil, 0, err
   559  	}
   560  
   561  	downloadURL := downloadInfo.AnonDownloadURL
   562  	if downloadURL == "" || authAvail {
   563  		downloadURL = downloadInfo.DownloadURL
   564  	}
   565  
   566  	storeURL, err := url.Parse(downloadURL)
   567  	if err != nil {
   568  		return nil, 0, err
   569  	}
   570  
   571  	cdnHeader, err := s.cdnHeader()
   572  	if err != nil {
   573  		return nil, 0, err
   574  	}
   575  
   576  	resp, err := doDownloadReq(ctx, storeURL, cdnHeader, resume, s, user)
   577  	if err != nil {
   578  		return nil, 0, err
   579  	}
   580  	return resp.Body, resp.StatusCode, nil
   581  }
   582  
   583  var doDownloadReq = doDownloadReqImpl
   584  
   585  func doDownloadReqImpl(ctx context.Context, storeURL *url.URL, cdnHeader string, resume int64, s *Store, user *auth.UserState) (*http.Response, error) {
   586  	reqOptions := downloadReqOpts(storeURL, cdnHeader, nil)
   587  	if resume > 0 {
   588  		reqOptions.ExtraHeaders["Range"] = fmt.Sprintf("bytes=%d-", resume)
   589  	}
   590  	cli := s.newHTTPClient(nil)
   591  	return s.doRequest(ctx, cli, reqOptions, user)
   592  }
   593  
   594  // downloadDelta downloads the delta for the preferred format, returning the path.
   595  func (s *Store) downloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error {
   596  
   597  	if len(downloadInfo.Deltas) != 1 {
   598  		return errors.New("store returned more than one download delta")
   599  	}
   600  
   601  	deltaInfo := downloadInfo.Deltas[0]
   602  
   603  	if deltaInfo.Format != s.deltaFormat {
   604  		return fmt.Errorf("store returned unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format)
   605  	}
   606  
   607  	authAvail, err := s.authAvailable(user)
   608  	if err != nil {
   609  		return err
   610  	}
   611  
   612  	url := deltaInfo.AnonDownloadURL
   613  	if url == "" || authAvail {
   614  		url = deltaInfo.DownloadURL
   615  	}
   616  
   617  	return download(context.TODO(), deltaName, deltaInfo.Sha3_384, url, user, s, w, 0, pbar, dlOpts)
   618  }
   619  
   620  func getXdelta3Cmd(args ...string) (*exec.Cmd, error) {
   621  	if osutil.ExecutableExists("xdelta3") {
   622  		return exec.Command("xdelta3", args...), nil
   623  	}
   624  	return snapdtool.CommandFromSystemSnap("/usr/bin/xdelta3", args...)
   625  }
   626  
   627  // applyDelta generates a target snap from a previously downloaded snap and a downloaded delta.
   628  var applyDelta = func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error {
   629  	snapBase := fmt.Sprintf("%s_%d.snap", name, deltaInfo.FromRevision)
   630  	snapPath := filepath.Join(dirs.SnapBlobDir, snapBase)
   631  
   632  	if !osutil.FileExists(snapPath) {
   633  		return fmt.Errorf("snap %q revision %d not found at %s", name, deltaInfo.FromRevision, snapPath)
   634  	}
   635  
   636  	if deltaInfo.Format != "xdelta3" {
   637  		return fmt.Errorf("cannot apply unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format)
   638  	}
   639  
   640  	partialTargetPath := targetPath + ".partial"
   641  
   642  	xdelta3Args := []string{"-d", "-s", snapPath, deltaPath, partialTargetPath}
   643  	cmd, err := getXdelta3Cmd(xdelta3Args...)
   644  	if err != nil {
   645  		return err
   646  	}
   647  
   648  	if err := cmd.Run(); err != nil {
   649  		if err := os.Remove(partialTargetPath); err != nil {
   650  			logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err)
   651  		}
   652  		return err
   653  	}
   654  
   655  	if err := os.Chmod(partialTargetPath, 0600); err != nil {
   656  		return err
   657  	}
   658  
   659  	bsha3_384, _, err := osutil.FileDigest(partialTargetPath, crypto.SHA3_384)
   660  	if err != nil {
   661  		return err
   662  	}
   663  	sha3_384 := fmt.Sprintf("%x", bsha3_384)
   664  	if targetSha3_384 != "" && sha3_384 != targetSha3_384 {
   665  		if err := os.Remove(partialTargetPath); err != nil {
   666  			logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err)
   667  		}
   668  		return HashError{name, sha3_384, targetSha3_384}
   669  	}
   670  
   671  	if err := os.Rename(partialTargetPath, targetPath); err != nil {
   672  		return osutil.CopyFile(partialTargetPath, targetPath, 0)
   673  	}
   674  
   675  	return nil
   676  }
   677  
   678  // downloadAndApplyDelta downloads and then applies the delta to the current snap.
   679  func (s *Store) downloadAndApplyDelta(name, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error {
   680  	deltaInfo := &downloadInfo.Deltas[0]
   681  
   682  	deltaPath := fmt.Sprintf("%s.%s-%d-to-%d.partial", targetPath, deltaInfo.Format, deltaInfo.FromRevision, deltaInfo.ToRevision)
   683  	deltaName := fmt.Sprintf(i18n.G("%s (delta)"), name)
   684  
   685  	w, err := os.OpenFile(deltaPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
   686  	if err != nil {
   687  		return err
   688  	}
   689  	defer func() {
   690  		if cerr := w.Close(); cerr != nil && err == nil {
   691  			err = cerr
   692  		}
   693  		os.Remove(deltaPath)
   694  	}()
   695  
   696  	err = s.downloadDelta(deltaName, downloadInfo, w, pbar, user, dlOpts)
   697  	if err != nil {
   698  		return err
   699  	}
   700  
   701  	logger.Debugf("Successfully downloaded delta for %q at %s", name, deltaPath)
   702  	if err := applyDelta(name, deltaPath, deltaInfo, targetPath, downloadInfo.Sha3_384); err != nil {
   703  		return err
   704  	}
   705  
   706  	logger.Debugf("Successfully applied delta for %q at %s, saving %d bytes.", name, deltaPath, downloadInfo.Size-deltaInfo.Size)
   707  	return nil
   708  }
   709  
   710  func (s *Store) CacheDownloads() int {
   711  	return s.cfg.CacheDownloads
   712  }
   713  
   714  func (s *Store) SetCacheDownloads(fileCount int) {
   715  	s.cfg.CacheDownloads = fileCount
   716  	if fileCount > 0 {
   717  		s.cacher = NewCacheManager(dirs.SnapDownloadCacheDir, fileCount)
   718  	} else {
   719  		s.cacher = &nullCache{}
   720  	}
   721  }