github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/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, io.SeekEnd) 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, io.SeekStart) 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.Since(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, io.SeekStart); 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, io.SeekStart); 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 // XXX: we're inside retry loop, so this will be closed only on return. 464 defer resp.Body.Close() 465 466 switch resp.StatusCode { 467 case 200, 206: // OK, Partial Content 468 case 402: // Payment Required 469 470 return fmt.Errorf("please buy %s before installing it", name) 471 default: 472 return &DownloadError{Code: resp.StatusCode, URL: resp.Request.URL} 473 } 474 475 if pbar == nil { 476 pbar = progress.Null 477 } 478 dlSize = float64(resp.ContentLength) 479 if resp.ContentLength == 0 { 480 logger.Noticef("Unexpected Content-Length: 0 for %s", downloadURL) 481 } else { 482 logger.Noticef("Download size for %s: %d", downloadURL, resp.ContentLength) 483 } 484 pbar.Start(name, dlSize) 485 mw := io.MultiWriter(w, h, pbar, tc) 486 var limiter io.Reader 487 limiter = resp.Body 488 if limit := dlOpts.RateLimit; limit > 0 { 489 bucket := ratelimit.NewBucketWithRate(float64(limit), 2*limit) 490 limiter = ratelimitReader(resp.Body, bucket) 491 } 492 493 stopMonitorCh := tc.Monitor() 494 _, finalErr = io.Copy(mw, limiter) 495 close(stopMonitorCh) 496 pbar.Finished() 497 498 if err := tc.Err(); err != nil { 499 return err 500 } 501 if cancelled(downloadCtx) { 502 // cancelled for other reason that download timeout (which would 503 // be caught by tc.Err() above). 504 return fmt.Errorf("the download has been cancelled: %s", downloadCtx.Err()) 505 } 506 507 if finalErr != nil { 508 if httputil.ShouldRetryAttempt(attempt, finalErr) { 509 // error while downloading should resume 510 var seekerr error 511 resume, seekerr = w.Seek(0, io.SeekEnd) 512 if seekerr == nil { 513 continue 514 } 515 // if seek failed, then don't retry end return the original error 516 } 517 break 518 } 519 520 actualSha3 := fmt.Sprintf("%x", h.Sum(nil)) 521 if sha3_384 != "" && sha3_384 != actualSha3 { 522 finalErr = HashError{name, actualSha3, sha3_384} 523 } 524 break 525 } 526 if finalErr == nil { 527 // not using quantity.FormatFoo as this is just for debug 528 dt := time.Since(startTime) 529 r := dlSize / dt.Seconds() 530 var p rune 531 for _, p = range " kMGTPEZY" { 532 if r < 1000 { 533 break 534 } 535 r /= 1000 536 } 537 538 logger.Debugf("Download succeeded in %.03fs (%.0f%cB/s).", dt.Seconds(), r, p) 539 } 540 return finalErr 541 } 542 543 // DownloadStream will copy the snap from the request to the io.Reader 544 func (s *Store) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, resume int64, user *auth.UserState) (io.ReadCloser, int, error) { 545 // XXX: coverage of this is rather poor 546 if path := s.cacher.GetPath(downloadInfo.Sha3_384); path != "" { 547 logger.Debugf("Cache hit for SHA3_384 …%.5s.", downloadInfo.Sha3_384) 548 file, err := os.OpenFile(path, os.O_RDONLY, 0600) 549 if err != nil { 550 return nil, 0, err 551 } 552 if resume == 0 { 553 return file, 200, nil 554 } 555 _, err = file.Seek(resume, io.SeekStart) 556 if err != nil { 557 return nil, 0, err 558 } 559 return file, 206, nil 560 } 561 562 authAvail, err := s.authAvailable(user) 563 if err != nil { 564 return nil, 0, err 565 } 566 567 downloadURL := downloadInfo.AnonDownloadURL 568 if downloadURL == "" || authAvail { 569 downloadURL = downloadInfo.DownloadURL 570 } 571 572 storeURL, err := url.Parse(downloadURL) 573 if err != nil { 574 return nil, 0, err 575 } 576 577 cdnHeader, err := s.cdnHeader() 578 if err != nil { 579 return nil, 0, err 580 } 581 582 resp, err := doDownloadReq(ctx, storeURL, cdnHeader, resume, s, user) 583 if err != nil { 584 return nil, 0, err 585 } 586 return resp.Body, resp.StatusCode, nil 587 } 588 589 var doDownloadReq = doDownloadReqImpl 590 591 func doDownloadReqImpl(ctx context.Context, storeURL *url.URL, cdnHeader string, resume int64, s *Store, user *auth.UserState) (*http.Response, error) { 592 reqOptions := downloadReqOpts(storeURL, cdnHeader, nil) 593 if resume > 0 { 594 reqOptions.ExtraHeaders["Range"] = fmt.Sprintf("bytes=%d-", resume) 595 } 596 cli := s.newHTTPClient(nil) 597 return s.doRequest(ctx, cli, reqOptions, user) 598 } 599 600 // downloadDelta downloads the delta for the preferred format, returning the path. 601 func (s *Store) downloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error { 602 603 if len(downloadInfo.Deltas) != 1 { 604 return errors.New("store returned more than one download delta") 605 } 606 607 deltaInfo := downloadInfo.Deltas[0] 608 609 if deltaInfo.Format != s.deltaFormat { 610 return fmt.Errorf("store returned unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format) 611 } 612 613 authAvail, err := s.authAvailable(user) 614 if err != nil { 615 return err 616 } 617 618 url := deltaInfo.AnonDownloadURL 619 if url == "" || authAvail { 620 url = deltaInfo.DownloadURL 621 } 622 623 return download(context.TODO(), deltaName, deltaInfo.Sha3_384, url, user, s, w, 0, pbar, dlOpts) 624 } 625 626 func getXdelta3Cmd(args ...string) (*exec.Cmd, error) { 627 if osutil.ExecutableExists("xdelta3") { 628 return exec.Command("xdelta3", args...), nil 629 } 630 return snapdtool.CommandFromSystemSnap("/usr/bin/xdelta3", args...) 631 } 632 633 // applyDelta generates a target snap from a previously downloaded snap and a downloaded delta. 634 var applyDelta = func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error { 635 snapBase := fmt.Sprintf("%s_%d.snap", name, deltaInfo.FromRevision) 636 snapPath := filepath.Join(dirs.SnapBlobDir, snapBase) 637 638 if !osutil.FileExists(snapPath) { 639 return fmt.Errorf("snap %q revision %d not found at %s", name, deltaInfo.FromRevision, snapPath) 640 } 641 642 if deltaInfo.Format != "xdelta3" { 643 return fmt.Errorf("cannot apply unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format) 644 } 645 646 partialTargetPath := targetPath + ".partial" 647 648 xdelta3Args := []string{"-d", "-s", snapPath, deltaPath, partialTargetPath} 649 cmd, err := getXdelta3Cmd(xdelta3Args...) 650 if err != nil { 651 return err 652 } 653 654 if err := cmd.Run(); err != nil { 655 if err := os.Remove(partialTargetPath); err != nil { 656 logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err) 657 } 658 return err 659 } 660 661 if err := os.Chmod(partialTargetPath, 0600); err != nil { 662 return err 663 } 664 665 bsha3_384, _, err := osutil.FileDigest(partialTargetPath, crypto.SHA3_384) 666 if err != nil { 667 return err 668 } 669 sha3_384 := fmt.Sprintf("%x", bsha3_384) 670 if targetSha3_384 != "" && sha3_384 != targetSha3_384 { 671 if err := os.Remove(partialTargetPath); err != nil { 672 logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err) 673 } 674 return HashError{name, sha3_384, targetSha3_384} 675 } 676 677 if err := os.Rename(partialTargetPath, targetPath); err != nil { 678 return osutil.CopyFile(partialTargetPath, targetPath, 0) 679 } 680 681 return nil 682 } 683 684 // downloadAndApplyDelta downloads and then applies the delta to the current snap. 685 func (s *Store) downloadAndApplyDelta(name, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error { 686 deltaInfo := &downloadInfo.Deltas[0] 687 688 deltaPath := fmt.Sprintf("%s.%s-%d-to-%d.partial", targetPath, deltaInfo.Format, deltaInfo.FromRevision, deltaInfo.ToRevision) 689 deltaName := fmt.Sprintf(i18n.G("%s (delta)"), name) 690 691 w, err := os.OpenFile(deltaPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 692 if err != nil { 693 return err 694 } 695 defer func() { 696 if cerr := w.Close(); cerr != nil && err == nil { 697 err = cerr 698 } 699 os.Remove(deltaPath) 700 }() 701 702 err = s.downloadDelta(deltaName, downloadInfo, w, pbar, user, dlOpts) 703 if err != nil { 704 return err 705 } 706 707 logger.Debugf("Successfully downloaded delta for %q at %s", name, deltaPath) 708 if err := applyDelta(name, deltaPath, deltaInfo, targetPath, downloadInfo.Sha3_384); err != nil { 709 return err 710 } 711 712 logger.Debugf("Successfully applied delta for %q at %s, saving %d bytes.", name, deltaPath, downloadInfo.Size-deltaInfo.Size) 713 return nil 714 } 715 716 func (s *Store) CacheDownloads() int { 717 return s.cfg.CacheDownloads 718 } 719 720 func (s *Store) SetCacheDownloads(fileCount int) { 721 s.cfg.CacheDownloads = fileCount 722 if fileCount > 0 { 723 s.cacher = NewCacheManager(dirs.SnapDownloadCacheDir, fileCount) 724 } else { 725 s.cacher = &nullCache{} 726 } 727 }