github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/file/s3file/stat.go (about)

     1  package s3file
     2  
     3  import (
     4  	"context"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/aws/aws-sdk-go/aws"
     9  	"github.com/aws/aws-sdk-go/service/s3"
    10  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
    11  	"github.com/Schaudge/grailbase/errors"
    12  	"github.com/Schaudge/grailbase/file"
    13  )
    14  
    15  // Stat implements file.Implementation interface.
    16  func (impl *s3Impl) Stat(ctx context.Context, path string, opts ...file.Opts) (file.Info, error) {
    17  	_, bucket, key, err := ParseURL(path)
    18  	if err != nil {
    19  		return nil, errors.E(errors.Invalid, "could not parse", path, err)
    20  	}
    21  	resp := runRequest(ctx, func() response {
    22  		clients, err := impl.clientsForAction(ctx, "GetObject", bucket, key)
    23  		if err != nil {
    24  			return response{err: err}
    25  		}
    26  		policy := newBackoffPolicy(clients, mergeFileOpts(opts))
    27  		info, err := stat(ctx, clients, policy, path, bucket, key)
    28  		if err != nil {
    29  			return response{err: err}
    30  		}
    31  		return response{info: info}
    32  	})
    33  	return resp.info, resp.err
    34  }
    35  
    36  func stat(ctx context.Context, clients []s3iface.S3API, policy retryPolicy, path, bucket, key string) (*s3Info, error) {
    37  	if key == "" {
    38  		return nil, errors.E(errors.Invalid, "cannot stat with empty S3 key", path)
    39  	}
    40  	metric := metrics.Op("stat").Start()
    41  	defer metric.Done()
    42  	for {
    43  		var ids s3RequestIDs
    44  		output, err := policy.client().HeadObjectWithContext(ctx,
    45  			&s3.HeadObjectInput{
    46  				Bucket: aws.String(bucket),
    47  				Key:    aws.String(key),
    48  			},
    49  			ids.captureOption(),
    50  		)
    51  		if policy.shouldRetry(ctx, err, path) {
    52  			metric.Retry()
    53  			continue
    54  		}
    55  		if err != nil {
    56  			return nil, annotate(err, ids, &policy, "s3file.stat", path)
    57  		}
    58  		if output.ETag == nil || *output.ETag == "" {
    59  			return nil, errors.E("s3file.stat: empty ETag", path, errors.NotExist, "awsrequestID:", ids.String())
    60  		}
    61  		if output.ContentLength == nil {
    62  			return nil, errors.E("s3file.stat: nil ContentLength", path, errors.NotExist, "awsrequestID:", ids.String())
    63  		}
    64  		if *output.ContentLength == 0 && strings.HasSuffix(path, "/") {
    65  			// Assume this is a directory marker:
    66  			// https://web.archive.org/web/20190424231712/https://docs.aws.amazon.com/AmazonS3/latest/user-guide/using-folders.html
    67  			return nil, errors.E("s3file.stat: directory marker at path", path, errors.NotExist, "awsrequestID:", ids.String())
    68  		}
    69  		if output.LastModified == nil {
    70  			return nil, errors.E("s3file.stat: nil LastModified", path, errors.NotExist, "awsrequestID:", ids.String())
    71  		}
    72  		return &s3Info{
    73  			name:    filepath.Base(path),
    74  			size:    *output.ContentLength,
    75  			modTime: *output.LastModified,
    76  			etag:    *output.ETag,
    77  		}, nil
    78  	}
    79  }