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 }