github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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 "strings" 35 "time" 36 37 "github.com/juju/ratelimit" 38 "gopkg.in/retry.v1" 39 40 "github.com/snapcore/snapd/dirs" 41 "github.com/snapcore/snapd/httputil" 42 "github.com/snapcore/snapd/i18n" 43 "github.com/snapcore/snapd/logger" 44 "github.com/snapcore/snapd/osutil" 45 "github.com/snapcore/snapd/overlord/auth" 46 "github.com/snapcore/snapd/progress" 47 "github.com/snapcore/snapd/snap" 48 "github.com/snapcore/snapd/snapdtool" 49 ) 50 51 var downloadRetryStrategy = retry.LimitCount(7, retry.LimitTime(90*time.Second, 52 retry.Exponential{ 53 Initial: 500 * time.Millisecond, 54 Factor: 2.5, 55 }, 56 )) 57 58 // Deltas enabled by default on classic, but allow opting in or out on both classic and core. 59 func useDeltas() bool { 60 // only xdelta3 is supported for now, so check the binary exists here 61 // TODO: have a per-format checker instead 62 if _, err := getXdelta3Cmd(); err != nil { 63 return false 64 } 65 66 return osutil.GetenvBool("SNAPD_USE_DELTAS_EXPERIMENTAL", true) 67 } 68 69 func (s *Store) cdnHeader() (string, error) { 70 if s.noCDN { 71 return "none", nil 72 } 73 74 if s.dauthCtx == nil { 75 return "", nil 76 } 77 78 // set Snap-CDN from cloud instance information 79 // if available 80 81 // TODO: do we want a more complex retry strategy 82 // where we first to send this header and if the 83 // operation fails that way to even get the connection 84 // then we retry without sending this? 85 86 cloudInfo, err := s.dauthCtx.CloudInfo() 87 if err != nil { 88 return "", err 89 } 90 91 if cloudInfo != nil { 92 cdnParams := []string{fmt.Sprintf("cloud-name=%q", cloudInfo.Name)} 93 if cloudInfo.Region != "" { 94 cdnParams = append(cdnParams, fmt.Sprintf("region=%q", cloudInfo.Region)) 95 } 96 if cloudInfo.AvailabilityZone != "" { 97 cdnParams = append(cdnParams, fmt.Sprintf("availability-zone=%q", cloudInfo.AvailabilityZone)) 98 } 99 100 return strings.Join(cdnParams, " "), nil 101 } 102 103 return "", nil 104 } 105 106 type HashError struct { 107 name string 108 sha3_384 string 109 targetSha3_384 string 110 } 111 112 func (e HashError) Error() string { 113 return fmt.Sprintf("sha3-384 mismatch for %q: got %s but expected %s", e.name, e.sha3_384, e.targetSha3_384) 114 } 115 116 type DownloadOptions struct { 117 RateLimit int64 118 IsAutoRefresh bool 119 LeavePartialOnError bool 120 } 121 122 // Download downloads the snap addressed by download info and returns its 123 // filename. 124 // The file is saved in temporary storage, and should be removed 125 // after use to prevent the disk from running out of space. 126 func (s *Store) Download(ctx context.Context, name string, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error { 127 if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { 128 return err 129 } 130 131 if err := s.cacher.Get(downloadInfo.Sha3_384, targetPath); err == nil { 132 logger.Debugf("Cache hit for SHA3_384 …%.5s.", downloadInfo.Sha3_384) 133 return nil 134 } 135 136 if useDeltas() { 137 logger.Debugf("Available deltas returned by store: %v", downloadInfo.Deltas) 138 139 if len(downloadInfo.Deltas) == 1 { 140 err := s.downloadAndApplyDelta(name, targetPath, downloadInfo, pbar, user, dlOpts) 141 if err == nil { 142 return nil 143 } 144 // We revert to normal downloads if there is any error. 145 logger.Noticef("Cannot download or apply deltas for %s: %v", name, err) 146 } 147 } 148 149 partialPath := targetPath + ".partial" 150 w, err := os.OpenFile(partialPath, os.O_RDWR|os.O_CREATE, 0600) 151 if err != nil { 152 return err 153 } 154 resume, err := w.Seek(0, os.SEEK_END) 155 if err != nil { 156 return err 157 } 158 defer func() { 159 fi, _ := w.Stat() 160 if cerr := w.Close(); cerr != nil && err == nil { 161 err = cerr 162 } 163 if err == nil { 164 return 165 } 166 if dlOpts == nil || !dlOpts.LeavePartialOnError || fi == nil || fi.Size() == 0 { 167 os.Remove(w.Name()) 168 } 169 }() 170 if resume > 0 { 171 logger.Debugf("Resuming download of %q at %d.", partialPath, resume) 172 } else { 173 logger.Debugf("Starting download of %q.", partialPath) 174 } 175 176 authAvail, err := s.authAvailable(user) 177 if err != nil { 178 return err 179 } 180 181 url := downloadInfo.AnonDownloadURL 182 if url == "" || authAvail { 183 url = downloadInfo.DownloadURL 184 } 185 186 if downloadInfo.Size == 0 || resume < downloadInfo.Size { 187 err = download(ctx, name, downloadInfo.Sha3_384, url, user, s, w, resume, pbar, dlOpts) 188 if err != nil { 189 logger.Debugf("download of %q failed: %#v", url, err) 190 } 191 } else { 192 // we're done! check the hash though 193 h := crypto.SHA3_384.New() 194 if _, err := w.Seek(0, os.SEEK_SET); err != nil { 195 return err 196 } 197 if _, err := io.Copy(h, w); err != nil { 198 return err 199 } 200 actualSha3 := fmt.Sprintf("%x", h.Sum(nil)) 201 if downloadInfo.Sha3_384 != actualSha3 { 202 err = HashError{name, actualSha3, downloadInfo.Sha3_384} 203 } 204 } 205 // If hashsum is incorrect retry once 206 if _, ok := err.(HashError); ok { 207 logger.Debugf("Hashsum error on download: %v", err.Error()) 208 logger.Debugf("Truncating and trying again from scratch.") 209 err = w.Truncate(0) 210 if err != nil { 211 return err 212 } 213 _, err = w.Seek(0, os.SEEK_SET) 214 if err != nil { 215 return err 216 } 217 err = download(ctx, name, downloadInfo.Sha3_384, url, user, s, w, 0, pbar, nil) 218 if err != nil { 219 logger.Debugf("download of %q failed: %#v", url, err) 220 } 221 } 222 223 if err != nil { 224 return err 225 } 226 227 if err := os.Rename(w.Name(), targetPath); err != nil { 228 return err 229 } 230 231 if err := w.Sync(); err != nil { 232 return err 233 } 234 235 return s.cacher.Put(downloadInfo.Sha3_384, targetPath) 236 } 237 238 func downloadReqOpts(storeURL *url.URL, cdnHeader string, opts *DownloadOptions) *requestOptions { 239 reqOptions := requestOptions{ 240 Method: "GET", 241 URL: storeURL, 242 ExtraHeaders: map[string]string{}, 243 // FIXME: use the new headers? with 244 // APILevel: apiV2Endps, 245 } 246 if cdnHeader != "" { 247 reqOptions.ExtraHeaders["Snap-CDN"] = cdnHeader 248 } 249 if opts != nil && opts.IsAutoRefresh { 250 reqOptions.ExtraHeaders["Snap-Refresh-Reason"] = "scheduled" 251 } 252 253 return &reqOptions 254 } 255 256 var ratelimitReader = ratelimit.Reader 257 258 var download = downloadImpl 259 260 // download writes an http.Request showing a progress.Meter 261 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 { 262 if dlOpts == nil { 263 dlOpts = &DownloadOptions{} 264 } 265 266 storeURL, err := url.Parse(downloadURL) 267 if err != nil { 268 return err 269 } 270 271 cdnHeader, err := s.cdnHeader() 272 if err != nil { 273 return err 274 } 275 276 var finalErr error 277 var dlSize float64 278 startTime := time.Now() 279 for attempt := retry.Start(downloadRetryStrategy, nil); attempt.Next(); { 280 reqOptions := downloadReqOpts(storeURL, cdnHeader, dlOpts) 281 282 httputil.MaybeLogRetryAttempt(reqOptions.URL.String(), attempt, startTime) 283 284 h := crypto.SHA3_384.New() 285 286 if resume > 0 { 287 reqOptions.ExtraHeaders["Range"] = fmt.Sprintf("bytes=%d-", resume) 288 // seed the sha3 with the already local file 289 if _, err := w.Seek(0, os.SEEK_SET); err != nil { 290 return err 291 } 292 n, err := io.Copy(h, w) 293 if err != nil { 294 return err 295 } 296 if n != resume { 297 return fmt.Errorf("resume offset wrong: %d != %d", resume, n) 298 } 299 } 300 301 if cancelled(ctx) { 302 return fmt.Errorf("The download has been cancelled: %s", ctx.Err()) 303 } 304 var resp *http.Response 305 cli := s.newHTTPClient(nil) 306 resp, finalErr = s.doRequest(ctx, cli, reqOptions, user) 307 308 if cancelled(ctx) { 309 return fmt.Errorf("The download has been cancelled: %s", ctx.Err()) 310 } 311 if finalErr != nil { 312 if httputil.ShouldRetryAttempt(attempt, finalErr) { 313 continue 314 } 315 break 316 } 317 if resume > 0 && resp.StatusCode != 206 { 318 logger.Debugf("server does not support resume") 319 if _, err := w.Seek(0, os.SEEK_SET); err != nil { 320 return err 321 } 322 h = crypto.SHA3_384.New() 323 resume = 0 324 } 325 if httputil.ShouldRetryHttpResponse(attempt, resp) { 326 resp.Body.Close() 327 continue 328 } 329 330 defer resp.Body.Close() 331 332 switch resp.StatusCode { 333 case 200, 206: // OK, Partial Content 334 case 402: // Payment Required 335 336 return fmt.Errorf("please buy %s before installing it.", name) 337 default: 338 return &DownloadError{Code: resp.StatusCode, URL: resp.Request.URL} 339 } 340 341 if pbar == nil { 342 pbar = progress.Null 343 } 344 dlSize = float64(resp.ContentLength) 345 pbar.Start(name, dlSize) 346 mw := io.MultiWriter(w, h, pbar) 347 var limiter io.Reader 348 limiter = resp.Body 349 if limit := dlOpts.RateLimit; limit > 0 { 350 bucket := ratelimit.NewBucketWithRate(float64(limit), 2*limit) 351 limiter = ratelimitReader(resp.Body, bucket) 352 } 353 _, finalErr = io.Copy(mw, limiter) 354 pbar.Finished() 355 if finalErr != nil { 356 if httputil.ShouldRetryAttempt(attempt, finalErr) { 357 // error while downloading should resume 358 var seekerr error 359 resume, seekerr = w.Seek(0, os.SEEK_END) 360 if seekerr == nil { 361 continue 362 } 363 // if seek failed, then don't retry end return the original error 364 } 365 break 366 } 367 368 if cancelled(ctx) { 369 return fmt.Errorf("The download has been cancelled: %s", ctx.Err()) 370 } 371 372 actualSha3 := fmt.Sprintf("%x", h.Sum(nil)) 373 if sha3_384 != "" && sha3_384 != actualSha3 { 374 finalErr = HashError{name, actualSha3, sha3_384} 375 } 376 break 377 } 378 if finalErr == nil { 379 // not using quantity.FormatFoo as this is just for debug 380 dt := time.Since(startTime) 381 r := dlSize / dt.Seconds() 382 var p rune 383 for _, p = range " kMGTPEZY" { 384 if r < 1000 { 385 break 386 } 387 r /= 1000 388 } 389 390 logger.Debugf("Download succeeded in %.03fs (%.0f%cB/s).", dt.Seconds(), r, p) 391 } 392 return finalErr 393 } 394 395 // DownloadStream will copy the snap from the request to the io.Reader 396 func (s *Store) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, resume int64, user *auth.UserState) (io.ReadCloser, int, error) { 397 // XXX: coverage of this is rather poor 398 if path := s.cacher.GetPath(downloadInfo.Sha3_384); path != "" { 399 logger.Debugf("Cache hit for SHA3_384 …%.5s.", downloadInfo.Sha3_384) 400 file, err := os.OpenFile(path, os.O_RDONLY, 0600) 401 if err != nil { 402 return nil, 0, err 403 } 404 if resume == 0 { 405 return file, 200, nil 406 } 407 _, err = file.Seek(resume, os.SEEK_SET) 408 if err != nil { 409 return nil, 0, err 410 } 411 return file, 206, nil 412 } 413 414 authAvail, err := s.authAvailable(user) 415 if err != nil { 416 return nil, 0, err 417 } 418 419 downloadURL := downloadInfo.AnonDownloadURL 420 if downloadURL == "" || authAvail { 421 downloadURL = downloadInfo.DownloadURL 422 } 423 424 storeURL, err := url.Parse(downloadURL) 425 if err != nil { 426 return nil, 0, err 427 } 428 429 cdnHeader, err := s.cdnHeader() 430 if err != nil { 431 return nil, 0, err 432 } 433 434 resp, err := doDownloadReq(ctx, storeURL, cdnHeader, resume, s, user) 435 if err != nil { 436 return nil, 0, err 437 } 438 return resp.Body, resp.StatusCode, nil 439 } 440 441 var doDownloadReq = doDownloadReqImpl 442 443 func doDownloadReqImpl(ctx context.Context, storeURL *url.URL, cdnHeader string, resume int64, s *Store, user *auth.UserState) (*http.Response, error) { 444 reqOptions := downloadReqOpts(storeURL, cdnHeader, nil) 445 if resume > 0 { 446 reqOptions.ExtraHeaders["Range"] = fmt.Sprintf("bytes=%d-", resume) 447 } 448 cli := s.newHTTPClient(nil) 449 return s.doRequest(ctx, cli, reqOptions, user) 450 } 451 452 // downloadDelta downloads the delta for the preferred format, returning the path. 453 func (s *Store) downloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error { 454 455 if len(downloadInfo.Deltas) != 1 { 456 return errors.New("store returned more than one download delta") 457 } 458 459 deltaInfo := downloadInfo.Deltas[0] 460 461 if deltaInfo.Format != s.deltaFormat { 462 return fmt.Errorf("store returned unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format) 463 } 464 465 authAvail, err := s.authAvailable(user) 466 if err != nil { 467 return err 468 } 469 470 url := deltaInfo.AnonDownloadURL 471 if url == "" || authAvail { 472 url = deltaInfo.DownloadURL 473 } 474 475 return download(context.TODO(), deltaName, deltaInfo.Sha3_384, url, user, s, w, 0, pbar, dlOpts) 476 } 477 478 func getXdelta3Cmd(args ...string) (*exec.Cmd, error) { 479 if osutil.ExecutableExists("xdelta3") { 480 return exec.Command("xdelta3", args...), nil 481 } 482 return snapdtool.CommandFromSystemSnap("/usr/bin/xdelta3", args...) 483 } 484 485 // applyDelta generates a target snap from a previously downloaded snap and a downloaded delta. 486 var applyDelta = func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error { 487 snapBase := fmt.Sprintf("%s_%d.snap", name, deltaInfo.FromRevision) 488 snapPath := filepath.Join(dirs.SnapBlobDir, snapBase) 489 490 if !osutil.FileExists(snapPath) { 491 return fmt.Errorf("snap %q revision %d not found at %s", name, deltaInfo.FromRevision, snapPath) 492 } 493 494 if deltaInfo.Format != "xdelta3" { 495 return fmt.Errorf("cannot apply unsupported delta format %q (only xdelta3 currently)", deltaInfo.Format) 496 } 497 498 partialTargetPath := targetPath + ".partial" 499 500 xdelta3Args := []string{"-d", "-s", snapPath, deltaPath, partialTargetPath} 501 cmd, err := getXdelta3Cmd(xdelta3Args...) 502 if err != nil { 503 return err 504 } 505 506 if err := cmd.Run(); err != nil { 507 if err := os.Remove(partialTargetPath); err != nil { 508 logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err) 509 } 510 return err 511 } 512 513 if err := os.Chmod(partialTargetPath, 0600); err != nil { 514 return err 515 } 516 517 bsha3_384, _, err := osutil.FileDigest(partialTargetPath, crypto.SHA3_384) 518 if err != nil { 519 return err 520 } 521 sha3_384 := fmt.Sprintf("%x", bsha3_384) 522 if targetSha3_384 != "" && sha3_384 != targetSha3_384 { 523 if err := os.Remove(partialTargetPath); err != nil { 524 logger.Noticef("failed to remove partial delta target %q: %s", partialTargetPath, err) 525 } 526 return HashError{name, sha3_384, targetSha3_384} 527 } 528 529 if err := os.Rename(partialTargetPath, targetPath); err != nil { 530 return osutil.CopyFile(partialTargetPath, targetPath, 0) 531 } 532 533 return nil 534 } 535 536 // downloadAndApplyDelta downloads and then applies the delta to the current snap. 537 func (s *Store) downloadAndApplyDelta(name, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error { 538 deltaInfo := &downloadInfo.Deltas[0] 539 540 deltaPath := fmt.Sprintf("%s.%s-%d-to-%d.partial", targetPath, deltaInfo.Format, deltaInfo.FromRevision, deltaInfo.ToRevision) 541 deltaName := fmt.Sprintf(i18n.G("%s (delta)"), name) 542 543 w, err := os.OpenFile(deltaPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 544 if err != nil { 545 return err 546 } 547 defer func() { 548 if cerr := w.Close(); cerr != nil && err == nil { 549 err = cerr 550 } 551 os.Remove(deltaPath) 552 }() 553 554 err = s.downloadDelta(deltaName, downloadInfo, w, pbar, user, dlOpts) 555 if err != nil { 556 return err 557 } 558 559 logger.Debugf("Successfully downloaded delta for %q at %s", name, deltaPath) 560 if err := applyDelta(name, deltaPath, deltaInfo, targetPath, downloadInfo.Sha3_384); err != nil { 561 return err 562 } 563 564 logger.Debugf("Successfully applied delta for %q at %s, saving %d bytes.", name, deltaPath, downloadInfo.Size-deltaInfo.Size) 565 return nil 566 } 567 568 func (s *Store) CacheDownloads() int { 569 return s.cfg.CacheDownloads 570 } 571 572 func (s *Store) SetCacheDownloads(fileCount int) { 573 s.cfg.CacheDownloads = fileCount 574 if fileCount > 0 { 575 s.cacher = NewCacheManager(dirs.SnapDownloadCacheDir, fileCount) 576 } else { 577 s.cacher = &nullCache{} 578 } 579 }