summaryrefslogtreecommitdiff
diff options
authorMichael Vogt <mvo@ubuntu.com>2020-01-16 18:13:55 +0100
committerMichael Vogt <mvo@ubuntu.com>2020-01-16 18:31:36 +0100
commitdf459178d25f714c96cb1853e52d3fb1cdc9c9b2 (patch)
tree04961bcbcbc49e35c5e303d8d17aefe67ddb5249
parente7f2ed36b7a37d2f0d447992721c75265a7b37d7 (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.go4
-rw-r--r--daemon/api_download_test.go22
-rw-r--r--overlord/snapstate/backend.go2
-rw-r--r--store/export_test.go2
-rw-r--r--store/store.go22
-rw-r--r--store/store_test.go32
-rw-r--r--store/storetest/storetest.go2
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")
}