github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/blob/gcsblob/gcsblob.go (about) 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 56 package gcsblob // import "gocloud.dev/blob/gcsblob" 57 58 import ( 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 90 const defaultPageSize = 1000 91 92 func init() { 93 blob.DefaultURLMux().RegisterBucket(Scheme, new(lazyCredsOpener)) 94 } 95 96 // Set holds Wire providers for this package. 97 var 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. 109 func 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. 145 type lazyCredsOpener struct { 146 init sync.Once 147 opener *URLOpener 148 err error 149 } 150 151 func (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. 198 const 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. 210 type 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. 220 func (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 228 func (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. 259 type 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 // Deprecated: Use MakeSignBytes instead. 269 PrivateKey []byte 270 271 // SignBytes is a function for implementing custom signing. 272 // Exactly one of PrivateKey, SignBytes, or MakeSignBytes must be non-nil to use SignedURL. 273 // See https://godoc.org/cloud.google.com/go/storage#SignedURLOptions. 274 // Deprecated: Use MakeSignBytes instead. 275 SignBytes func([]byte) ([]byte, error) 276 277 // MakeSignBytes is a factory for functions that are being used in place of an empty SignBytes. 278 // If your implementation of 'SignBytes' needs a request context, set this instead. 279 MakeSignBytes func(requestCtx context.Context) SignBytesFunc 280 } 281 282 // SignBytesFunc is shorthand for the signature of Options.SignBytes. 283 type SignBytesFunc func([]byte) ([]byte, error) 284 285 // openBucket returns a GCS Bucket that communicates using the given HTTP client. 286 func openBucket(ctx context.Context, client *gcp.HTTPClient, bucketName string, opts *Options) (*bucket, error) { 287 if client == nil { 288 return nil, errors.New("gcsblob.OpenBucket: client is required") 289 } 290 if bucketName == "" { 291 return nil, errors.New("gcsblob.OpenBucket: bucketName is required") 292 } 293 294 clientOpts := []option.ClientOption{option.WithHTTPClient(useragent.HTTPClient(&client.Client, "blob"))} 295 if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != "" { 296 clientOpts = []option.ClientOption{ 297 option.WithoutAuthentication(), 298 option.WithEndpoint("http://" + host + "/storage/v1/"), 299 option.WithHTTPClient(http.DefaultClient), 300 } 301 } 302 303 // We wrap the provided http.Client to add a Go CDK User-Agent. 304 c, err := storage.NewClient(ctx, clientOpts...) 305 if err != nil { 306 return nil, err 307 } 308 if opts == nil { 309 opts = &Options{} 310 } 311 return &bucket{name: bucketName, client: c, opts: opts}, nil 312 } 313 314 // OpenBucket returns a *blob.Bucket backed by an existing GCS bucket. See the 315 // package documentation for an example. 316 func OpenBucket(ctx context.Context, client *gcp.HTTPClient, bucketName string, opts *Options) (*blob.Bucket, error) { 317 drv, err := openBucket(ctx, client, bucketName, opts) 318 if err != nil { 319 return nil, err 320 } 321 return blob.NewBucket(drv), nil 322 } 323 324 // bucket represents a GCS bucket, which handles read, write and delete operations 325 // on objects within it. 326 type bucket struct { 327 name string 328 client *storage.Client 329 opts *Options 330 } 331 332 var emptyBody = ioutil.NopCloser(strings.NewReader("")) 333 334 // reader reads a GCS object. It implements driver.Reader. 335 type reader struct { 336 body io.ReadCloser 337 attrs driver.ReaderAttributes 338 raw *storage.Reader 339 } 340 341 func (r *reader) Read(p []byte) (int, error) { 342 return r.body.Read(p) 343 } 344 345 // Close closes the reader itself. It must be called when done reading. 346 func (r *reader) Close() error { 347 return r.body.Close() 348 } 349 350 func (r *reader) Attributes() *driver.ReaderAttributes { 351 return &r.attrs 352 } 353 354 func (r *reader) As(i interface{}) bool { 355 p, ok := i.(**storage.Reader) 356 if !ok { 357 return false 358 } 359 *p = r.raw 360 return true 361 } 362 363 func (b *bucket) ErrorCode(err error) gcerrors.ErrorCode { 364 if err == storage.ErrObjectNotExist || err == storage.ErrBucketNotExist { 365 return gcerrors.NotFound 366 } 367 if gerr, ok := err.(*googleapi.Error); ok { 368 switch gerr.Code { 369 case http.StatusForbidden: 370 return gcerrors.PermissionDenied 371 case http.StatusNotFound: 372 return gcerrors.NotFound 373 case http.StatusPreconditionFailed: 374 return gcerrors.FailedPrecondition 375 case http.StatusTooManyRequests: 376 return gcerrors.ResourceExhausted 377 } 378 } 379 return gcerrors.Unknown 380 } 381 382 func (b *bucket) Close() error { 383 return nil 384 } 385 386 // ListPaged implements driver.ListPaged. 387 func (b *bucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) { 388 bkt := b.client.Bucket(b.name) 389 query := &storage.Query{ 390 Prefix: escapeKey(opts.Prefix), 391 Delimiter: escapeKey(opts.Delimiter), 392 } 393 if opts.BeforeList != nil { 394 asFunc := func(i interface{}) bool { 395 p, ok := i.(**storage.Query) 396 if !ok { 397 return false 398 } 399 *p = query 400 return true 401 } 402 if err := opts.BeforeList(asFunc); err != nil { 403 return nil, err 404 } 405 } 406 pageSize := opts.PageSize 407 if pageSize == 0 { 408 pageSize = defaultPageSize 409 } 410 iter := bkt.Objects(ctx, query) 411 pager := iterator.NewPager(iter, pageSize, string(opts.PageToken)) 412 var objects []*storage.ObjectAttrs 413 nextPageToken, err := pager.NextPage(&objects) 414 if err != nil { 415 return nil, err 416 } 417 page := driver.ListPage{NextPageToken: []byte(nextPageToken)} 418 if len(objects) > 0 { 419 page.Objects = make([]*driver.ListObject, len(objects)) 420 for i, obj := range objects { 421 toCopy := obj 422 asFunc := func(val interface{}) bool { 423 p, ok := val.(*storage.ObjectAttrs) 424 if !ok { 425 return false 426 } 427 *p = *toCopy 428 return true 429 } 430 if obj.Prefix == "" { 431 // Regular blob. 432 page.Objects[i] = &driver.ListObject{ 433 Key: unescapeKey(obj.Name), 434 ModTime: obj.Updated, 435 Size: obj.Size, 436 MD5: obj.MD5, 437 AsFunc: asFunc, 438 } 439 } else { 440 // "Directory". 441 page.Objects[i] = &driver.ListObject{ 442 Key: unescapeKey(obj.Prefix), 443 IsDir: true, 444 AsFunc: asFunc, 445 } 446 } 447 } 448 // GCS always returns "directories" at the end; sort them. 449 sort.Slice(page.Objects, func(i, j int) bool { 450 return page.Objects[i].Key < page.Objects[j].Key 451 }) 452 } 453 return &page, nil 454 } 455 456 // As implements driver.As. 457 func (b *bucket) As(i interface{}) bool { 458 p, ok := i.(**storage.Client) 459 if !ok { 460 return false 461 } 462 *p = b.client 463 return true 464 } 465 466 // As implements driver.ErrorAs. 467 func (b *bucket) ErrorAs(err error, i interface{}) bool { 468 switch v := err.(type) { 469 case *googleapi.Error: 470 if p, ok := i.(**googleapi.Error); ok { 471 *p = v 472 return true 473 } 474 } 475 return false 476 } 477 478 // Attributes implements driver.Attributes. 479 func (b *bucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) { 480 key = escapeKey(key) 481 bkt := b.client.Bucket(b.name) 482 obj := bkt.Object(key) 483 attrs, err := obj.Attrs(ctx) 484 if err != nil { 485 return nil, err 486 } 487 // GCS seems to unquote the ETag; restore them. 488 // It should be of the form "xxxx" or W/"xxxx". 489 eTag := attrs.Etag 490 if !strings.HasPrefix(eTag, "W/\"") && !strings.HasPrefix(eTag, "\"") && !strings.HasSuffix(eTag, "\"") { 491 eTag = fmt.Sprintf("%q", eTag) 492 } 493 return &driver.Attributes{ 494 CacheControl: attrs.CacheControl, 495 ContentDisposition: attrs.ContentDisposition, 496 ContentEncoding: attrs.ContentEncoding, 497 ContentLanguage: attrs.ContentLanguage, 498 ContentType: attrs.ContentType, 499 Metadata: attrs.Metadata, 500 CreateTime: attrs.Created, 501 ModTime: attrs.Updated, 502 Size: attrs.Size, 503 MD5: attrs.MD5, 504 ETag: eTag, 505 AsFunc: func(i interface{}) bool { 506 p, ok := i.(*storage.ObjectAttrs) 507 if !ok { 508 return false 509 } 510 *p = *attrs 511 return true 512 }, 513 }, nil 514 } 515 516 // NewRangeReader implements driver.NewRangeReader. 517 func (b *bucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) { 518 key = escapeKey(key) 519 bkt := b.client.Bucket(b.name) 520 obj := bkt.Object(key) 521 522 // Add an extra level of indirection so that BeforeRead can replace obj 523 // if needed. For example, ObjectHandle.If returns a new ObjectHandle. 524 // Also, make the Reader lazily in case this replacement happens. 525 objp := &obj 526 makeReader := func() (*storage.Reader, error) { 527 return (*objp).NewRangeReader(ctx, offset, length) 528 } 529 530 var r *storage.Reader 531 var rerr error 532 madeReader := false 533 if opts.BeforeRead != nil { 534 asFunc := func(i interface{}) bool { 535 if p, ok := i.(***storage.ObjectHandle); ok && !madeReader { 536 *p = objp 537 return true 538 } 539 if p, ok := i.(**storage.Reader); ok { 540 if !madeReader { 541 r, rerr = makeReader() 542 madeReader = true 543 if r == nil { 544 return false 545 } 546 } 547 *p = r 548 return true 549 } 550 return false 551 } 552 if err := opts.BeforeRead(asFunc); err != nil { 553 return nil, err 554 } 555 } 556 if !madeReader { 557 r, rerr = makeReader() 558 } 559 if rerr != nil { 560 return nil, rerr 561 } 562 return &reader{ 563 body: r, 564 attrs: driver.ReaderAttributes{ 565 ContentType: r.Attrs.ContentType, 566 ModTime: r.Attrs.LastModified, 567 Size: r.Attrs.Size, 568 }, 569 raw: r, 570 }, nil 571 } 572 573 // escapeKey does all required escaping for UTF-8 strings to work with GCS. 574 func escapeKey(key string) string { 575 return escape.HexEscape(key, func(r []rune, i int) bool { 576 switch { 577 // GCS doesn't handle these characters (determined via experimentation). 578 case r[i] == 10 || r[i] == 13: 579 return true 580 // For "../", escape the trailing slash. 581 case i > 1 && r[i] == '/' && r[i-1] == '.' && r[i-2] == '.': 582 return true 583 } 584 return false 585 }) 586 } 587 588 // unescapeKey reverses escapeKey. 589 func unescapeKey(key string) string { 590 return escape.HexUnescape(key) 591 } 592 593 // NewTypedWriter implements driver.NewTypedWriter. 594 func (b *bucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) { 595 key = escapeKey(key) 596 bkt := b.client.Bucket(b.name) 597 obj := bkt.Object(key) 598 599 // Add an extra level of indirection so that BeforeWrite can replace obj 600 // if needed. For example, ObjectHandle.If returns a new ObjectHandle. 601 // Also, make the Writer lazily in case this replacement happens. 602 objp := &obj 603 makeWriter := func() *storage.Writer { 604 w := (*objp).NewWriter(ctx) 605 w.CacheControl = opts.CacheControl 606 w.ContentDisposition = opts.ContentDisposition 607 w.ContentEncoding = opts.ContentEncoding 608 w.ContentLanguage = opts.ContentLanguage 609 w.ContentType = contentType 610 w.ChunkSize = bufferSize(opts.BufferSize) 611 w.Metadata = opts.Metadata 612 w.MD5 = opts.ContentMD5 613 return w 614 } 615 616 var w *storage.Writer 617 if opts.BeforeWrite != nil { 618 asFunc := func(i interface{}) bool { 619 if p, ok := i.(***storage.ObjectHandle); ok && w == nil { 620 *p = objp 621 return true 622 } 623 if p, ok := i.(**storage.Writer); ok { 624 if w == nil { 625 w = makeWriter() 626 } 627 *p = w 628 return true 629 } 630 return false 631 } 632 if err := opts.BeforeWrite(asFunc); err != nil { 633 return nil, err 634 } 635 } 636 if w == nil { 637 w = makeWriter() 638 } 639 return w, nil 640 } 641 642 // CopyObjectHandles holds the ObjectHandles for the destination and source 643 // of a Copy. It is used by the BeforeCopy As hook. 644 type CopyObjectHandles struct { 645 Dst, Src *storage.ObjectHandle 646 } 647 648 // Copy implements driver.Copy. 649 func (b *bucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error { 650 dstKey = escapeKey(dstKey) 651 srcKey = escapeKey(srcKey) 652 bkt := b.client.Bucket(b.name) 653 654 // Add an extra level of indirection so that BeforeCopy can replace the 655 // dst or src ObjectHandles if needed. 656 // Also, make the Copier lazily in case this replacement happens. 657 handles := CopyObjectHandles{ 658 Dst: bkt.Object(dstKey), 659 Src: bkt.Object(srcKey), 660 } 661 makeCopier := func() *storage.Copier { 662 return handles.Dst.CopierFrom(handles.Src) 663 } 664 665 var copier *storage.Copier 666 if opts.BeforeCopy != nil { 667 asFunc := func(i interface{}) bool { 668 if p, ok := i.(**CopyObjectHandles); ok && copier == nil { 669 *p = &handles 670 return true 671 } 672 if p, ok := i.(**storage.Copier); ok { 673 if copier == nil { 674 copier = makeCopier() 675 } 676 *p = copier 677 return true 678 } 679 return false 680 } 681 if err := opts.BeforeCopy(asFunc); err != nil { 682 return err 683 } 684 } 685 if copier == nil { 686 copier = makeCopier() 687 } 688 _, err := copier.Run(ctx) 689 return err 690 } 691 692 // Delete implements driver.Delete. 693 func (b *bucket) Delete(ctx context.Context, key string) error { 694 key = escapeKey(key) 695 bkt := b.client.Bucket(b.name) 696 obj := bkt.Object(key) 697 return obj.Delete(ctx) 698 } 699 700 func (b *bucket) SignedURL(ctx context.Context, key string, dopts *driver.SignedURLOptions) (string, error) { 701 numSigners := 0 702 if b.opts.PrivateKey != nil { 703 numSigners++ 704 } 705 if b.opts.SignBytes != nil { 706 numSigners++ 707 } 708 if b.opts.MakeSignBytes != nil { 709 numSigners++ 710 } 711 if b.opts.GoogleAccessID == "" || numSigners != 1 { 712 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") 713 } 714 715 key = escapeKey(key) 716 opts := &storage.SignedURLOptions{ 717 Expires: time.Now().Add(dopts.Expiry), 718 Method: dopts.Method, 719 ContentType: dopts.ContentType, 720 GoogleAccessID: b.opts.GoogleAccessID, 721 PrivateKey: b.opts.PrivateKey, 722 SignBytes: b.opts.SignBytes, 723 } 724 if b.opts.MakeSignBytes != nil { 725 opts.SignBytes = b.opts.MakeSignBytes(ctx) 726 } 727 if dopts.BeforeSign != nil { 728 asFunc := func(i interface{}) bool { 729 v, ok := i.(**storage.SignedURLOptions) 730 if ok { 731 *v = opts 732 } 733 return ok 734 } 735 if err := dopts.BeforeSign(asFunc); err != nil { 736 return "", err 737 } 738 } 739 return storage.SignedURL(b.name, key, opts) 740 } 741 742 func bufferSize(size int) int { 743 if size == 0 { 744 return googleapi.DefaultUploadChunkSize 745 } else if size > 0 { 746 return size 747 } 748 return 0 // disable buffering 749 }