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 }