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