gitee.com/mysnapcore/mysnapd@v0.1.0/store/store_download.go (about)

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