1// Copyright 2018 The Go Cloud Development Kit Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package gcsblob provides a blob implementation that uses GCS. Use OpenBucket 16// to construct a *blob.Bucket. 17// 18// URLs 19// 20// For blob.OpenBucket, gcsblob registers for the scheme "gs". 21// The default URL opener will set up a connection using default credentials 22// from the environment, as described in 23// https://cloud.google.com/docs/authentication/production. 24// Some environments, such as GCE, come without a private key. In such cases 25// the IAM Credentials API will be configured for use in Options.MakeSignBytes, 26// which will introduce latency to any and all calls to bucket.SignedURL 27// that you can avoid by installing a service account credentials file or 28// obtaining and configuring a private key: 29// https://cloud.google.com/iam/docs/creating-managing-service-account-keys 30// 31// To customize the URL opener, or for more details on the URL format, 32// see URLOpener. 33// See https://gocloud.dev/concepts/urls/ for background information. 34// 35// Escaping 36// 37// Go CDK supports all UTF-8 strings; to make this work with services lacking 38// full UTF-8 support, strings must be escaped (during writes) and unescaped 39// (during reads). The following escapes are performed for gcsblob: 40// - Blob keys: ASCII characters 10 and 13 are escaped to "__0x<hex>__". 41// Additionally, the "/" in "../" is escaped in the same way. 42// 43// As 44// 45// gcsblob exposes the following types for As: 46// - Bucket: *storage.Client 47// - Error: *googleapi.Error 48// - ListObject: storage.ObjectAttrs 49// - ListOptions.BeforeList: *storage.Query 50// - Reader: *storage.Reader 51// - ReaderOptions.BeforeRead: **storage.ObjectHandle, *storage.Reader (if accessing both, must be in that order) 52// - Attributes: storage.ObjectAttrs 53// - CopyOptions.BeforeCopy: *CopyObjectHandles, *storage.Copier (if accessing both, must be in that order) 54// - WriterOptions.BeforeWrite: **storage.ObjectHandle, *storage.Writer (if accessing both, must be in that order) 55// - SignedURLOptions.BeforeSign: *storage.SignedURLOptions 56package gcsblob // import "gocloud.dev/blob/gcsblob" 57 58import ( 59 "context" 60 "encoding/json" 61 "errors" 62 "fmt" 63 "io" 64 "io/ioutil" 65 "net/http" 66 "net/url" 67 "os" 68 "sort" 69 "strings" 70 "sync" 71 "time" 72 73 "cloud.google.com/go/compute/metadata" 74 "cloud.google.com/go/storage" 75 "github.com/google/wire" 76 "golang.org/x/oauth2/google" 77 "google.golang.org/api/googleapi" 78 "google.golang.org/api/iterator" 79 "google.golang.org/api/option" 80 81 "gocloud.dev/blob" 82 "gocloud.dev/blob/driver" 83 "gocloud.dev/gcerrors" 84 "gocloud.dev/gcp" 85 "gocloud.dev/internal/escape" 86 "gocloud.dev/internal/gcerr" 87 "gocloud.dev/internal/useragent" 88) 89 90const defaultPageSize = 1000 91 92func init() { 93 blob.DefaultURLMux().RegisterBucket(Scheme, new(lazyCredsOpener)) 94} 95 96// Set holds Wire providers for this package. 97var Set = wire.NewSet( 98 wire.Struct(new(URLOpener), "Client"), 99) 100 101// readDefaultCredentials gets the field values from the supplied JSON data. 102// For its possible formats please see 103// https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-go 104// 105// Use "golang.org/x/oauth2/google".DefaultCredentials.JSON to get 106// the contents of the preferred credential file. 107// 108// Returns null-values for fields that have not been obtained. 109func readDefaultCredentials(credFileAsJSON []byte) (AccessID string, PrivateKey []byte) { 110 // For example, a credentials file as generated for service accounts through the web console. 111 var contentVariantA struct { 112 ClientEmail string `json:"client_email"` 113 PrivateKey string `json:"private_key"` 114 } 115 if err := json.Unmarshal(credFileAsJSON, &contentVariantA); err == nil { 116 AccessID = contentVariantA.ClientEmail 117 PrivateKey = []byte(contentVariantA.PrivateKey) 118 } 119 if AccessID != "" { 120 return 121 } 122 123 // If obtained through the REST API. 124 var contentVariantB struct { 125 Name string `json:"name"` 126 PrivateKeyData string `json:"privateKeyData"` 127 } 128 if err := json.Unmarshal(credFileAsJSON, &contentVariantB); err == nil { 129 nextFieldIsAccessID := false 130 for _, s := range strings.Split(contentVariantB.Name, "/") { 131 if nextFieldIsAccessID { 132 AccessID = s 133 break 134 } 135 nextFieldIsAccessID = s == "serviceAccounts" 136 } 137 PrivateKey = []byte(contentVariantB.PrivateKeyData) 138 } 139 140 return 141} 142 143// lazyCredsOpener obtains Application Default Credentials on the first call 144// to OpenBucketURL. 145type lazyCredsOpener struct { 146 init sync.Once 147 opener *URLOpener 148 err error 149} 150 151func (o *lazyCredsOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) { 152 o.init.Do(func() { 153 var opts Options 154 var creds *google.Credentials 155 if os.Getenv("STORAGE_EMULATOR_HOST") != "" { 156 creds, _ = google.CredentialsFromJSON(ctx, []byte(`{"type": "service_account", "project_id": "my-project-id"}`)) 157 } else { 158 var err error 159 creds, err = gcp.DefaultCredentials(ctx) 160 if err != nil { 161 o.err = err 162 return 163 } 164 165 // Populate default values from credentials files, where available. 166 opts.GoogleAccessID, opts.PrivateKey = readDefaultCredentials(creds.JSON) 167 168 // … else, on GCE, at least get the instance's main service account. 169 if opts.GoogleAccessID == "" && metadata.OnGCE() { 170 mc := metadata.NewClient(nil) 171 opts.GoogleAccessID, _ = mc.Email("") 172 } 173 } 174 175 // Provide a default factory for SignBytes for environments without a private key. 176 if len(opts.PrivateKey) <= 0 && opts.GoogleAccessID != "" { 177 iam := new(credentialsClient) 178 // We cannot hold onto the first context: it might've been cancelled already. 179 ctx := context.Background() 180 opts.MakeSignBytes = iam.CreateMakeSignBytesWith(ctx, opts.GoogleAccessID) 181 } 182 183 client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), creds.TokenSource) 184 if err != nil { 185 o.err = err 186 return 187 } 188 o.opener = &URLOpener{Client: client, Options: opts} 189 }) 190 if o.err != nil { 191 return nil, fmt.Errorf("open bucket %v: %v", u, o.err) 192 } 193 return o.opener.OpenBucketURL(ctx, u) 194} 195 196// Scheme is the URL scheme gcsblob registers its URLOpener under on 197// blob.DefaultMux. 198const Scheme = "gs" 199 200// URLOpener opens GCS URLs like "gs://mybucket". 201// 202// The URL host is used as the bucket name. 203// 204// The following query parameters are supported: 205// 206// - access_id: sets Options.GoogleAccessID 207// - private_key_path: path to read for Options.PrivateKey 208// 209// Currently their use is limited to SignedURL. 210type URLOpener struct { 211 // Client must be set to a non-nil HTTP client authenticated with 212 // Cloud Storage scope or equivalent. 213 Client *gcp.HTTPClient 214 215 // Options specifies the default options to pass to OpenBucket. 216 Options Options 217} 218 219// OpenBucketURL opens the GCS bucket with the same name as the URL's host. 220func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) { 221 opts, err := o.forParams(ctx, u.Query()) 222 if err != nil { 223 return nil, fmt.Errorf("open bucket %v: %v", u, err) 224 } 225 return OpenBucket(ctx, o.Client, u.Host, opts) 226} 227 228func (o *URLOpener) forParams(ctx context.Context, q url.Values) (*Options, error) { 229 for k := range q { 230 if k != "access_id" && k != "private_key_path" { 231 return nil, fmt.Errorf("invalid query parameter %q", k) 232 } 233 } 234 opts := new(Options) 235 *opts = o.Options 236 if accessID := q.Get("access_id"); accessID != "" && accessID != opts.GoogleAccessID { 237 opts.GoogleAccessID = accessID 238 opts.PrivateKey = nil // Clear any previous key unrelated to the new accessID. 239 240 // Clear this as well to prevent calls with the old and mismatched accessID. 241 opts.MakeSignBytes = nil 242 } 243 if keyPath := q.Get("private_key_path"); keyPath != "" { 244 pk, err := ioutil.ReadFile(keyPath) 245 if err != nil { 246 return nil, err 247 } 248 opts.PrivateKey = pk 249 } else if _, exists := q["private_key_path"]; exists { 250 // A possible default value has been cleared by setting this to an empty value: 251 // The private key might have expired, or falling back to SignBytes/MakeSignBytes 252 // is intentional such as for tests or involving a key stored in a HSM/TPM. 253 opts.PrivateKey = nil 254 } 255 return opts, nil 256} 257 258// Options sets options for constructing a *blob.Bucket backed by GCS. 259type Options struct { 260 // GoogleAccessID represents the authorizer for SignedURL. 261 // Required to use SignedURL. 262 // See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions. 263 GoogleAccessID string 264 265 // PrivateKey is the Google service account private key. 266 // Exactly one of PrivateKey or SignBytes must be non-nil to use SignedURL. 267 // See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions. 268 PrivateKey []byte 269 270 // SignBytes is a function for implementing custom signing. 271 // Exactly one of PrivateKey, SignBytes, or MakeSignBytes must be non-nil to use SignedURL. 272 // See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions. 273 SignBytes func([]byte) ([]byte, error) 274 275 // MakeSignBytes is a factory for functions that are being used in place of an empty SignBytes. 276 // If your implementation of 'SignBytes' needs a request context, set this instead. 277 MakeSignBytes func(requestCtx context.Context) SignBytesFunc 278} 279 280// SignBytesFunc is shorthand for the signature of Options.SignBytes. 281type SignBytesFunc func([]byte) ([]byte, error) 282 283// openBucket returns a GCS Bucket that communicates using the given HTTP client. 284func openBucket(ctx context.Context, client *gcp.HTTPClient, bucketName string, opts *Options) (*bucket, error) { 285 if client == nil { 286 return nil, errors.New("gcsblob.OpenBucket: client is required") 287 } 288 if bucketName == "" { 289 return nil, errors.New("gcsblob.OpenBucket: bucketName is required") 290 } 291 292 clientOpts := []option.ClientOption{option.WithHTTPClient(useragent.HTTPClient(&client.Client, "blob"))} 293 if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != "" { 294 clientOpts = []option.ClientOption{ 295 option.WithoutAuthentication(), 296 option.WithEndpoint("http://" + host + "/storage/v1/"), 297 option.WithHTTPClient(http.DefaultClient), 298 } 299 } 300 301 // We wrap the provided http.Client to add a Go CDK User-Agent. 302 c, err := storage.NewClient(ctx, clientOpts...) 303 if err != nil { 304 return nil, err 305 } 306 if opts == nil { 307 opts = &Options{} 308 } 309 return &bucket{name: bucketName, client: c, opts: opts}, nil 310} 311 312// OpenBucket returns a *blob.Bucket backed by an existing GCS bucket. See the 313// package documentation for an example. 314func OpenBucket(ctx context.Context, client *gcp.HTTPClient, bucketName string, opts *Options) (*blob.Bucket, error) { 315 drv, err := openBucket(ctx, client, bucketName, opts) 316 if err != nil { 317 return nil, err 318 } 319 return blob.NewBucket(drv), nil 320} 321 322// bucket represents a GCS bucket, which handles read, write and delete operations 323// on objects within it. 324type bucket struct { 325 name string 326 client *storage.Client 327 opts *Options 328} 329 330var emptyBody = ioutil.NopCloser(strings.NewReader("")) 331 332// reader reads a GCS object. It implements driver.Reader. 333type reader struct { 334 body io.ReadCloser 335 attrs driver.ReaderAttributes 336 raw *storage.Reader 337} 338 339func (r *reader) Read(p []byte) (int, error) { 340 return r.body.Read(p) 341} 342 343// Close closes the reader itself. It must be called when done reading. 344func (r *reader) Close() error { 345 return r.body.Close() 346} 347 348func (r *reader) Attributes() *driver.ReaderAttributes { 349 return &r.attrs 350} 351 352func (r *reader) As(i interface{}) bool { 353 p, ok := i.(**storage.Reader) 354 if !ok { 355 return false 356 } 357 *p = r.raw 358 return true 359} 360 361func (b *bucket) ErrorCode(err error) gcerrors.ErrorCode { 362 if err == storage.ErrObjectNotExist || err == storage.ErrBucketNotExist { 363 return gcerrors.NotFound 364 } 365 if gerr, ok := err.(*googleapi.Error); ok { 366 switch gerr.Code { 367 case http.StatusForbidden: 368 return gcerrors.PermissionDenied 369 case http.StatusNotFound: 370 return gcerrors.NotFound 371 case http.StatusPreconditionFailed: 372 return gcerrors.FailedPrecondition 373 case http.StatusTooManyRequests: 374 return gcerrors.ResourceExhausted 375 } 376 } 377 return gcerrors.Unknown 378} 379 380func (b *bucket) Close() error { 381 return nil 382} 383 384// ListPaged implements driver.ListPaged. 385func (b *bucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) { 386 bkt := b.client.Bucket(b.name) 387 query := &storage.Query{ 388 Prefix: escapeKey(opts.Prefix), 389 Delimiter: escapeKey(opts.Delimiter), 390 } 391 if opts.BeforeList != nil { 392 asFunc := func(i interface{}) bool { 393 p, ok := i.(**storage.Query) 394 if !ok { 395 return false 396 } 397 *p = query 398 return true 399 } 400 if err := opts.BeforeList(asFunc); err != nil { 401 return nil, err 402 } 403 } 404 pageSize := opts.PageSize 405 if pageSize == 0 { 406 pageSize = defaultPageSize 407 } 408 iter := bkt.Objects(ctx, query) 409 pager := iterator.NewPager(iter, pageSize, string(opts.PageToken)) 410 var objects []*storage.ObjectAttrs 411 nextPageToken, err := pager.NextPage(&objects) 412 if err != nil { 413 return nil, err 414 } 415 page := driver.ListPage{NextPageToken: []byte(nextPageToken)} 416 if len(objects) > 0 { 417 page.Objects = make([]*driver.ListObject, len(objects)) 418 for i, obj := range objects { 419 toCopy := obj 420 asFunc := func(val interface{}) bool { 421 p, ok := val.(*storage.ObjectAttrs) 422 if !ok { 423 return false 424 } 425 *p = *toCopy 426 return true 427 } 428 if obj.Prefix == "" { 429 // Regular blob. 430 page.Objects[i] = &driver.ListObject{ 431 Key: unescapeKey(obj.Name), 432 ModTime: obj.Updated, 433 Size: obj.Size, 434 MD5: obj.MD5, 435 AsFunc: asFunc, 436 } 437 } else { 438 // "Directory". 439 page.Objects[i] = &driver.ListObject{ 440 Key: unescapeKey(obj.Prefix), 441 IsDir: true, 442 AsFunc: asFunc, 443 } 444 } 445 } 446 // GCS always returns "directories" at the end; sort them. 447 sort.Slice(page.Objects, func(i, j int) bool { 448 return page.Objects[i].Key < page.Objects[j].Key 449 }) 450 } 451 return &page, nil 452} 453 454// As implements driver.As. 455func (b *bucket) As(i interface{}) bool { 456 p, ok := i.(**storage.Client) 457 if !ok { 458 return false 459 } 460 *p = b.client 461 return true 462} 463 464// As implements driver.ErrorAs. 465func (b *bucket) ErrorAs(err error, i interface{}) bool { 466 switch v := err.(type) { 467 case *googleapi.Error: 468 if p, ok := i.(**googleapi.Error); ok { 469 *p = v 470 return true 471 } 472 } 473 return false 474} 475 476// Attributes implements driver.Attributes. 477func (b *bucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) { 478 key = escapeKey(key) 479 bkt := b.client.Bucket(b.name) 480 obj := bkt.Object(key) 481 attrs, err := obj.Attrs(ctx) 482 if err != nil { 483 return nil, err 484 } 485 // GCS seems to unquote the ETag; restore them. 486 // It should be of the form "xxxx" or W/"xxxx". 487 eTag := attrs.Etag 488 if !strings.HasPrefix(eTag, "W/\"") && !strings.HasPrefix(eTag, "\"") && !strings.HasSuffix(eTag, "\"") { 489 eTag = fmt.Sprintf("%q", eTag) 490 } 491 return &driver.Attributes{ 492 CacheControl: attrs.CacheControl, 493 ContentDisposition: attrs.ContentDisposition, 494 ContentEncoding: attrs.ContentEncoding, 495 ContentLanguage: attrs.ContentLanguage, 496 ContentType: attrs.ContentType, 497 Metadata: attrs.Metadata, 498 CreateTime: attrs.Created, 499 ModTime: attrs.Updated, 500 Size: attrs.Size, 501 MD5: attrs.MD5, 502 ETag: eTag, 503 AsFunc: func(i interface{}) bool { 504 p, ok := i.(*storage.ObjectAttrs) 505 if !ok { 506 return false 507 } 508 *p = *attrs 509 return true 510 }, 511 }, nil 512} 513 514// NewRangeReader implements driver.NewRangeReader. 515func (b *bucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) { 516 key = escapeKey(key) 517 bkt := b.client.Bucket(b.name) 518 obj := bkt.Object(key) 519 520 // Add an extra level of indirection so that BeforeRead can replace obj 521 // if needed. For example, ObjectHandle.If returns a new ObjectHandle. 522 // Also, make the Reader lazily in case this replacement happens. 523 objp := &obj 524 makeReader := func() (*storage.Reader, error) { 525 return (*objp).NewRangeReader(ctx, offset, length) 526 } 527 528 var r *storage.Reader 529 var rerr error 530 madeReader := false 531 if opts.BeforeRead != nil { 532 asFunc := func(i interface{}) bool { 533 if p, ok := i.(***storage.ObjectHandle); ok && !madeReader { 534 *p = objp 535 return true 536 } 537 if p, ok := i.(**storage.Reader); ok { 538 if !madeReader { 539 r, rerr = makeReader() 540 madeReader = true 541 if r == nil { 542 return false 543 } 544 } 545 *p = r 546 return true 547 } 548 return false 549 } 550 if err := opts.BeforeRead(asFunc); err != nil { 551 return nil, err 552 } 553 } 554 if !madeReader { 555 r, rerr = makeReader() 556 } 557 if rerr != nil { 558 return nil, rerr 559 } 560 return &reader{ 561 body: r, 562 attrs: driver.ReaderAttributes{ 563 ContentType: r.Attrs.ContentType, 564 ModTime: r.Attrs.LastModified, 565 Size: r.Attrs.Size, 566 }, 567 raw: r, 568 }, nil 569} 570 571// escapeKey does all required escaping for UTF-8 strings to work with GCS. 572func escapeKey(key string) string { 573 return escape.HexEscape(key, func(r []rune, i int) bool { 574 switch { 575 // GCS doesn't handle these characters (determined via experimentation). 576 case r[i] == 10 || r[i] == 13: 577 return true 578 // For "../", escape the trailing slash. 579 case i > 1 && r[i] == '/' && r[i-1] == '.' && r[i-2] == '.': 580 return true 581 } 582 return false 583 }) 584} 585 586// unescapeKey reverses escapeKey. 587func unescapeKey(key string) string { 588 return escape.HexUnescape(key) 589} 590 591// NewTypedWriter implements driver.NewTypedWriter. 592func (b *bucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) { 593 key = escapeKey(key) 594 bkt := b.client.Bucket(b.name) 595 obj := bkt.Object(key) 596 597 // Add an extra level of indirection so that BeforeWrite can replace obj 598 // if needed. For example, ObjectHandle.If returns a new ObjectHandle. 599 // Also, make the Writer lazily in case this replacement happens. 600 objp := &obj 601 makeWriter := func() *storage.Writer { 602 w := (*objp).NewWriter(ctx) 603 w.CacheControl = opts.CacheControl 604 w.ContentDisposition = opts.ContentDisposition 605 w.ContentEncoding = opts.ContentEncoding 606 w.ContentLanguage = opts.ContentLanguage 607 w.ContentType = contentType 608 w.ChunkSize = bufferSize(opts.BufferSize) 609 w.Metadata = opts.Metadata 610 w.MD5 = opts.ContentMD5 611 return w 612 } 613 614 var w *storage.Writer 615 if opts.BeforeWrite != nil { 616 asFunc := func(i interface{}) bool { 617 if p, ok := i.(***storage.ObjectHandle); ok && w == nil { 618 *p = objp 619 return true 620 } 621 if p, ok := i.(**storage.Writer); ok { 622 if w == nil { 623 w = makeWriter() 624 } 625 *p = w 626 return true 627 } 628 return false 629 } 630 if err := opts.BeforeWrite(asFunc); err != nil { 631 return nil, err 632 } 633 } 634 if w == nil { 635 w = makeWriter() 636 } 637 return w, nil 638} 639 640// CopyObjectHandles holds the ObjectHandles for the destination and source 641// of a Copy. It is used by the BeforeCopy As hook. 642type CopyObjectHandles struct { 643 Dst, Src *storage.ObjectHandle 644} 645 646// Copy implements driver.Copy. 647func (b *bucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error { 648 dstKey = escapeKey(dstKey) 649 srcKey = escapeKey(srcKey) 650 bkt := b.client.Bucket(b.name) 651 652 // Add an extra level of indirection so that BeforeCopy can replace the 653 // dst or src ObjectHandles if needed. 654 // Also, make the Copier lazily in case this replacement happens. 655 handles := CopyObjectHandles{ 656 Dst: bkt.Object(dstKey), 657 Src: bkt.Object(srcKey), 658 } 659 makeCopier := func() *storage.Copier { 660 return handles.Dst.CopierFrom(handles.Src) 661 } 662 663 var copier *storage.Copier 664 if opts.BeforeCopy != nil { 665 asFunc := func(i interface{}) bool { 666 if p, ok := i.(**CopyObjectHandles); ok && copier == nil { 667 *p = &handles 668 return true 669 } 670 if p, ok := i.(**storage.Copier); ok { 671 if copier == nil { 672 copier = makeCopier() 673 } 674 *p = copier 675 return true 676 } 677 return false 678 } 679 if err := opts.BeforeCopy(asFunc); err != nil { 680 return err 681 } 682 } 683 if copier == nil { 684 copier = makeCopier() 685 } 686 _, err := copier.Run(ctx) 687 return err 688} 689 690// Delete implements driver.Delete. 691func (b *bucket) Delete(ctx context.Context, key string) error { 692 key = escapeKey(key) 693 bkt := b.client.Bucket(b.name) 694 obj := bkt.Object(key) 695 return obj.Delete(ctx) 696} 697 698func (b *bucket) SignedURL(ctx context.Context, key string, dopts *driver.SignedURLOptions) (string, error) { 699 numSigners := 0 700 if b.opts.PrivateKey != nil { 701 numSigners++ 702 } 703 if b.opts.SignBytes != nil { 704 numSigners++ 705 } 706 if b.opts.MakeSignBytes != nil { 707 numSigners++ 708 } 709 if b.opts.GoogleAccessID == "" || numSigners != 1 { 710 return "", gcerr.New(gcerr.Unimplemented, nil, 1, "gcsblob: to use SignedURL, you must call OpenBucket with a valid Options.GoogleAccessID and exactly one of Options.PrivateKey, Options.SignBytes, or Options.MakeSignBytes") 711 } 712 713 key = escapeKey(key) 714 opts := &storage.SignedURLOptions{ 715 Expires: time.Now().Add(dopts.Expiry), 716 Method: dopts.Method, 717 ContentType: dopts.ContentType, 718 GoogleAccessID: b.opts.GoogleAccessID, 719 PrivateKey: b.opts.PrivateKey, 720 SignBytes: b.opts.SignBytes, 721 } 722 if b.opts.MakeSignBytes != nil { 723 opts.SignBytes = b.opts.MakeSignBytes(ctx) 724 } 725 if dopts.BeforeSign != nil { 726 asFunc := func(i interface{}) bool { 727 v, ok := i.(**storage.SignedURLOptions) 728 if ok { 729 *v = opts 730 } 731 return ok 732 } 733 if err := dopts.BeforeSign(asFunc); err != nil { 734 return "", err 735 } 736 } 737 return storage.SignedURL(b.name, key, opts) 738} 739 740func bufferSize(size int) int { 741 if size == 0 { 742 return googleapi.DefaultUploadChunkSize 743 } else if size > 0 { 744 return size 745 } 746 return 0 // disable buffering 747} 748