diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2020-01-16 18:13:55 +0100 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2020-01-16 18:31:36 +0100 |
| commit | df459178d25f714c96cb1853e52d3fb1cdc9c9b2 (patch) | |
| tree | 04961bcbcbc49e35c5e303d8d17aefe67ddb5249 | |
| parent | e7f2ed36b7a37d2f0d447992721c75265a7b37d7 (diff) | |
daemon, store: support download resume from /v2/downloadapi-download-resume
This commit adds support for resuming downloads from the /v2/download endpoint.
| -rw-r--r-- | daemon/api_download.go | 4 | ||||
| -rw-r--r-- | daemon/api_download_test.go | 22 | ||||
| -rw-r--r-- | overlord/snapstate/backend.go | 2 | ||||
| -rw-r--r-- | store/export_test.go | 2 | ||||
| -rw-r--r-- | store/store.go | 22 | ||||
| -rw-r--r-- | store/store_test.go | 32 | ||||
| -rw-r--r-- | store/storetest/storetest.go | 2 |
7 files changed, 75 insertions, 11 deletions
diff --git a/daemon/api_download.go b/daemon/api_download.go index 4c7bf3b129..1bc0a52897 100644 --- a/daemon/api_download.go +++ b/daemon/api_download.go @@ -39,6 +39,7 @@ var snapDownloadCmd = &Command{ // SnapDownloadAction is used to request a snap download type snapDownloadAction struct { SnapName string `json:"snap-name,omitempty"` + Resume int64 `json:"resume,omitempty"` snapRevisionOptions } @@ -86,7 +87,8 @@ func streamOneSnap(c *Command, action snapDownloadAction, user *auth.UserState) info := sars[0].Info downloadInfo := info.DownloadInfo - r, err := getStore(c).DownloadStream(context.TODO(), action.SnapName, &downloadInfo, user) + dlOpts := &store.DownloadOptions{Resume: action.Resume} + r, err := getStore(c).DownloadStream(context.TODO(), action.SnapName, &downloadInfo, user, dlOpts) if err != nil { return InternalError(err.Error()) } diff --git a/daemon/api_download_test.go b/daemon/api_download_test.go index d595890457..89618a477e 100644 --- a/daemon/api_download_test.go +++ b/daemon/api_download_test.go @@ -104,6 +104,17 @@ var storeSnaps = map[string]*snap.Info{ Sha3_384: "sha3sha3sha3", }, }, + "foo-resume": { + SideInfo: snap.SideInfo{ + RealName: "foo-resume", + Revision: snap.R(1), + }, + DownloadInfo: snap.DownloadInfo{ + Size: int64(len(snapContent)), + AnonDownloadURL: "http://localhost/foo-resume", + Sha3_384: "sha3sha3sha3", + }, + }, "download-error-trigger-snap": { DownloadInfo: snap.DownloadInfo{ Size: 100, @@ -134,10 +145,13 @@ func (s *snapDownloadSuite) SnapAction(ctx context.Context, currentSnaps []*stor return []store.SnapActionResult{{Info: info}}, nil } -func (s *snapDownloadSuite) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, user *auth.UserState) (io.ReadCloser, error) { +func (s *snapDownloadSuite) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, user *auth.UserState, dlOpts *store.DownloadOptions) (io.ReadCloser, error) { if name == "download-error-trigger-snap" { return nil, fmt.Errorf("error triggered by download-error-trigger-snap") } + if name == "foo-resume" && dlOpts.Resume != 77 { + panic("foo-resume should set dlOpts.Resume to 77") + } if _, ok := storeSnaps[name]; ok { return ioutil.NopCloser(bytes.NewReader([]byte(snapContent))), nil } @@ -223,6 +237,12 @@ func (s *snapDownloadSuite) TestStreamOneSnap(c *check.C) { status: 200, err: "", }, + { + snapName: "foo-resume", + dataJSON: `{"snap-name": "foo-resume", "resume":77}`, + status: 200, + err: "", + }, } { req, err := http.NewRequest("POST", "/v2/download", strings.NewReader(s.dataJSON)) c.Assert(err, check.IsNil) diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index f8ca02411d..4b1719c7b6 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -47,7 +47,7 @@ type StoreService interface { WriteCatalogs(ctx context.Context, names io.Writer, adder store.SnapAdder) error Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState, *store.DownloadOptions) error - DownloadStream(context.Context, string, *snap.DownloadInfo, *auth.UserState) (io.ReadCloser, error) + DownloadStream(context.Context, string, *snap.DownloadInfo, *auth.UserState, *store.DownloadOptions) (io.ReadCloser, error) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) diff --git a/store/export_test.go b/store/export_test.go index 73820da253..74b893fbde 100644 --- a/store/export_test.go +++ b/store/export_test.go @@ -113,7 +113,7 @@ func MockDownload(f func(ctx context.Context, name, sha3_384, downloadURL string } } -func MockDoDownloadReq(f func(ctx context.Context, storeURL *url.URL, cdnHeader string, s *Store, user *auth.UserState) (*http.Response, error)) (restore func()) { +func MockDoDownloadReq(f func(ctx context.Context, storeURL *url.URL, cdnHeader string, opts *DownloadOptions, s *Store, user *auth.UserState) (*http.Response, error)) (restore func()) { orig := doDownloadReq doDownloadReq = f return func() { diff --git a/store/store.go b/store/store.go index 5ed78b4dd6..5faf61cbd6 100644 --- a/store/store.go +++ b/store/store.go @@ -1338,6 +1338,7 @@ type DownloadOptions struct { RateLimit int64 IsAutoRefresh bool LeavePartialOnError bool + Resume int64 } // Download downloads the snap addressed by download info and returns its @@ -1470,6 +1471,9 @@ func downloadReqOpts(storeURL *url.URL, cdnHeader string, opts *DownloadOptions) if opts != nil && opts.IsAutoRefresh { reqOptions.ExtraHeaders["Snap-Refresh-Reason"] = "scheduled" } + if opts != nil && opts.Resume > 0 { + reqOptions.ExtraHeaders["Range"] = fmt.Sprintf("bytes=%d-", opts.Resume) + } return &reqOptions } @@ -1606,13 +1610,23 @@ func downloadImpl(ctx context.Context, name, sha3_384, downloadURL string, user } // DownloadStream will copy the snap from the request to the io.Reader -func (s *Store) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, user *auth.UserState) (io.ReadCloser, error) { +func (s *Store) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, user *auth.UserState, dlOpts *DownloadOptions) (io.ReadCloser, error) { + if dlOpts == nil { + dlOpts = &DownloadOptions{} + } if path := s.cacher.GetPath(downloadInfo.Sha3_384); path != "" { logger.Debugf("Cache hit for SHA3_384 …%.5s.", downloadInfo.Sha3_384) file, err := os.OpenFile(path, os.O_RDONLY, 0600) if err != nil { return nil, err } + + if dlOpts.Resume > 0 { + if _, err := file.Seek(dlOpts.Resume, 0); err != nil { + return nil, err + } + } + return file, nil } @@ -1636,7 +1650,7 @@ func (s *Store) DownloadStream(ctx context.Context, name string, downloadInfo *s return nil, err } - resp, err := doDownloadReq(ctx, storeURL, cdnHeader, s, user) + resp, err := doDownloadReq(ctx, storeURL, cdnHeader, dlOpts, s, user) if err != nil { return nil, err } @@ -1645,8 +1659,8 @@ func (s *Store) DownloadStream(ctx context.Context, name string, downloadInfo *s var doDownloadReq = doDownloadReqImpl -func doDownloadReqImpl(ctx context.Context, storeURL *url.URL, cdnHeader string, s *Store, user *auth.UserState) (*http.Response, error) { - reqOptions := downloadReqOpts(storeURL, cdnHeader, nil) +func doDownloadReqImpl(ctx context.Context, storeURL *url.URL, cdnHeader string, opts *DownloadOptions, s *Store, user *auth.UserState) (*http.Response, error) { + reqOptions := downloadReqOpts(storeURL, cdnHeader, opts) return s.doRequest(ctx, httputil.NewHTTPClient(&httputil.ClientOptions{Proxy: s.proxy}), reqOptions, user) } diff --git a/store/store_test.go b/store/store_test.go index 738135c28d..8a2687e31f 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -454,7 +454,7 @@ func (s *storeTestSuite) expectedAuthorization(c *C, user *auth.UserState) strin func (s *storeTestSuite) TestDownloadStreamOK(c *C) { expectedContent := []byte("I was downloaded") - restore := store.MockDoDownloadReq(func(ctx context.Context, url *url.URL, cdnHeader string, s *store.Store, user *auth.UserState) (*http.Response, error) { + restore := store.MockDoDownloadReq(func(ctx context.Context, url *url.URL, cdnHeader string, opts *store.DownloadOptions, s *store.Store, user *auth.UserState) (*http.Response, error) { c.Check(url.String(), Equals, "http://anon-url") r := &http.Response{ Body: ioutil.NopCloser(bytes.NewReader(expectedContent)), @@ -469,7 +469,7 @@ func (s *storeTestSuite) TestDownloadStreamOK(c *C) { snap.DownloadURL = "AUTH-URL" snap.Size = int64(len(expectedContent)) - stream, err := s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, nil) + stream, err := s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, nil, nil) c.Assert(err, IsNil) buf := new(bytes.Buffer) @@ -477,6 +477,34 @@ func (s *storeTestSuite) TestDownloadStreamOK(c *C) { c.Check(buf.String(), Equals, string(expectedContent)) } +func (s *storeTestSuite) TestDownloadStreamResumeOk(c *C) { + expectedContent := []byte("I was downloaded") + restore := store.MockDoDownloadReq(func(ctx context.Context, url *url.URL, cdnHeader string, opts *store.DownloadOptions, s *store.Store, user *auth.UserState) (*http.Response, error) { + c.Check(url.String(), Equals, "http://anon-url") + r := &http.Response{ + Body: ioutil.NopCloser(bytes.NewReader(expectedContent[opts.Resume:])), + } + return r, nil + }) + defer restore() + + snap := &snap.Info{} + snap.RealName = "foo" + snap.AnonDownloadURL = "http://anon-url" + snap.DownloadURL = "AUTH-URL" + snap.Size = int64(len(expectedContent)) + + resumePoint := int64(5) + dlOpts := &store.DownloadOptions{Resume: resumePoint} + + stream, err := s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, nil, dlOpts) + c.Assert(err, IsNil) + + buf := bytes.NewBuffer(expectedContent[:resumePoint]) + buf.ReadFrom(stream) + c.Check(buf.String(), Equals, string(expectedContent)) +} + func (s *storeTestSuite) TestDownloadOK(c *C) { expectedContent := []byte("I was downloaded") diff --git a/store/storetest/storetest.go b/store/storetest/storetest.go index 0aeca31e4e..74d76ebdc7 100644 --- a/store/storetest/storetest.go +++ b/store/storetest/storetest.go @@ -61,7 +61,7 @@ func (Store) Download(context.Context, string, string, *snap.DownloadInfo, progr panic("Store.Download not expected") } -func (Store) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, user *auth.UserState) (io.ReadCloser, error) { +func (Store) DownloadStream(ctx context.Context, name string, downloadInfo *snap.DownloadInfo, user *auth.UserState, dlOpts *store.DownloadOptions) (io.ReadCloser, error) { panic("Store.DownloadStream not expected") } |
