fix(storage): revise Reader to send XML preconditions (#4479)
This updates `Reader` to send precondition request headers to fit the XML API used for downloads.
- adds a new function `setConditionsHeaders()` that parses `Conditions` and sets the corresponding precondition headers
- revises `NewRangerReader` to call `setConditionsHeaders()`
- if an object version is specified via `generation`, include `generation` as query string parameters
- updates tests
Fixes #4470
diff --git a/storage/integration_test.go b/storage/integration_test.go
index 7ea61e3..82745f6 100644
--- a/storage/integration_test.go
+++ b/storage/integration_test.go
@@ -744,6 +744,36 @@
}
}
+func TestIntegration_ConditionalDownload(t *testing.T) {
+ ctx := context.Background()
+ client := testConfig(ctx, t)
+ defer client.Close()
+ h := testHelper{t}
+
+ o := client.Bucket(bucketName).Object("condread")
+ defer o.Delete(ctx)
+
+ wc := o.NewWriter(ctx)
+ wc.ContentType = "text/plain"
+ h.mustWrite(wc, []byte("foo"))
+
+ gen := wc.Attrs().Generation
+ metaGen := wc.Attrs().Metageneration
+
+ if _, err := o.Generation(gen + 1).NewReader(ctx); err == nil {
+ t.Fatalf("Unexpected successful download with nonexistent Generation")
+ }
+ if _, err := o.If(Conditions{MetagenerationMatch: metaGen + 1}).NewReader(ctx); err == nil {
+ t.Fatalf("Unexpected successful download with failed preconditions IfMetaGenerationMatch")
+ }
+ if _, err := o.If(Conditions{GenerationMatch: gen + 1}).NewReader(ctx); err == nil {
+ t.Fatalf("Unexpected successful download with failed preconditions IfGenerationMatch")
+ }
+ if _, err := o.If(Conditions{GenerationMatch: gen}).NewReader(ctx); err != nil {
+ t.Fatalf("Download failed: %v", err)
+ }
+}
+
func TestIntegration_Objects(t *testing.T) {
// TODO(jba): Use subtests (Go 1.7).
ctx := context.Background()
diff --git a/storage/reader.go b/storage/reader.go
index 94563c2..a992249 100644
--- a/storage/reader.go
+++ b/storage/reader.go
@@ -149,7 +149,14 @@
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, offset+length-1))
}
// We wait to assign conditions here because the generation number can change in between reopen() runs.
- req.URL.RawQuery = conditionsQuery(gen, o.conds)
+ if err := setConditionsHeaders(req.Header, o.conds); err != nil {
+ return nil, err
+ }
+ // If an object generation is specified, include generation as query string parameters.
+ if gen >= 0 {
+ req.URL.RawQuery = fmt.Sprintf("generation=%d", gen)
+ }
+
var res *http.Response
err = runWithRetry(ctx, func() error {
res, err = o.c.hc.Do(req)
@@ -324,6 +331,24 @@
return 0, false
}
+// setConditionsHeaders sets precondition request headers for downloads
+// using the XML API. It assumes that the conditions have been validated.
+func setConditionsHeaders(headers http.Header, conds *Conditions) error {
+ if conds == nil {
+ return nil
+ }
+ if conds.MetagenerationMatch != 0 {
+ headers.Set("x-goog-if-metageneration-match", fmt.Sprint(conds.MetagenerationMatch))
+ }
+ switch {
+ case conds.GenerationMatch != 0:
+ headers.Set("x-goog-if-generation-match", fmt.Sprint(conds.GenerationMatch))
+ case conds.DoesNotExist:
+ headers.Set("x-goog-if-generation-match", "0")
+ }
+ return nil
+}
+
var emptyBody = ioutil.NopCloser(strings.NewReader(""))
// Reader reads a Cloud Storage object.
diff --git a/storage/storage.go b/storage/storage.go
index c46d7c2..1b0610e 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -33,7 +33,6 @@
"reflect"
"regexp"
"sort"
- "strconv"
"strings"
"time"
"unicode/utf8"
@@ -1596,44 +1595,6 @@
return true
}
-// conditionsQuery returns the generation and conditions as a URL query
-// string suitable for URL.RawQuery. It assumes that the conditions
-// have been validated.
-func conditionsQuery(gen int64, conds *Conditions) string {
- // URL escapes are elided because integer strings are URL-safe.
- var buf []byte
-
- appendParam := func(s string, n int64) {
- if len(buf) > 0 {
- buf = append(buf, '&')
- }
- buf = append(buf, s...)
- buf = strconv.AppendInt(buf, n, 10)
- }
-
- if gen >= 0 {
- appendParam("generation=", gen)
- }
- if conds == nil {
- return string(buf)
- }
- switch {
- case conds.GenerationMatch != 0:
- appendParam("ifGenerationMatch=", conds.GenerationMatch)
- case conds.GenerationNotMatch != 0:
- appendParam("ifGenerationNotMatch=", conds.GenerationNotMatch)
- case conds.DoesNotExist:
- appendParam("ifGenerationMatch=", 0)
- }
- switch {
- case conds.MetagenerationMatch != 0:
- appendParam("ifMetagenerationMatch=", conds.MetagenerationMatch)
- case conds.MetagenerationNotMatch != 0:
- appendParam("ifMetagenerationNotMatch=", conds.MetagenerationNotMatch)
- }
- return string(buf)
-}
-
// composeSourceObj wraps a *raw.ComposeRequestSourceObjects, but adds the methods
// that modifyCall searches for by name.
type composeSourceObj struct {
diff --git a/storage/storage_test.go b/storage/storage_test.go
index 1cd4274..55d00e2 100644
--- a/storage/storage_test.go
+++ b/storage/storage_test.go
@@ -646,34 +646,6 @@
},
{
func() error {
- _, err := obj.If(Conditions{GenerationMatch: 1234}).NewReader(ctx)
- return err
- },
- "GET /buck/obj?ifGenerationMatch=1234",
- },
- {
- func() error {
- _, err := obj.If(Conditions{GenerationNotMatch: 1234}).NewReader(ctx)
- return err
- },
- "GET /buck/obj?ifGenerationNotMatch=1234",
- },
- {
- func() error {
- _, err := obj.If(Conditions{MetagenerationMatch: 1234}).NewReader(ctx)
- return err
- },
- "GET /buck/obj?ifMetagenerationMatch=1234",
- },
- {
- func() error {
- _, err := obj.If(Conditions{MetagenerationNotMatch: 1234}).NewReader(ctx)
- return err
- },
- "GET /buck/obj?ifMetagenerationNotMatch=1234",
- },
- {
- func() error {
_, err := obj.If(Conditions{MetagenerationNotMatch: 1234}).Attrs(ctx)
return err
},
@@ -734,6 +706,59 @@
}
}
+ readerTests := []struct {
+ fn func() error
+ want string
+ }{
+ {
+ func() error {
+ _, err := obj.If(Conditions{GenerationMatch: 1234}).NewReader(ctx)
+ return err
+ },
+ "x-goog-if-generation-match: 1234, x-goog-if-metageneration-match: ",
+ },
+ {
+ func() error {
+ _, err := obj.If(Conditions{MetagenerationMatch: 5}).NewReader(ctx)
+ return err
+ },
+ "x-goog-if-generation-match: , x-goog-if-metageneration-match: 5",
+ },
+ {
+ func() error {
+ _, err := obj.If(Conditions{GenerationMatch: 1234, MetagenerationMatch: 5}).NewReader(ctx)
+ return err
+ },
+ "x-goog-if-generation-match: 1234, x-goog-if-metageneration-match: 5",
+ },
+ }
+
+ for i, tt := range readerTests {
+ if err := tt.fn(); err != nil && err != io.EOF {
+ t.Error(err)
+ continue
+ }
+
+ select {
+ case r := <-gotReq:
+ generationConds := r.Header.Get("x-goog-if-generation-match")
+ metagenerationConds := r.Header.Get("x-goog-if-metageneration-match")
+ got := fmt.Sprintf(
+ "x-goog-if-generation-match: %s, x-goog-if-metageneration-match: %s",
+ generationConds,
+ metagenerationConds,
+ )
+ if got != tt.want {
+ t.Errorf("%d. RequestHeaders = %q; want %q", i, got, tt.want)
+ }
+ case <-time.After(5 * time.Second):
+ t.Fatalf("%d. timeout", i)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
// Test an error, too:
err = obj.Generation(1234).NewWriter(ctx).Close()
if err == nil || !strings.Contains(err.Error(), "NewWriter: generation not supported") {