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  }