mirror of
https://github.com/restic/restic.git
synced 2026-09-19 10:47:58 +00:00
Update dependencies
Among others, this updates minio-go, so that the new "eu-west-3" zone for AWS is supported.
This commit is contained in:
+5
-22
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"cloud.google.com/go/internal/pretty"
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"google.golang.org/api/googleapi"
|
||||
raw "google.golang.org/api/storage/v1"
|
||||
@@ -56,11 +55,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) {
|
||||
Versioning: nil, // ignore VersioningEnabled if false
|
||||
Labels: map[string]string{"label": "value"},
|
||||
}
|
||||
msg, ok, err := pretty.Diff(want, got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
if msg := testutil.Diff(got, want); msg != "" {
|
||||
t.Error(msg)
|
||||
}
|
||||
|
||||
@@ -69,11 +64,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) {
|
||||
got = attrs.toRawBucket()
|
||||
want.Versioning = &raw.BucketVersioning{Enabled: true}
|
||||
want.Billing = &raw.BucketBilling{RequesterPays: true}
|
||||
msg, ok, err = pretty.Diff(want, got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
if msg := testutil.Diff(got, want); msg != "" {
|
||||
t.Error(msg)
|
||||
}
|
||||
}
|
||||
@@ -103,11 +94,7 @@ func TestBucketAttrsToUpdateToRawBucket(t *testing.T) {
|
||||
},
|
||||
NullFields: []string{"Labels.b"},
|
||||
}
|
||||
msg, ok, err := pretty.Diff(want, got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
if msg := testutil.Diff(got, want); msg != "" {
|
||||
t.Error(msg)
|
||||
}
|
||||
|
||||
@@ -119,14 +106,10 @@ func TestBucketAttrsToUpdateToRawBucket(t *testing.T) {
|
||||
ForceSendFields: []string{"Labels"},
|
||||
NullFields: []string{"Labels.b"},
|
||||
}
|
||||
msg, ok, err = pretty.Diff(want, got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
|
||||
if msg := testutil.Diff(got, want); msg != "" {
|
||||
t.Error(msg)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCallBuilders(t *testing.T) {
|
||||
|
||||
-2
@@ -23,8 +23,6 @@ All of the methods of this package use exponential backoff to retry calls
|
||||
that fail with certain errors, as described in
|
||||
https://cloud.google.com/storage/docs/exponential-backoff.
|
||||
|
||||
Note: This package is in beta. Some backwards-incompatible changes may occur.
|
||||
|
||||
|
||||
Creating a Client
|
||||
|
||||
|
||||
+290
-40
@@ -310,11 +310,7 @@ func TestIntegration_ConditionalDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIntegration_Objects(t *testing.T) {
|
||||
// TODO(djd): there are a lot of closely-related tests here which share
|
||||
// a common setup. Once we can depend on Go 1.7 features, we should refactor
|
||||
// this test to use the sub-test feature. This will increase the readability
|
||||
// of this test, and should also reduce the time it takes to execute.
|
||||
// https://golang.org/pkg/testing/#hdr-Subtests_and_Sub_benchmarks
|
||||
// TODO(jba): Use subtests (Go 1.7).
|
||||
ctx := context.Background()
|
||||
client, bucket := testConfig(ctx, t)
|
||||
defer client.Close()
|
||||
@@ -386,33 +382,6 @@ func TestIntegration_Objects(t *testing.T) {
|
||||
if err := rc.Close(); err != nil {
|
||||
t.Errorf("%v Close: %v", obj, err)
|
||||
}
|
||||
|
||||
// Test SignedURL
|
||||
opts := &SignedURLOptions{
|
||||
GoogleAccessID: "xxx@clientid",
|
||||
PrivateKey: dummyKey("rsa"),
|
||||
Method: "GET",
|
||||
MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
|
||||
Expires: time.Date(2020, time.October, 2, 10, 0, 0, 0, time.UTC),
|
||||
ContentType: "application/json",
|
||||
Headers: []string{"x-header1", "x-header2"},
|
||||
}
|
||||
u, err := SignedURL(bucket, obj, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("SignedURL(%q, %q) errored with %v", bucket, obj, err)
|
||||
}
|
||||
res, err := client.hc.Get(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Can't get URL %q: %v", u, err)
|
||||
}
|
||||
slurp, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Can't ReadAll signed object %v, errored with %v", obj, err)
|
||||
}
|
||||
if got, want := slurp, contents[obj]; !bytes.Equal(got, want) {
|
||||
t.Errorf("Contents (%v) = %q; want %q", obj, got, want)
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
obj := objects[0]
|
||||
@@ -763,6 +732,115 @@ func testObjectIterator(t *testing.T, bkt *BucketHandle, objects []string) {
|
||||
// TODO(jba): test query.Delimiter != ""
|
||||
}
|
||||
|
||||
func TestIntegration_SignedURL(t *testing.T) {
|
||||
// To test SignedURL, we need a real user email and private key. Extract them
|
||||
// from the JSON key file.
|
||||
jwtConf, err := testutil.JWTConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if jwtConf == nil {
|
||||
t.Skip("JSON key file is not present")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, bucket := testConfig(ctx, t)
|
||||
defer client.Close()
|
||||
|
||||
bkt := client.Bucket(bucket)
|
||||
obj := "signedURL"
|
||||
contents := []byte("This is a test of SignedURL.\n")
|
||||
md5 := "Jyxvgwm9n2MsrGTMPbMeYA==" // base64-encoded MD5 of contents
|
||||
if err := writeObject(ctx, bkt.Object(obj), "text/plain", contents); err != nil {
|
||||
t.Fatalf("writing: %v", err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
opts SignedURLOptions
|
||||
headers map[string][]string
|
||||
fail bool
|
||||
}{
|
||||
{
|
||||
desc: "basic",
|
||||
},
|
||||
{
|
||||
desc: "MD5 sent and matches",
|
||||
opts: SignedURLOptions{MD5: md5},
|
||||
headers: map[string][]string{"Content-MD5": {md5}},
|
||||
},
|
||||
{
|
||||
desc: "MD5 not sent",
|
||||
opts: SignedURLOptions{MD5: md5},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
desc: "Content-Type sent and matches",
|
||||
opts: SignedURLOptions{ContentType: "text/plain"},
|
||||
headers: map[string][]string{"Content-Type": {"text/plain"}},
|
||||
},
|
||||
{
|
||||
desc: "Content-Type sent but does not match",
|
||||
opts: SignedURLOptions{ContentType: "text/plain"},
|
||||
headers: map[string][]string{"Content-Type": {"application/json"}},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
desc: "Canonical headers sent and match",
|
||||
opts: SignedURLOptions{Headers: []string{
|
||||
" X-Goog-Foo: Bar baz ",
|
||||
"X-Goog-Novalue", // ignored: no value
|
||||
"X-Google-Foo", // ignored: wrong prefix
|
||||
}},
|
||||
headers: map[string][]string{"X-Goog-foo": {"Bar baz "}},
|
||||
},
|
||||
{
|
||||
desc: "Canonical headers sent but don't match",
|
||||
opts: SignedURLOptions{Headers: []string{" X-Goog-Foo: Bar baz"}},
|
||||
headers: map[string][]string{"X-Goog-Foo": {"bar baz"}},
|
||||
fail: true,
|
||||
},
|
||||
} {
|
||||
opts := test.opts
|
||||
opts.GoogleAccessID = jwtConf.Email
|
||||
opts.PrivateKey = jwtConf.PrivateKey
|
||||
opts.Method = "GET"
|
||||
opts.Expires = time.Now().Add(time.Hour)
|
||||
u, err := SignedURL(bucket, obj, &opts)
|
||||
if err != nil {
|
||||
t.Errorf("%s: SignedURL: %v", test.desc, err)
|
||||
continue
|
||||
}
|
||||
got, err := getURL(u, test.headers)
|
||||
if err != nil && !test.fail {
|
||||
t.Errorf("%s: getURL %q: %v", test.desc, u, err)
|
||||
} else if err == nil && !bytes.Equal(got, contents) {
|
||||
t.Errorf("%s: got %q, want %q", test.desc, got, contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make a GET request to a URL using an unauthenticated client, and return its contents.
|
||||
func getURL(url string, headers map[string][]string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = headers
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
bytes, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("code=%d, body=%s", res.StatusCode, string(bytes))
|
||||
}
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func TestIntegration_ACL(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client, bucket := testConfig(ctx, t)
|
||||
@@ -1012,7 +1090,7 @@ func TestIntegration_Encryption(t *testing.T) {
|
||||
}
|
||||
gotContents := string(got)
|
||||
if gotContents != wantContents {
|
||||
t.Errorf("%s: got %q, want %q", gotContents, wantContents)
|
||||
t.Errorf("%s: got %q, want %q", msg, gotContents, wantContents)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1441,12 +1519,11 @@ func TestIntegration_RequesterPays(t *testing.T) {
|
||||
// user: not an Owner on the containing project
|
||||
// userProject: not the containing one, but user has Editor role on it
|
||||
// result: success, by the standard requester-pays rule
|
||||
// TODO(jba): enable when the service is fixed.
|
||||
// if err := f(ob.UserProject(otherProjID)); err != nil {
|
||||
// t.Errorf("%s: got %v, want nil\n"+
|
||||
// "confirm that %s is an Editor on %s",
|
||||
// msg, err, otherUser, otherProjID)
|
||||
// }
|
||||
if err := f(ob.UserProject(otherProjID)); err != nil {
|
||||
t.Errorf("%s: got %v, want nil\n"+
|
||||
"confirm that %s is an Editor on %s and that that project has billing enabled",
|
||||
msg, err, otherUser, otherProjID)
|
||||
}
|
||||
// user: not an Owner on the containing project
|
||||
// userProject: the containing one, on which the user does NOT have Editor permission.
|
||||
// result: failure
|
||||
@@ -1469,7 +1546,7 @@ func TestIntegration_RequesterPays(t *testing.T) {
|
||||
})
|
||||
if attrs != nil {
|
||||
if got, want := attrs.RequesterPays, true; got != want {
|
||||
t.Fatalf("attr.RequesterPays = %b, want %b", got, want)
|
||||
t.Fatalf("attr.RequesterPays = %t, want %t", got, want)
|
||||
}
|
||||
}
|
||||
// Object operations.
|
||||
@@ -1689,6 +1766,179 @@ func TestIntegration_Public(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_ReadCRC(t *testing.T) {
|
||||
// Test that the checksum is handled correctly when reading files.
|
||||
// For gzipped files, see https://github.com/GoogleCloudPlatform/google-cloud-dotnet/issues/1641.
|
||||
if testing.Short() {
|
||||
t.Skip("Integration tests skipped in short mode")
|
||||
}
|
||||
const (
|
||||
// This is an uncompressed file.
|
||||
// See https://cloud.google.com/storage/docs/public-datasets/landsat
|
||||
uncompressedBucket = "gcp-public-data-landsat"
|
||||
uncompressedObject = "LC08/PRE/044/034/LC80440342016259LGN00/LC80440342016259LGN00_MTL.txt"
|
||||
|
||||
gzippedBucket = "storage-library-test-bucket"
|
||||
gzippedObject = "gzipped-text.txt"
|
||||
gzippedContents = "hello world" // uncompressed contents of the file
|
||||
)
|
||||
ctx := context.Background()
|
||||
client, err := NewClient(ctx, option.WithoutAuthentication())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
obj *ObjectHandle
|
||||
offset, length int64
|
||||
readCompressed bool // don't decompress a gzipped file
|
||||
|
||||
wantErr bool
|
||||
wantCheck bool // Should Reader try to check the CRC?
|
||||
wantChecked bool // Did Reader actually check the CRC?
|
||||
}{
|
||||
{
|
||||
desc: "uncompressed, entire file",
|
||||
obj: client.Bucket(uncompressedBucket).Object(uncompressedObject),
|
||||
offset: 0,
|
||||
length: -1,
|
||||
readCompressed: false,
|
||||
wantCheck: true,
|
||||
wantChecked: true,
|
||||
},
|
||||
{
|
||||
desc: "uncompressed, entire file, don't decompress",
|
||||
obj: client.Bucket(uncompressedBucket).Object(uncompressedObject),
|
||||
offset: 0,
|
||||
length: -1,
|
||||
readCompressed: true,
|
||||
wantCheck: true,
|
||||
wantChecked: true,
|
||||
},
|
||||
{
|
||||
desc: "uncompressed, suffix",
|
||||
obj: client.Bucket(uncompressedBucket).Object(uncompressedObject),
|
||||
offset: 1,
|
||||
length: -1,
|
||||
readCompressed: false,
|
||||
wantCheck: false,
|
||||
wantChecked: false,
|
||||
},
|
||||
{
|
||||
desc: "uncompressed, prefix",
|
||||
obj: client.Bucket(uncompressedBucket).Object(uncompressedObject),
|
||||
offset: 0,
|
||||
length: 18,
|
||||
readCompressed: false,
|
||||
wantCheck: false,
|
||||
wantChecked: false,
|
||||
},
|
||||
{
|
||||
// When a gzipped file is unzipped by GCS, we can't verify the checksum
|
||||
// because it was computed against the zipped contents. There is no
|
||||
// header that indicates that a gzipped file is being served unzipped.
|
||||
// But our CRC check only happens if there is a Content-Length header,
|
||||
// and that header is absent for this read.
|
||||
desc: "compressed, entire file, server unzips",
|
||||
obj: client.Bucket(gzippedBucket).Object(gzippedObject),
|
||||
offset: 0,
|
||||
length: -1,
|
||||
readCompressed: false,
|
||||
wantCheck: true,
|
||||
wantChecked: false,
|
||||
},
|
||||
{
|
||||
// When we read a gzipped file uncompressed, it's like reading a regular file:
|
||||
// the served content and the CRC match.
|
||||
desc: "compressed, entire file, read compressed",
|
||||
obj: client.Bucket(gzippedBucket).Object(gzippedObject),
|
||||
offset: 0,
|
||||
length: -1,
|
||||
readCompressed: true,
|
||||
wantCheck: true,
|
||||
wantChecked: true,
|
||||
},
|
||||
{
|
||||
desc: "compressed, partial, server unzips",
|
||||
obj: client.Bucket(gzippedBucket).Object(gzippedObject),
|
||||
offset: 1,
|
||||
length: 8,
|
||||
readCompressed: false,
|
||||
wantErr: true, // GCS can't serve part of a gzipped object
|
||||
wantCheck: false,
|
||||
wantChecked: false,
|
||||
},
|
||||
{
|
||||
desc: "compressed, partial, read compressed",
|
||||
obj: client.Bucket(gzippedBucket).Object(gzippedObject),
|
||||
offset: 1,
|
||||
length: 8,
|
||||
readCompressed: true,
|
||||
wantCheck: false,
|
||||
wantChecked: false,
|
||||
},
|
||||
} {
|
||||
obj := test.obj.ReadCompressed(test.readCompressed)
|
||||
r, err := obj.NewRangeReader(ctx, test.offset, test.length)
|
||||
if err != nil {
|
||||
if test.wantErr {
|
||||
continue
|
||||
}
|
||||
t.Fatalf("%s: %v", test.desc, err)
|
||||
}
|
||||
if got, want := r.checkCRC, test.wantCheck; got != want {
|
||||
t.Errorf("%s, checkCRC: got %t, want %t", test.desc, got, want)
|
||||
}
|
||||
_, err = ioutil.ReadAll(r)
|
||||
_ = r.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", test.desc, err)
|
||||
}
|
||||
if got, want := r.checkedCRC, test.wantChecked; got != want {
|
||||
t.Errorf("%s, checkedCRC: got %t, want %t", test.desc, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_CancelWrite(t *testing.T) {
|
||||
// Verify that canceling the writer's context immediately stops uploading an object.
|
||||
if testing.Short() {
|
||||
t.Skip("Integration tests skipped in short mode")
|
||||
}
|
||||
ctx := context.Background()
|
||||
client, bucket := testConfig(ctx, t)
|
||||
defer client.Close()
|
||||
bkt := client.Bucket(bucket)
|
||||
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
obj := bkt.Object("cancel-write")
|
||||
w := obj.NewWriter(cctx)
|
||||
w.ChunkSize = googleapi.MinUploadChunkSize
|
||||
buf := make([]byte, w.ChunkSize)
|
||||
// Write the first chunk. This is read in its entirety before sending the request
|
||||
// (see google.golang.org/api/gensupport.PrepareUpload), so we expect it to return
|
||||
// without error.
|
||||
_, err := w.Write(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Now cancel the context.
|
||||
cancel()
|
||||
// The next Write should return context.Canceled.
|
||||
_, err = w.Write(buf)
|
||||
if err != context.Canceled {
|
||||
t.Fatalf("got %v, wanted context.Canceled", err)
|
||||
}
|
||||
// The Close should too.
|
||||
err = w.Close()
|
||||
if err != context.Canceled {
|
||||
t.Fatalf("got %v, wanted context.Canceled", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeObject(ctx context.Context, obj *ObjectHandle, contentType string, contents []byte) error {
|
||||
w := obj.NewWriter(ctx)
|
||||
w.ContentType = contentType
|
||||
|
||||
+24
-10
@@ -24,14 +24,20 @@ var crc32cTable = crc32.MakeTable(crc32.Castagnoli)
|
||||
|
||||
// Reader reads a Cloud Storage object.
|
||||
// It implements io.Reader.
|
||||
//
|
||||
// Typically, a Reader computes the CRC of the downloaded content and compares it to
|
||||
// the stored CRC, returning an error from Read if there is a mismatch. This integrity check
|
||||
// is skipped if transcoding occurs. See https://cloud.google.com/storage/docs/transcoding.
|
||||
type Reader struct {
|
||||
body io.ReadCloser
|
||||
remain, size int64
|
||||
contentType string
|
||||
cacheControl string
|
||||
checkCRC bool // should we check the CRC?
|
||||
wantCRC uint32 // the CRC32c value the server sent in the header
|
||||
gotCRC uint32 // running crc
|
||||
body io.ReadCloser
|
||||
remain, size int64
|
||||
contentType string
|
||||
contentEncoding string
|
||||
cacheControl string
|
||||
checkCRC bool // should we check the CRC?
|
||||
wantCRC uint32 // the CRC32c value the server sent in the header
|
||||
gotCRC uint32 // running crc
|
||||
checkedCRC bool // did we check the CRC? (For tests.)
|
||||
}
|
||||
|
||||
// Close closes the Reader. It must be called when done reading.
|
||||
@@ -49,9 +55,12 @@ func (r *Reader) Read(p []byte) (int, error) {
|
||||
// Check CRC here. It would be natural to check it in Close, but
|
||||
// everybody defers Close on the assumption that it doesn't return
|
||||
// anything worth looking at.
|
||||
if r.remain == 0 && r.gotCRC != r.wantCRC {
|
||||
return n, fmt.Errorf("storage: bad CRC on read: got %d, want %d",
|
||||
r.gotCRC, r.wantCRC)
|
||||
if r.remain == 0 { // Only check if we have Content-Length.
|
||||
r.checkedCRC = true
|
||||
if r.gotCRC != r.wantCRC {
|
||||
return n, fmt.Errorf("storage: bad CRC on read: got %d, want %d",
|
||||
r.gotCRC, r.wantCRC)
|
||||
}
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
@@ -74,6 +83,11 @@ func (r *Reader) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
// ContentEncoding returns the content encoding of the object.
|
||||
func (r *Reader) ContentEncoding() string {
|
||||
return r.contentEncoding
|
||||
}
|
||||
|
||||
// CacheControl returns the cache control of the object.
|
||||
func (r *Reader) CacheControl() string {
|
||||
return r.cacheControl
|
||||
|
||||
+84
-15
@@ -30,6 +30,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -182,6 +184,60 @@ type SignedURLOptions struct {
|
||||
MD5 string
|
||||
}
|
||||
|
||||
var (
|
||||
canonicalHeaderRegexp = regexp.MustCompile(`(?i)^(x-goog-[^:]+):(.*)?$`)
|
||||
excludedCanonicalHeaders = map[string]bool{
|
||||
"x-goog-encryption-key": true,
|
||||
"x-goog-encryption-key-sha256": true,
|
||||
}
|
||||
)
|
||||
|
||||
// sanitizeHeaders applies the specifications for canonical extension headers at
|
||||
// https://cloud.google.com/storage/docs/access-control/signed-urls#about-canonical-extension-headers.
|
||||
func sanitizeHeaders(hdrs []string) []string {
|
||||
headerMap := map[string][]string{}
|
||||
for _, hdr := range hdrs {
|
||||
// No leading or trailing whitespaces.
|
||||
sanitizedHeader := strings.TrimSpace(hdr)
|
||||
|
||||
// Only keep canonical headers, discard any others.
|
||||
headerMatches := canonicalHeaderRegexp.FindStringSubmatch(sanitizedHeader)
|
||||
if len(headerMatches) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
header := strings.ToLower(strings.TrimSpace(headerMatches[1]))
|
||||
if excludedCanonicalHeaders[headerMatches[1]] {
|
||||
// Do not keep any deliberately excluded canonical headers when signing.
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(headerMatches[2])
|
||||
if len(value) > 0 {
|
||||
// Remove duplicate headers by appending the values of duplicates
|
||||
// in their order of appearance.
|
||||
headerMap[header] = append(headerMap[header], value)
|
||||
}
|
||||
}
|
||||
|
||||
var sanitizedHeaders []string
|
||||
for header, values := range headerMap {
|
||||
// There should be no spaces around the colon separating the
|
||||
// header name from the header value or around the values
|
||||
// themselves. The values should be separated by commas.
|
||||
// NOTE: The semantics for headers without a value are not clear.
|
||||
// However from specifications these should be edge-cases
|
||||
// anyway and we should assume that there will be no
|
||||
// canonical headers using empty values. Any such headers
|
||||
// are discarded at the regexp stage above.
|
||||
sanitizedHeaders = append(
|
||||
sanitizedHeaders,
|
||||
fmt.Sprintf("%s:%s", header, strings.Join(values, ",")),
|
||||
)
|
||||
}
|
||||
sort.Strings(sanitizedHeaders)
|
||||
return sanitizedHeaders
|
||||
}
|
||||
|
||||
// SignedURL returns a URL for the specified object. Signed URLs allow
|
||||
// the users access to a restricted resource for a limited time without having a
|
||||
// Google account or signing in. For more information about the signed
|
||||
@@ -208,6 +264,7 @@ func SignedURL(bucket, name string, opts *SignedURLOptions) (string, error) {
|
||||
return "", errors.New("storage: invalid MD5 checksum")
|
||||
}
|
||||
}
|
||||
opts.Headers = sanitizeHeaders(opts.Headers)
|
||||
|
||||
signBytes := opts.SignBytes
|
||||
if opts.PrivateKey != nil {
|
||||
@@ -258,14 +315,15 @@ func SignedURL(bucket, name string, opts *SignedURLOptions) (string, error) {
|
||||
// ObjectHandle provides operations on an object in a Google Cloud Storage bucket.
|
||||
// Use BucketHandle.Object to get a handle.
|
||||
type ObjectHandle struct {
|
||||
c *Client
|
||||
bucket string
|
||||
object string
|
||||
acl ACLHandle
|
||||
gen int64 // a negative value indicates latest
|
||||
conds *Conditions
|
||||
encryptionKey []byte // AES-256 key
|
||||
userProject string // for requester-pays buckets
|
||||
c *Client
|
||||
bucket string
|
||||
object string
|
||||
acl ACLHandle
|
||||
gen int64 // a negative value indicates latest
|
||||
conds *Conditions
|
||||
encryptionKey []byte // AES-256 key
|
||||
userProject string // for requester-pays buckets
|
||||
readCompressed bool // Accept-Encoding: gzip
|
||||
}
|
||||
|
||||
// ACL provides access to the object's access control list.
|
||||
@@ -467,6 +525,13 @@ func (o *ObjectHandle) Delete(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadCompressed when true causes the read to happen without decompressing.
|
||||
func (o *ObjectHandle) ReadCompressed(compressed bool) *ObjectHandle {
|
||||
o2 := *o
|
||||
o2.readCompressed = compressed
|
||||
return &o2
|
||||
}
|
||||
|
||||
// NewReader creates a new Reader to read the contents of the
|
||||
// object.
|
||||
// ErrObjectNotExist will be returned if the object is not found.
|
||||
@@ -514,6 +579,9 @@ func (o *ObjectHandle) NewRangeReader(ctx context.Context, offset, length int64)
|
||||
if o.userProject != "" {
|
||||
req.Header.Set("X-Goog-User-Project", o.userProject)
|
||||
}
|
||||
if o.readCompressed {
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
if err := setEncryptionHeaders(req.Header, o.encryptionKey, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -576,13 +644,14 @@ func (o *ObjectHandle) NewRangeReader(ctx context.Context, offset, length int64)
|
||||
crc, checkCRC = parseCRC32c(res)
|
||||
}
|
||||
return &Reader{
|
||||
body: body,
|
||||
size: size,
|
||||
remain: remain,
|
||||
contentType: res.Header.Get("Content-Type"),
|
||||
cacheControl: res.Header.Get("Cache-Control"),
|
||||
wantCRC: crc,
|
||||
checkCRC: checkCRC,
|
||||
body: body,
|
||||
size: size,
|
||||
remain: remain,
|
||||
contentType: res.Header.Get("Content-Type"),
|
||||
contentEncoding: res.Header.Get("Content-Encoding"),
|
||||
cacheControl: res.Header.Get("Cache-Control"),
|
||||
wantCRC: crc,
|
||||
checkCRC: checkCRC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+66
-19
@@ -40,6 +40,52 @@ import (
|
||||
raw "google.golang.org/api/storage/v1"
|
||||
)
|
||||
|
||||
func TestHeaderSanitization(t *testing.T) {
|
||||
t.Parallel()
|
||||
var tests = []struct {
|
||||
desc string
|
||||
in []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
desc: "already sanitized headers should not be modified",
|
||||
in: []string{"x-goog-header1:true", "x-goog-header2:0"},
|
||||
want: []string{"x-goog-header1:true", "x-goog-header2:0"},
|
||||
},
|
||||
{
|
||||
desc: "sanitized headers should be sorted",
|
||||
in: []string{"x-goog-header2:0", "x-goog-header1:true"},
|
||||
want: []string{"x-goog-header1:true", "x-goog-header2:0"},
|
||||
},
|
||||
{
|
||||
desc: "non-canonical headers should be removed",
|
||||
in: []string{"x-goog-header1:true", "x-goog-no-value", "non-canonical-header:not-of-use"},
|
||||
want: []string{"x-goog-header1:true"},
|
||||
},
|
||||
{
|
||||
desc: "excluded canonical headers should be removed",
|
||||
in: []string{"x-goog-header1:true", "x-goog-encryption-key:my_key", "x-goog-encryption-key-sha256:my_sha256"},
|
||||
want: []string{"x-goog-header1:true"},
|
||||
},
|
||||
{
|
||||
desc: "dirty headers should be formatted correctly",
|
||||
in: []string{" x-goog-header1 : \textra-spaces ", "X-Goog-Header2:CamelCaseValue"},
|
||||
want: []string{"x-goog-header1:extra-spaces", "x-goog-header2:CamelCaseValue"},
|
||||
},
|
||||
{
|
||||
desc: "duplicate headers should be merged",
|
||||
in: []string{"x-goog-header1:value1", "X-Goog-Header1:value2"},
|
||||
want: []string{"x-goog-header1:value1,value2"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := sanitizeHeaders(test.in)
|
||||
if !testutil.Equal(got, test.want) {
|
||||
t.Errorf("%s: got %v, want %v", test.desc, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
|
||||
@@ -50,20 +96,20 @@ func TestSignedURL(t *testing.T) {
|
||||
MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
|
||||
Expires: expires,
|
||||
ContentType: "application/json",
|
||||
Headers: []string{"x-header1", "x-header2"},
|
||||
Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
want := "https://storage.googleapis.com/bucket-name/object-name?" +
|
||||
"Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
|
||||
"ZMw18bZVhySNYAMEX87RMyuZCUMtGLVi%2B2zU2ByiQ0Rxgij%2BhFZ5LsT" +
|
||||
"5ZPIH5h3QXB%2BiSb1URJnZo3aF0exVP%2FYR1hpg2e65w9HHt7yYjIqcg" +
|
||||
"%2FfAOIyxriFtgRYk3oAv%2FFLF62fI8iF%2BCp0fWSm%2FHggz22blVnQz" +
|
||||
"EtSP%2BuRhFle4172L%2B710sfMDtyQLKTz6W4TmRjC9ymTi8mVj95dZgyF" +
|
||||
"RXbibTdtw0JzndE0Ig4c6pU4xDPPiyaziUSVDMIpzZDJH1GYOGHxbFasba4" +
|
||||
"1rRoWWkdBnsMtHm2ck%2FsFD2leL6u8q0OpVAc4ZdxseucL4OpCy%2BCLhQ" +
|
||||
"JFQT5bqSljP0g%3D%3D"
|
||||
"RfsHlPtbB2JUYjzCgNr2Mi%2BjggdEuL1V7E6N9o6aaqwVLBDuTv3I0%2B9" +
|
||||
"x94E6rmmr%2FVgnmZigkIUxX%2Blfl7LgKf30uPGLt0mjKGH2p7r9ey1ONJ" +
|
||||
"%2BhVec23FnTRcSgopglvHPuCMWU2oNJE%2F1y8EwWE27baHrG1RhRHbLVF" +
|
||||
"bPpLZ9xTRFK20pluIkfHV00JGljB1imqQHXM%2B2XPWqBngLr%2FwqxLN7i" +
|
||||
"FcUiqR8xQEOHF%2F2e7fbkTHPNq4TazaLZ8X0eZ3eFdJ55A5QmNi8atlN4W" +
|
||||
"5q7Hvs0jcxElG3yqIbx439A995BkspLiAcA%2Fo4%2BxAwEMkGLICdbvakq" +
|
||||
"3eEprNCojw%3D%3D"
|
||||
if url != want {
|
||||
t.Fatalf("Unexpected signed URL; found %v", url)
|
||||
}
|
||||
@@ -79,16 +125,17 @@ func TestSignedURL_PEMPrivateKey(t *testing.T) {
|
||||
MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
|
||||
Expires: expires,
|
||||
ContentType: "application/json",
|
||||
Headers: []string{"x-header1", "x-header2"},
|
||||
Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
want := "https://storage.googleapis.com/bucket-name/object-name?" +
|
||||
"Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
|
||||
"gHlh63sOxJnNj22X%2B%2F4kwOSNMeqwXWr4udEfrzJPQcq1xzxA8ovMM5SOrOc%" +
|
||||
"2FuE%2Ftc9%2Bq7a42CDBwZff1PsvuJMBDaPbluU257h%2Bvxx8lHMnb%2Bg1wD1" +
|
||||
"99FiCE014MRH9TlIg%2FdXRkErosVWTy4GqAgZemmKHo0HwDGT6IovB9mdg%3D"
|
||||
"TiyKD%2FgGb6Kh0kkb2iF%2FfF%2BnTx7L0J4YiZua8AcTmnidutePEGIU5" +
|
||||
"NULYlrGl6l52gz4zqFb3VFfIRTcPXMdXnnFdMCDhz2QuJBUpsU1Ai9zlyTQ" +
|
||||
"dkb6ShG03xz9%2BEXWAUQO4GBybJw%2FULASuv37xA00SwLdkqj8YdyS5II" +
|
||||
"1lro%3D"
|
||||
if url != want {
|
||||
t.Fatalf("Unexpected signed URL; found %v", url)
|
||||
}
|
||||
@@ -106,7 +153,7 @@ func TestSignedURL_SignBytes(t *testing.T) {
|
||||
MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
|
||||
Expires: expires,
|
||||
ContentType: "application/json",
|
||||
Headers: []string{"x-header1", "x-header2"},
|
||||
Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -129,16 +176,16 @@ func TestSignedURL_URLUnsafeObjectName(t *testing.T) {
|
||||
MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
|
||||
Expires: expires,
|
||||
ContentType: "application/json",
|
||||
Headers: []string{"x-header1", "x-header2"},
|
||||
Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
want := "https://storage.googleapis.com/bucket-name/object%20name%E7%95%8C?" +
|
||||
"Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
|
||||
"LSxs1YwXNKOa7mQv1ZAI2ao0Fuv6yXLLU7%2BQ97z2B7hYZ57OiFwQ72EdGXSiIM" +
|
||||
"JwLisEKkwoSlYCMm3uuTdgJtXXVi7SYXMfdeKaonyQwMv531KETCBTSewt8CW%2B" +
|
||||
"FaUJ5SEYG44SeJCiqeIr3GF7t90UNWs6TdFXDaKShpQzBGg%3D"
|
||||
"Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=bxVH1%2Bl%2" +
|
||||
"BSxpnj3XuqKz6mOFk6M94Y%2B4w85J6FCmJan%2FNhGSpndP6fAw1uLHlOn%2F8xUaY%2F" +
|
||||
"SfZ5GzcQ%2BbxOL1WA37yIwZ7xgLYlO%2ByAi3GuqMUmHZiNCai28emODXQ8RtWHvgv6dE" +
|
||||
"SQ%2F0KpDMIWW7rYCaUa63UkUyeSQsKhrVqkIA%3D"
|
||||
if url != want {
|
||||
t.Fatalf("Unexpected signed URL; found %v", url)
|
||||
}
|
||||
@@ -325,7 +372,7 @@ func TestObjectNames(t *testing.T) {
|
||||
MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
|
||||
Expires: time.Date(2002, time.October, 2, 10, 0, 0, 0, time.UTC),
|
||||
ContentType: "application/json",
|
||||
Headers: []string{"x-header1", "x-header2"},
|
||||
Headers: []string{"x-goog-header1", "x-goog-header2"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
+2
@@ -187,6 +187,8 @@ func (w *Writer) Close() error {
|
||||
|
||||
// CloseWithError aborts the write operation with the provided error.
|
||||
// CloseWithError always returns nil.
|
||||
//
|
||||
// Deprecated: cancel the context passed to NewWriter instead.
|
||||
func (w *Writer) CloseWithError(err error) error {
|
||||
if !w.opened {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user