package storage import ( "context" "errors" "fmt" "io" "strings" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" ) type S3 struct { bucket string prefix string client *s3.Client } func NewS3(ctx context.Context, bucket, prefix string) (*S3, error) { if bucket == "" { return nil, fmt.Errorf("storage: s3 backend requires a bucket") } sdkConfig, err := awsconfig.LoadDefaultConfig(ctx) if err != nil { return nil, fmt.Errorf("storage: load s3 config: %w", err) } client := s3.NewFromConfig(sdkConfig) versioning, err := client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{ Bucket: &bucket, }) if err != nil { return nil, fmt.Errorf("storage: check s3 bucket versioning: %w", err) } if versioning.Status != "" { return nil, fmt.Errorf("storage: s3 cache bucket must not use versioning") } return &S3{ bucket: bucket, prefix: strings.Trim(prefix, "/"), client: client, }, nil } func (s *S3) fullKey(key string) (string, error) { if err := ValidateKey(key); err != nil { return "", err } if s.prefix == "" { return key, nil } return s.prefix + "/" + key, nil } func (s *S3) Get(ctx context.Context, key string) (io.ReadCloser, error) { full, err := s.fullKey(key) if err != nil { return nil, err } res, err := s.client.GetObject(ctx, &s3.GetObjectInput{ Bucket: &s.bucket, Key: &full, }) if err != nil { var nsk *types.NoSuchKey if errors.As(err, &nsk) { return nil, ErrNotExist } return nil, fmt.Errorf("storage: get %q: %w", key, err) } return res.Body, nil } func (s *S3) Put(ctx context.Context, key string, r io.Reader) error { full, err := s.fullKey(key) if err != nil { return err } if _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: &s.bucket, Key: &full, Body: r, }); err != nil { return fmt.Errorf("storage: put %q: %w", key, err) } return nil } func (s *S3) Delete(ctx context.Context, key string) error { full, err := s.fullKey(key) if err != nil { return err } if _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: &s.bucket, Key: &full, }); err != nil { return fmt.Errorf("storage: delete %q: %w", key, err) } return nil }