github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/blob/s3blob/s3blob.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 s3blob provides a blob implementation that uses S3. Use OpenBucket 16 // to construct a *blob.Bucket. 17 // 18 // # URLs 19 // 20 // For blob.OpenBucket, s3blob registers for the scheme "s3". 21 // The default URL opener will use an AWS session with the default credentials 22 // and configuration; see https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ 23 // for more details. 24 // Use "awssdk=v1" or "awssdk=v2" to force a specific AWS SDK version. 25 // To customize the URL opener, or for more details on the URL format, 26 // To customize the URL opener, or for more details on the URL format, 27 // see URLOpener. 28 // See https://gocloud.dev/concepts/urls/ for background information. 29 // 30 // # Escaping 31 // 32 // Go CDK supports all UTF-8 strings; to make this work with services lacking 33 // full UTF-8 support, strings must be escaped (during writes) and unescaped 34 // (during reads). The following escapes are performed for s3blob: 35 // - Blob keys: ASCII characters 0-31 are escaped to "__0x<hex>__". 36 // Additionally, the "/" in "../" and the trailing "/" in "//" are escaped in 37 // the same way. 38 // - Metadata keys: Escaped using URL encoding, then additionally "@:=" are 39 // escaped using "__0x<hex>__". These characters were determined by 40 // experimentation. 41 // - Metadata values: Escaped using URL encoding. 42 // 43 // # As 44 // 45 // s3blob exposes the following types for As: 46 // - Bucket: (V1) *s3.S3; (V2) *s3v2.Client 47 // - Error: (V1) awserr.Error; (V2) any error type returned by the service, notably smithy.APIError 48 // - ListObject: (V1) s3.Object for objects, s3.CommonPrefix for "directories"; (V2) typesv2.Object for objects, typesv2.CommonPrefix for "directories 49 // - ListOptions.BeforeList: (V1) *s3.ListObjectsV2Input, or *s3.ListObjectsInput 50 // when Options.UseLegacyList == true; (V2) *s3v2.ListObjectsV2Input, or *s3v2.ListObjectsInput 51 // when Options.UseLegacyList == true 52 // - Reader: (V1) s3.GetObjectOutput; (V2) s3v2.GetObjectInput 53 // - ReaderOptions.BeforeRead: (V1) *s3.GetObjectInput; (V2) *s3v2.GetObjectInput 54 // - Attributes: (V1) s3.HeadObjectOutput; (V2)s3v2.HeadObjectOutput 55 // - CopyOptions.BeforeCopy: *(V1) s3.CopyObjectInput; (V2) s3v2.CopyObjectInput 56 // - WriterOptions.BeforeWrite: (V1) *s3manager.UploadInput, *s3manager.Uploader; (V2) *s3v2.PutObjectInput, *s3v2manager.Uploader 57 // - SignedURLOptions.BeforeSign: 58 // (V1) *s3.GetObjectInput; (V2) *s3v2.GetObjectInput, when Options.Method == http.MethodGet, or 59 // (V1) *s3.PutObjectInput; (V2) *s3v2.PutObjectInput, when Options.Method == http.MethodPut, or 60 // (V1) *s3.DeleteObjectInput; (V2) [not supported] when Options.Method == http.MethodDelete 61 package s3blob // import "gocloud.dev/blob/s3blob" 62 63 import ( 64 "context" 65 "encoding/base64" 66 "encoding/hex" 67 "errors" 68 "fmt" 69 "io" 70 "net/http" 71 "net/url" 72 "sort" 73 "strconv" 74 "strings" 75 76 s3managerv2 "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 77 s3v2 "github.com/aws/aws-sdk-go-v2/service/s3" 78 typesv2 "github.com/aws/aws-sdk-go-v2/service/s3/types" 79 "github.com/aws/aws-sdk-go/aws" 80 "github.com/aws/aws-sdk-go/aws/awserr" 81 "github.com/aws/aws-sdk-go/aws/client" 82 "github.com/aws/aws-sdk-go/aws/request" 83 "github.com/aws/aws-sdk-go/service/s3" 84 "github.com/aws/aws-sdk-go/service/s3/s3manager" 85 "github.com/aws/smithy-go" 86 "github.com/google/wire" 87 gcaws "gocloud.dev/aws" 88 "gocloud.dev/blob" 89 "gocloud.dev/blob/driver" 90 "gocloud.dev/gcerrors" 91 "gocloud.dev/internal/escape" 92 "gocloud.dev/internal/gcerr" 93 ) 94 95 const defaultPageSize = 1000 96 97 func init() { 98 blob.DefaultURLMux().RegisterBucket(Scheme, new(urlSessionOpener)) 99 } 100 101 // Set holds Wire providers for this package. 102 var Set = wire.NewSet( 103 wire.Struct(new(URLOpener), "ConfigProvider"), 104 ) 105 106 type urlSessionOpener struct{} 107 108 func (o *urlSessionOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) { 109 if gcaws.UseV2(u.Query()) { 110 opener := &URLOpener{UseV2: true} 111 return opener.OpenBucketURL(ctx, u) 112 } 113 sess, rest, err := gcaws.NewSessionFromURLParams(u.Query()) 114 if err != nil { 115 return nil, fmt.Errorf("open bucket %v: %v", u, err) 116 } 117 opener := &URLOpener{ 118 ConfigProvider: sess, 119 } 120 u.RawQuery = rest.Encode() 121 return opener.OpenBucketURL(ctx, u) 122 } 123 124 // Scheme is the URL scheme s3blob registers its URLOpener under on 125 // blob.DefaultMux. 126 const Scheme = "s3" 127 128 // URLOpener opens S3 URLs like "s3://mybucket". 129 // 130 // The URL host is used as the bucket name. 131 // 132 // Use "awssdk=v1" to force using AWS SDK v1, "awssdk=v2" to force using AWS SDK v2, 133 // or anything else to accept the default. 134 // 135 // For V1, see gocloud.dev/aws/ConfigFromURLParams for supported query parameters 136 // for overriding the aws.Session from the URL. 137 // For V2, see gocloud.dev/aws/V2ConfigFromURLParams. 138 type URLOpener struct { 139 // UseV2 indicates whether the AWS SDK V2 should be used. 140 UseV2 bool 141 142 // ConfigProvider must be set to a non-nil value if UseV2 is false. 143 ConfigProvider client.ConfigProvider 144 145 // Options specifies the options to pass to OpenBucket. 146 Options Options 147 } 148 149 // OpenBucketURL opens a blob.Bucket based on u. 150 func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) { 151 if o.UseV2 { 152 cfg, err := gcaws.V2ConfigFromURLParams(ctx, u.Query()) 153 if err != nil { 154 return nil, fmt.Errorf("open bucket %v: %v", u, err) 155 } 156 clientV2 := s3v2.NewFromConfig(cfg) 157 return OpenBucketV2(ctx, clientV2, u.Host, &o.Options) 158 } 159 configProvider := &gcaws.ConfigOverrider{ 160 Base: o.ConfigProvider, 161 } 162 overrideCfg, err := gcaws.ConfigFromURLParams(u.Query()) 163 if err != nil { 164 return nil, fmt.Errorf("open bucket %v: %v", u, err) 165 } 166 configProvider.Configs = append(configProvider.Configs, overrideCfg) 167 return OpenBucket(ctx, configProvider, u.Host, &o.Options) 168 } 169 170 // Options sets options for constructing a *blob.Bucket backed by fileblob. 171 type Options struct { 172 // UseLegacyList forces the use of ListObjects instead of ListObjectsV2. 173 // Some S3-compatible services (like CEPH) do not currently support 174 // ListObjectsV2. 175 UseLegacyList bool 176 } 177 178 // openBucket returns an S3 Bucket. 179 func openBucket(ctx context.Context, useV2 bool, sess client.ConfigProvider, clientV2 *s3v2.Client, bucketName string, opts *Options) (*bucket, error) { 180 if bucketName == "" { 181 return nil, errors.New("s3blob.OpenBucket: bucketName is required") 182 } 183 if opts == nil { 184 opts = &Options{} 185 } 186 var client *s3.S3 187 if useV2 { 188 if clientV2 == nil { 189 return nil, errors.New("s3blob.OpenBucketV2: client is required") 190 } 191 } else { 192 if sess == nil { 193 return nil, errors.New("s3blob.OpenBucket: sess is required") 194 } 195 client = s3.New(sess) 196 } 197 return &bucket{ 198 useV2: useV2, 199 name: bucketName, 200 client: client, 201 clientV2: clientV2, 202 useLegacyList: opts.UseLegacyList, 203 }, nil 204 } 205 206 // OpenBucket returns a *blob.Bucket backed by S3. 207 // AWS buckets are bound to a region; sess must have been created using an 208 // aws.Config with Region set to the right region for bucketName. 209 // See the package documentation for an example. 210 func OpenBucket(ctx context.Context, sess client.ConfigProvider, bucketName string, opts *Options) (*blob.Bucket, error) { 211 drv, err := openBucket(ctx, false, sess, nil, bucketName, opts) 212 if err != nil { 213 return nil, err 214 } 215 return blob.NewBucket(drv), nil 216 } 217 218 // OpenBucketV2 returns a *blob.Bucket backed by S3, using AWS SDK v2. 219 func OpenBucketV2(ctx context.Context, client *s3v2.Client, bucketName string, opts *Options) (*blob.Bucket, error) { 220 drv, err := openBucket(ctx, true, nil, client, bucketName, opts) 221 if err != nil { 222 return nil, err 223 } 224 return blob.NewBucket(drv), nil 225 } 226 227 // reader reads an S3 object. It implements io.ReadCloser. 228 type reader struct { 229 useV2 bool 230 body io.ReadCloser 231 attrs driver.ReaderAttributes 232 raw *s3.GetObjectOutput 233 rawV2 *s3v2.GetObjectOutput 234 } 235 236 func (r *reader) Read(p []byte) (int, error) { 237 return r.body.Read(p) 238 } 239 240 // Close closes the reader itself. It must be called when done reading. 241 func (r *reader) Close() error { 242 return r.body.Close() 243 } 244 245 func (r *reader) As(i interface{}) bool { 246 if r.useV2 { 247 p, ok := i.(*s3v2.GetObjectOutput) 248 if !ok { 249 return false 250 } 251 *p = *r.rawV2 252 } else { 253 p, ok := i.(*s3.GetObjectOutput) 254 if !ok { 255 return false 256 } 257 *p = *r.raw 258 } 259 return true 260 } 261 262 func (r *reader) Attributes() *driver.ReaderAttributes { 263 return &r.attrs 264 } 265 266 // writer writes an S3 object, it implements io.WriteCloser. 267 type writer struct { 268 w *io.PipeWriter // created when the first byte is written 269 270 ctx context.Context 271 useV2 bool 272 // v1 273 uploader *s3manager.Uploader 274 req *s3manager.UploadInput 275 // v2 276 uploaderV2 *s3managerv2.Uploader 277 reqV2 *s3v2.PutObjectInput 278 279 donec chan struct{} // closed when done writing 280 // The following fields will be written before donec closes: 281 err error 282 } 283 284 // Write appends p to w. User must call Close to close the w after done writing. 285 func (w *writer) Write(p []byte) (int, error) { 286 // Avoid opening the pipe for a zero-length write; 287 // the concrete can do these for empty blobs. 288 if len(p) == 0 { 289 return 0, nil 290 } 291 if w.w == nil { 292 // We'll write into pw and use pr as an io.Reader for the 293 // Upload call to S3. 294 pr, pw := io.Pipe() 295 w.w = pw 296 if err := w.open(pr); err != nil { 297 return 0, err 298 } 299 } 300 select { 301 case <-w.donec: 302 return 0, w.err 303 default: 304 } 305 return w.w.Write(p) 306 } 307 308 // pr may be nil if we're Closing and no data was written. 309 func (w *writer) open(pr *io.PipeReader) error { 310 311 go func() { 312 defer close(w.donec) 313 314 body := io.Reader(pr) 315 if pr == nil { 316 // AWS doesn't like a nil Body. 317 body = http.NoBody 318 } 319 var err error 320 if w.useV2 { 321 w.reqV2.Body = body 322 _, err = w.uploaderV2.Upload(w.ctx, w.reqV2) 323 } else { 324 w.req.Body = body 325 _, err = w.uploader.UploadWithContext(w.ctx, w.req) 326 } 327 if err != nil { 328 w.err = err 329 if pr != nil { 330 pr.CloseWithError(err) 331 } 332 return 333 } 334 }() 335 return nil 336 } 337 338 // Close completes the writer and closes it. Any error occurring during write 339 // will be returned. If a writer is closed before any Write is called, Close 340 // will create an empty file at the given key. 341 func (w *writer) Close() error { 342 if w.w == nil { 343 // We never got any bytes written. We'll write an http.NoBody. 344 w.open(nil) 345 } else if err := w.w.Close(); err != nil { 346 return err 347 } 348 <-w.donec 349 return w.err 350 } 351 352 // bucket represents an S3 bucket and handles read, write and delete operations. 353 type bucket struct { 354 name string 355 useV2 bool 356 client *s3.S3 357 clientV2 *s3v2.Client 358 useLegacyList bool 359 } 360 361 func (b *bucket) Close() error { 362 return nil 363 } 364 365 func (b *bucket) ErrorCode(err error) gcerrors.ErrorCode { 366 var code string 367 if b.useV2 { 368 var ae smithy.APIError 369 var oe *smithy.OperationError 370 if errors.As(err, &oe) && strings.Contains(oe.Error(), "301") { 371 // V2 returns an OperationError with a missing redirect for invalid buckets. 372 code = "NoSuchBucket" 373 } else if errors.As(err, &ae) { 374 code = ae.ErrorCode() 375 } else { 376 return gcerrors.Unknown 377 } 378 } else { 379 e, ok := err.(awserr.Error) 380 if !ok { 381 return gcerrors.Unknown 382 } 383 code = e.Code() 384 } 385 switch { 386 case code == "NoSuchBucket" || code == "NoSuchKey" || code == "NotFound" || code == s3.ErrCodeObjectNotInActiveTierError: 387 return gcerrors.NotFound 388 default: 389 return gcerrors.Unknown 390 } 391 } 392 393 // ListPaged implements driver.ListPaged. 394 func (b *bucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) { 395 pageSize := opts.PageSize 396 if pageSize == 0 { 397 pageSize = defaultPageSize 398 } 399 if b.useV2 { 400 in := &s3v2.ListObjectsV2Input{ 401 Bucket: aws.String(b.name), 402 MaxKeys: int32(pageSize), 403 } 404 if len(opts.PageToken) > 0 { 405 in.ContinuationToken = aws.String(string(opts.PageToken)) 406 } 407 if opts.Prefix != "" { 408 in.Prefix = aws.String(escapeKey(opts.Prefix)) 409 } 410 if opts.Delimiter != "" { 411 in.Delimiter = aws.String(escapeKey(opts.Delimiter)) 412 } 413 resp, err := b.listObjectsV2(ctx, in, opts) 414 if err != nil { 415 return nil, err 416 } 417 page := driver.ListPage{} 418 if resp.NextContinuationToken != nil { 419 page.NextPageToken = []byte(*resp.NextContinuationToken) 420 } 421 if n := len(resp.Contents) + len(resp.CommonPrefixes); n > 0 { 422 page.Objects = make([]*driver.ListObject, n) 423 for i, obj := range resp.Contents { 424 obj := obj 425 page.Objects[i] = &driver.ListObject{ 426 Key: unescapeKey(aws.StringValue(obj.Key)), 427 ModTime: *obj.LastModified, 428 Size: obj.Size, 429 MD5: eTagToMD5(obj.ETag), 430 AsFunc: func(i interface{}) bool { 431 p, ok := i.(*typesv2.Object) 432 if !ok { 433 return false 434 } 435 *p = obj 436 return true 437 }, 438 } 439 } 440 for i, prefix := range resp.CommonPrefixes { 441 prefix := prefix 442 page.Objects[i+len(resp.Contents)] = &driver.ListObject{ 443 Key: unescapeKey(aws.StringValue(prefix.Prefix)), 444 IsDir: true, 445 AsFunc: func(i interface{}) bool { 446 p, ok := i.(*typesv2.CommonPrefix) 447 if !ok { 448 return false 449 } 450 *p = prefix 451 return true 452 }, 453 } 454 } 455 if len(resp.Contents) > 0 && len(resp.CommonPrefixes) > 0 { 456 // S3 gives us blobs and "directories" in separate lists; sort them. 457 sort.Slice(page.Objects, func(i, j int) bool { 458 return page.Objects[i].Key < page.Objects[j].Key 459 }) 460 } 461 } 462 return &page, nil 463 } else { 464 in := &s3.ListObjectsV2Input{ 465 Bucket: aws.String(b.name), 466 MaxKeys: aws.Int64(int64(pageSize)), 467 } 468 if len(opts.PageToken) > 0 { 469 in.ContinuationToken = aws.String(string(opts.PageToken)) 470 } 471 if opts.Prefix != "" { 472 in.Prefix = aws.String(escapeKey(opts.Prefix)) 473 } 474 if opts.Delimiter != "" { 475 in.Delimiter = aws.String(escapeKey(opts.Delimiter)) 476 } 477 resp, err := b.listObjects(ctx, in, opts) 478 if err != nil { 479 return nil, err 480 } 481 page := driver.ListPage{} 482 if resp.NextContinuationToken != nil { 483 page.NextPageToken = []byte(*resp.NextContinuationToken) 484 } 485 if n := len(resp.Contents) + len(resp.CommonPrefixes); n > 0 { 486 page.Objects = make([]*driver.ListObject, n) 487 for i, obj := range resp.Contents { 488 obj := obj 489 page.Objects[i] = &driver.ListObject{ 490 Key: unescapeKey(aws.StringValue(obj.Key)), 491 ModTime: *obj.LastModified, 492 Size: *obj.Size, 493 MD5: eTagToMD5(obj.ETag), 494 AsFunc: func(i interface{}) bool { 495 p, ok := i.(*s3.Object) 496 if !ok { 497 return false 498 } 499 *p = *obj 500 return true 501 }, 502 } 503 } 504 for i, prefix := range resp.CommonPrefixes { 505 prefix := prefix 506 page.Objects[i+len(resp.Contents)] = &driver.ListObject{ 507 Key: unescapeKey(aws.StringValue(prefix.Prefix)), 508 IsDir: true, 509 AsFunc: func(i interface{}) bool { 510 p, ok := i.(*s3.CommonPrefix) 511 if !ok { 512 return false 513 } 514 *p = *prefix 515 return true 516 }, 517 } 518 } 519 if len(resp.Contents) > 0 && len(resp.CommonPrefixes) > 0 { 520 // S3 gives us blobs and "directories" in separate lists; sort them. 521 sort.Slice(page.Objects, func(i, j int) bool { 522 return page.Objects[i].Key < page.Objects[j].Key 523 }) 524 } 525 } 526 return &page, nil 527 } 528 } 529 530 func (b *bucket) listObjectsV2(ctx context.Context, in *s3v2.ListObjectsV2Input, opts *driver.ListOptions) (*s3v2.ListObjectsV2Output, error) { 531 if !b.useLegacyList { 532 if opts.BeforeList != nil { 533 asFunc := func(i interface{}) bool { 534 p, ok := i.(**s3v2.ListObjectsV2Input) 535 if !ok { 536 return false 537 } 538 *p = in 539 return true 540 } 541 if err := opts.BeforeList(asFunc); err != nil { 542 return nil, err 543 } 544 } 545 return b.clientV2.ListObjectsV2(ctx, in) 546 } 547 548 // Use the legacy ListObjects request. 549 legacyIn := &s3v2.ListObjectsInput{ 550 Bucket: in.Bucket, 551 Delimiter: in.Delimiter, 552 EncodingType: in.EncodingType, 553 Marker: in.ContinuationToken, 554 MaxKeys: in.MaxKeys, 555 Prefix: in.Prefix, 556 RequestPayer: in.RequestPayer, 557 } 558 if opts.BeforeList != nil { 559 asFunc := func(i interface{}) bool { 560 p, ok := i.(**s3v2.ListObjectsInput) 561 if !ok { 562 return false 563 } 564 *p = legacyIn 565 return true 566 } 567 if err := opts.BeforeList(asFunc); err != nil { 568 return nil, err 569 } 570 } 571 legacyResp, err := b.clientV2.ListObjects(ctx, legacyIn) 572 if err != nil { 573 return nil, err 574 } 575 576 var nextContinuationToken *string 577 if legacyResp.NextMarker != nil { 578 nextContinuationToken = legacyResp.NextMarker 579 } else if legacyResp.IsTruncated { 580 nextContinuationToken = aws.String(aws.StringValue(legacyResp.Contents[len(legacyResp.Contents)-1].Key)) 581 } 582 return &s3v2.ListObjectsV2Output{ 583 CommonPrefixes: legacyResp.CommonPrefixes, 584 Contents: legacyResp.Contents, 585 NextContinuationToken: nextContinuationToken, 586 }, nil 587 } 588 589 func (b *bucket) listObjects(ctx context.Context, in *s3.ListObjectsV2Input, opts *driver.ListOptions) (*s3.ListObjectsV2Output, error) { 590 if !b.useLegacyList { 591 if opts.BeforeList != nil { 592 asFunc := func(i interface{}) bool { 593 p, ok := i.(**s3.ListObjectsV2Input) 594 if !ok { 595 return false 596 } 597 *p = in 598 return true 599 } 600 if err := opts.BeforeList(asFunc); err != nil { 601 return nil, err 602 } 603 } 604 return b.client.ListObjectsV2WithContext(ctx, in) 605 } 606 607 // Use the legacy ListObjects request. 608 legacyIn := &s3.ListObjectsInput{ 609 Bucket: in.Bucket, 610 Delimiter: in.Delimiter, 611 EncodingType: in.EncodingType, 612 Marker: in.ContinuationToken, 613 MaxKeys: in.MaxKeys, 614 Prefix: in.Prefix, 615 RequestPayer: in.RequestPayer, 616 } 617 if opts.BeforeList != nil { 618 asFunc := func(i interface{}) bool { 619 p, ok := i.(**s3.ListObjectsInput) 620 if !ok { 621 return false 622 } 623 *p = legacyIn 624 return true 625 } 626 if err := opts.BeforeList(asFunc); err != nil { 627 return nil, err 628 } 629 } 630 legacyResp, err := b.client.ListObjectsWithContext(ctx, legacyIn) 631 if err != nil { 632 return nil, err 633 } 634 635 var nextContinuationToken *string 636 if legacyResp.NextMarker != nil { 637 nextContinuationToken = legacyResp.NextMarker 638 } else if aws.BoolValue(legacyResp.IsTruncated) { 639 nextContinuationToken = aws.String(aws.StringValue(legacyResp.Contents[len(legacyResp.Contents)-1].Key)) 640 } 641 return &s3.ListObjectsV2Output{ 642 CommonPrefixes: legacyResp.CommonPrefixes, 643 Contents: legacyResp.Contents, 644 NextContinuationToken: nextContinuationToken, 645 }, nil 646 } 647 648 // As implements driver.As. 649 func (b *bucket) As(i interface{}) bool { 650 if b.useV2 { 651 p, ok := i.(**s3v2.Client) 652 if !ok { 653 return false 654 } 655 *p = b.clientV2 656 } else { 657 p, ok := i.(**s3.S3) 658 if !ok { 659 return false 660 } 661 *p = b.client 662 } 663 return true 664 } 665 666 // As implements driver.ErrorAs. 667 func (b *bucket) ErrorAs(err error, i interface{}) bool { 668 if b.useV2 { 669 return errors.As(err, i) 670 } 671 switch v := err.(type) { 672 case awserr.Error: 673 if p, ok := i.(*awserr.Error); ok { 674 *p = v 675 return true 676 } 677 } 678 return false 679 } 680 681 // Attributes implements driver.Attributes. 682 func (b *bucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) { 683 key = escapeKey(key) 684 if b.useV2 { 685 in := &s3v2.HeadObjectInput{ 686 Bucket: aws.String(b.name), 687 Key: aws.String(key), 688 } 689 resp, err := b.clientV2.HeadObject(ctx, in) 690 if err != nil { 691 return nil, err 692 } 693 694 md := make(map[string]string, len(resp.Metadata)) 695 for k, v := range resp.Metadata { 696 // See the package comments for more details on escaping of metadata 697 // keys & values. 698 md[escape.HexUnescape(escape.URLUnescape(k))] = escape.URLUnescape(v) 699 } 700 return &driver.Attributes{ 701 CacheControl: aws.StringValue(resp.CacheControl), 702 ContentDisposition: aws.StringValue(resp.ContentDisposition), 703 ContentEncoding: aws.StringValue(resp.ContentEncoding), 704 ContentLanguage: aws.StringValue(resp.ContentLanguage), 705 ContentType: aws.StringValue(resp.ContentType), 706 Metadata: md, 707 // CreateTime not supported; left as the zero time. 708 ModTime: aws.TimeValue(resp.LastModified), 709 Size: resp.ContentLength, 710 MD5: eTagToMD5(resp.ETag), 711 ETag: aws.StringValue(resp.ETag), 712 AsFunc: func(i interface{}) bool { 713 p, ok := i.(*s3v2.HeadObjectOutput) 714 if !ok { 715 return false 716 } 717 *p = *resp 718 return true 719 }, 720 }, nil 721 } else { 722 in := &s3.HeadObjectInput{ 723 Bucket: aws.String(b.name), 724 Key: aws.String(key), 725 } 726 resp, err := b.client.HeadObjectWithContext(ctx, in) 727 if err != nil { 728 return nil, err 729 } 730 731 md := make(map[string]string, len(resp.Metadata)) 732 for k, v := range resp.Metadata { 733 // See the package comments for more details on escaping of metadata 734 // keys & values. 735 md[escape.HexUnescape(escape.URLUnescape(k))] = escape.URLUnescape(aws.StringValue(v)) 736 } 737 return &driver.Attributes{ 738 CacheControl: aws.StringValue(resp.CacheControl), 739 ContentDisposition: aws.StringValue(resp.ContentDisposition), 740 ContentEncoding: aws.StringValue(resp.ContentEncoding), 741 ContentLanguage: aws.StringValue(resp.ContentLanguage), 742 ContentType: aws.StringValue(resp.ContentType), 743 Metadata: md, 744 // CreateTime not supported; left as the zero time. 745 ModTime: aws.TimeValue(resp.LastModified), 746 Size: aws.Int64Value(resp.ContentLength), 747 MD5: eTagToMD5(resp.ETag), 748 ETag: aws.StringValue(resp.ETag), 749 AsFunc: func(i interface{}) bool { 750 p, ok := i.(*s3.HeadObjectOutput) 751 if !ok { 752 return false 753 } 754 *p = *resp 755 return true 756 }, 757 }, nil 758 } 759 } 760 761 // NewRangeReader implements driver.NewRangeReader. 762 func (b *bucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) { 763 key = escapeKey(key) 764 var byteRange *string 765 if offset > 0 && length < 0 { 766 byteRange = aws.String(fmt.Sprintf("bytes=%d-", offset)) 767 } else if length == 0 { 768 // AWS doesn't support a zero-length read; we'll read 1 byte and then 769 // ignore it in favor of http.NoBody below. 770 byteRange = aws.String(fmt.Sprintf("bytes=%d-%d", offset, offset)) 771 } else if length >= 0 { 772 byteRange = aws.String(fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) 773 } 774 if b.useV2 { 775 in := &s3v2.GetObjectInput{ 776 Bucket: aws.String(b.name), 777 Key: aws.String(key), 778 Range: byteRange, 779 } 780 if opts.BeforeRead != nil { 781 asFunc := func(i interface{}) bool { 782 if p, ok := i.(**s3v2.GetObjectInput); ok { 783 *p = in 784 return true 785 } 786 return false 787 } 788 if err := opts.BeforeRead(asFunc); err != nil { 789 return nil, err 790 } 791 } 792 resp, err := b.clientV2.GetObject(ctx, in) 793 if err != nil { 794 return nil, err 795 } 796 body := resp.Body 797 if length == 0 { 798 body = http.NoBody 799 } 800 return &reader{ 801 useV2: true, 802 body: body, 803 attrs: driver.ReaderAttributes{ 804 ContentType: aws.StringValue(resp.ContentType), 805 ModTime: aws.TimeValue(resp.LastModified), 806 Size: getSize(resp.ContentLength, aws.StringValue(resp.ContentRange)), 807 }, 808 rawV2: resp, 809 }, nil 810 } else { 811 in := &s3.GetObjectInput{ 812 Bucket: aws.String(b.name), 813 Key: aws.String(key), 814 Range: byteRange, 815 } 816 if opts.BeforeRead != nil { 817 asFunc := func(i interface{}) bool { 818 if p, ok := i.(**s3.GetObjectInput); ok { 819 *p = in 820 return true 821 } 822 return false 823 } 824 if err := opts.BeforeRead(asFunc); err != nil { 825 return nil, err 826 } 827 } 828 resp, err := b.client.GetObjectWithContext(ctx, in) 829 if err != nil { 830 return nil, err 831 } 832 body := resp.Body 833 if length == 0 { 834 body = http.NoBody 835 } 836 return &reader{ 837 useV2: false, 838 body: body, 839 attrs: driver.ReaderAttributes{ 840 ContentType: aws.StringValue(resp.ContentType), 841 ModTime: aws.TimeValue(resp.LastModified), 842 Size: getSize(aws.Int64Value(resp.ContentLength), aws.StringValue(resp.ContentRange)), 843 }, 844 raw: resp, 845 }, nil 846 } 847 } 848 849 // etagToMD5 processes an ETag header and returns an MD5 hash if possible. 850 // S3's ETag header is sometimes a quoted hexstring of the MD5. Other times, 851 // notably when the object was uploaded in multiple parts, it is not. 852 // We do the best we can. 853 // Some links about ETag: 854 // https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html 855 // https://github.com/aws/aws-sdk-net/issues/815 856 // https://teppen.io/2018/06/23/aws_s3_etags/ 857 func eTagToMD5(etag *string) []byte { 858 if etag == nil { 859 // No header at all. 860 return nil 861 } 862 // Strip the expected leading and trailing quotes. 863 quoted := *etag 864 if len(quoted) < 2 || quoted[0] != '"' || quoted[len(quoted)-1] != '"' { 865 return nil 866 } 867 unquoted := quoted[1 : len(quoted)-1] 868 // Un-hex; we return nil on error. In particular, we'll get an error here 869 // for multi-part uploaded blobs, whose ETag will contain a "-" and so will 870 // never be a legal hex encoding. 871 md5, err := hex.DecodeString(unquoted) 872 if err != nil { 873 return nil 874 } 875 return md5 876 } 877 878 func getSize(contentLength int64, contentRange string) int64 { 879 // Default size to ContentLength, but that's incorrect for partial-length reads, 880 // where ContentLength refers to the size of the returned Body, not the entire 881 // size of the blob. ContentRange has the full size. 882 size := contentLength 883 if contentRange != "" { 884 // Sample: bytes 10-14/27 (where 27 is the full size). 885 parts := strings.Split(contentRange, "/") 886 if len(parts) == 2 { 887 if i, err := strconv.ParseInt(parts[1], 10, 64); err == nil { 888 size = i 889 } 890 } 891 } 892 return size 893 } 894 895 // escapeKey does all required escaping for UTF-8 strings to work with S3. 896 func escapeKey(key string) string { 897 return escape.HexEscape(key, func(r []rune, i int) bool { 898 c := r[i] 899 switch { 900 // S3 doesn't handle these characters (determined via experimentation). 901 case c < 32: 902 return true 903 // For "../", escape the trailing slash. 904 case i > 1 && c == '/' && r[i-1] == '.' && r[i-2] == '.': 905 return true 906 // For "//", escape the trailing slash. Otherwise, S3 drops it. 907 case i > 0 && c == '/' && r[i-1] == '/': 908 return true 909 } 910 return false 911 }) 912 } 913 914 // unescapeKey reverses escapeKey. 915 func unescapeKey(key string) string { 916 return escape.HexUnescape(key) 917 } 918 919 // NewTypedWriter implements driver.NewTypedWriter. 920 func (b *bucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) { 921 key = escapeKey(key) 922 if b.useV2 { 923 uploaderV2 := s3managerv2.NewUploader(b.clientV2, func(u *s3managerv2.Uploader) { 924 if opts.BufferSize != 0 { 925 u.PartSize = int64(opts.BufferSize) 926 } 927 if opts.MaxConcurrency != 0 { 928 u.Concurrency = opts.MaxConcurrency 929 } 930 }) 931 md := make(map[string]string, len(opts.Metadata)) 932 for k, v := range opts.Metadata { 933 // See the package comments for more details on escaping of metadata 934 // keys & values. 935 k = escape.HexEscape(url.PathEscape(k), func(runes []rune, i int) bool { 936 c := runes[i] 937 return c == '@' || c == ':' || c == '=' 938 }) 939 md[k] = url.PathEscape(v) 940 } 941 reqV2 := &s3v2.PutObjectInput{ 942 Bucket: aws.String(b.name), 943 ContentType: aws.String(contentType), 944 Key: aws.String(key), 945 Metadata: md, 946 } 947 if opts.CacheControl != "" { 948 reqV2.CacheControl = aws.String(opts.CacheControl) 949 } 950 if opts.ContentDisposition != "" { 951 reqV2.ContentDisposition = aws.String(opts.ContentDisposition) 952 } 953 if opts.ContentEncoding != "" { 954 reqV2.ContentEncoding = aws.String(opts.ContentEncoding) 955 } 956 if opts.ContentLanguage != "" { 957 reqV2.ContentLanguage = aws.String(opts.ContentLanguage) 958 } 959 if len(opts.ContentMD5) > 0 { 960 reqV2.ContentMD5 = aws.String(base64.StdEncoding.EncodeToString(opts.ContentMD5)) 961 } 962 if opts.BeforeWrite != nil { 963 asFunc := func(i interface{}) bool { 964 pu, ok := i.(**s3managerv2.Uploader) 965 if ok { 966 *pu = uploaderV2 967 return true 968 } 969 pui, ok := i.(**s3v2.PutObjectInput) 970 if ok { 971 *pui = reqV2 972 return true 973 } 974 return false 975 } 976 if err := opts.BeforeWrite(asFunc); err != nil { 977 return nil, err 978 } 979 } 980 return &writer{ 981 ctx: ctx, 982 useV2: true, 983 uploaderV2: uploaderV2, 984 reqV2: reqV2, 985 donec: make(chan struct{}), 986 }, nil 987 } else { 988 uploader := s3manager.NewUploaderWithClient(b.client, func(u *s3manager.Uploader) { 989 if opts.BufferSize != 0 { 990 u.PartSize = int64(opts.BufferSize) 991 } 992 if opts.MaxConcurrency != 0 { 993 u.Concurrency = opts.MaxConcurrency 994 } 995 }) 996 md := make(map[string]*string, len(opts.Metadata)) 997 for k, v := range opts.Metadata { 998 // See the package comments for more details on escaping of metadata 999 // keys & values. 1000 k = escape.HexEscape(url.PathEscape(k), func(runes []rune, i int) bool { 1001 c := runes[i] 1002 return c == '@' || c == ':' || c == '=' 1003 }) 1004 md[k] = aws.String(url.PathEscape(v)) 1005 } 1006 req := &s3manager.UploadInput{ 1007 Bucket: aws.String(b.name), 1008 ContentType: aws.String(contentType), 1009 Key: aws.String(key), 1010 Metadata: md, 1011 } 1012 if opts.CacheControl != "" { 1013 req.CacheControl = aws.String(opts.CacheControl) 1014 } 1015 if opts.ContentDisposition != "" { 1016 req.ContentDisposition = aws.String(opts.ContentDisposition) 1017 } 1018 if opts.ContentEncoding != "" { 1019 req.ContentEncoding = aws.String(opts.ContentEncoding) 1020 } 1021 if opts.ContentLanguage != "" { 1022 req.ContentLanguage = aws.String(opts.ContentLanguage) 1023 } 1024 if len(opts.ContentMD5) > 0 { 1025 req.ContentMD5 = aws.String(base64.StdEncoding.EncodeToString(opts.ContentMD5)) 1026 } 1027 if opts.BeforeWrite != nil { 1028 asFunc := func(i interface{}) bool { 1029 pu, ok := i.(**s3manager.Uploader) 1030 if ok { 1031 *pu = uploader 1032 return true 1033 } 1034 pui, ok := i.(**s3manager.UploadInput) 1035 if ok { 1036 *pui = req 1037 return true 1038 } 1039 return false 1040 } 1041 if err := opts.BeforeWrite(asFunc); err != nil { 1042 return nil, err 1043 } 1044 } 1045 return &writer{ 1046 ctx: ctx, 1047 uploader: uploader, 1048 req: req, 1049 donec: make(chan struct{}), 1050 }, nil 1051 } 1052 } 1053 1054 // Copy implements driver.Copy. 1055 func (b *bucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error { 1056 dstKey = escapeKey(dstKey) 1057 srcKey = escapeKey(srcKey) 1058 if b.useV2 { 1059 input := &s3v2.CopyObjectInput{ 1060 Bucket: aws.String(b.name), 1061 CopySource: aws.String(b.name + "/" + srcKey), 1062 Key: aws.String(dstKey), 1063 } 1064 if opts.BeforeCopy != nil { 1065 asFunc := func(i interface{}) bool { 1066 switch v := i.(type) { 1067 case **s3v2.CopyObjectInput: 1068 *v = input 1069 return true 1070 } 1071 return false 1072 } 1073 if err := opts.BeforeCopy(asFunc); err != nil { 1074 return err 1075 } 1076 } 1077 _, err := b.clientV2.CopyObject(ctx, input) 1078 return err 1079 } else { 1080 input := &s3.CopyObjectInput{ 1081 Bucket: aws.String(b.name), 1082 CopySource: aws.String(b.name + "/" + srcKey), 1083 Key: aws.String(dstKey), 1084 } 1085 if opts.BeforeCopy != nil { 1086 asFunc := func(i interface{}) bool { 1087 switch v := i.(type) { 1088 case **s3.CopyObjectInput: 1089 *v = input 1090 return true 1091 } 1092 return false 1093 } 1094 if err := opts.BeforeCopy(asFunc); err != nil { 1095 return err 1096 } 1097 } 1098 _, err := b.client.CopyObjectWithContext(ctx, input) 1099 return err 1100 } 1101 } 1102 1103 // Delete implements driver.Delete. 1104 func (b *bucket) Delete(ctx context.Context, key string) error { 1105 if _, err := b.Attributes(ctx, key); err != nil { 1106 return err 1107 } 1108 key = escapeKey(key) 1109 if b.useV2 { 1110 input := &s3v2.DeleteObjectInput{ 1111 Bucket: aws.String(b.name), 1112 Key: aws.String(key), 1113 } 1114 _, err := b.clientV2.DeleteObject(ctx, input) 1115 return err 1116 } else { 1117 input := &s3.DeleteObjectInput{ 1118 Bucket: aws.String(b.name), 1119 Key: aws.String(key), 1120 } 1121 _, err := b.client.DeleteObjectWithContext(ctx, input) 1122 return err 1123 } 1124 } 1125 1126 func (b *bucket) SignedURL(ctx context.Context, key string, opts *driver.SignedURLOptions) (string, error) { 1127 key = escapeKey(key) 1128 var req *request.Request 1129 switch opts.Method { 1130 case http.MethodGet: 1131 if b.useV2 { 1132 in := &s3v2.GetObjectInput{ 1133 Bucket: aws.String(b.name), 1134 Key: aws.String(key), 1135 } 1136 if opts.BeforeSign != nil { 1137 asFunc := func(i interface{}) bool { 1138 v, ok := i.(**s3v2.GetObjectInput) 1139 if ok { 1140 *v = in 1141 } 1142 return ok 1143 } 1144 if err := opts.BeforeSign(asFunc); err != nil { 1145 return "", err 1146 } 1147 } 1148 p, err := s3v2.NewPresignClient(b.clientV2, s3v2.WithPresignExpires(opts.Expiry)).PresignGetObject(ctx, in) 1149 if err != nil { 1150 return "", err 1151 } 1152 return p.URL, nil 1153 } else { 1154 in := &s3.GetObjectInput{ 1155 Bucket: aws.String(b.name), 1156 Key: aws.String(key), 1157 } 1158 if opts.BeforeSign != nil { 1159 asFunc := func(i interface{}) bool { 1160 v, ok := i.(**s3.GetObjectInput) 1161 if ok { 1162 *v = in 1163 } 1164 return ok 1165 } 1166 if err := opts.BeforeSign(asFunc); err != nil { 1167 return "", err 1168 } 1169 } 1170 req, _ = b.client.GetObjectRequest(in) 1171 // fall through with req 1172 } 1173 case http.MethodPut: 1174 if b.useV2 { 1175 in := &s3v2.PutObjectInput{ 1176 Bucket: aws.String(b.name), 1177 Key: aws.String(key), 1178 } 1179 if opts.EnforceAbsentContentType || opts.ContentType != "" { 1180 // https://github.com/aws/aws-sdk-go-v2/issues/1475 1181 return "", gcerr.New(gcerr.Unimplemented, nil, 1, "s3blob: AWS SDK v2 does not supported enforcing ContentType in SignedURLs for PUT") 1182 } 1183 if opts.BeforeSign != nil { 1184 asFunc := func(i interface{}) bool { 1185 v, ok := i.(**s3v2.PutObjectInput) 1186 if ok { 1187 *v = in 1188 } 1189 return ok 1190 } 1191 if err := opts.BeforeSign(asFunc); err != nil { 1192 return "", err 1193 } 1194 } 1195 p, err := s3v2.NewPresignClient(b.clientV2, s3v2.WithPresignExpires(opts.Expiry)).PresignPutObject(ctx, in) 1196 if err != nil { 1197 return "", err 1198 } 1199 return p.URL, nil 1200 } else { 1201 in := &s3.PutObjectInput{ 1202 Bucket: aws.String(b.name), 1203 Key: aws.String(key), 1204 } 1205 if opts.EnforceAbsentContentType || opts.ContentType != "" { 1206 in.ContentType = aws.String(opts.ContentType) 1207 } 1208 if opts.BeforeSign != nil { 1209 asFunc := func(i interface{}) bool { 1210 v, ok := i.(**s3.PutObjectInput) 1211 if ok { 1212 *v = in 1213 } 1214 return ok 1215 } 1216 if err := opts.BeforeSign(asFunc); err != nil { 1217 return "", err 1218 } 1219 } 1220 req, _ = b.client.PutObjectRequest(in) 1221 // fall through with req 1222 } 1223 case http.MethodDelete: 1224 if b.useV2 { 1225 // https://github.com/aws/aws-sdk-java-v2/issues/2520 1226 return "", gcerr.New(gcerr.Unimplemented, nil, 1, "s3blob: AWS SDK v2 does not support SignedURL for DELETE") 1227 } 1228 in := &s3.DeleteObjectInput{ 1229 Bucket: aws.String(b.name), 1230 Key: aws.String(key), 1231 } 1232 if opts.BeforeSign != nil { 1233 asFunc := func(i interface{}) bool { 1234 v, ok := i.(**s3.DeleteObjectInput) 1235 if ok { 1236 *v = in 1237 } 1238 return ok 1239 } 1240 if err := opts.BeforeSign(asFunc); err != nil { 1241 return "", err 1242 } 1243 } 1244 req, _ = b.client.DeleteObjectRequest(in) 1245 default: 1246 return "", fmt.Errorf("unsupported Method %q", opts.Method) 1247 } 1248 return req.Presign(opts.Expiry) 1249 }