/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ /* Package s3 implements a very thin wrapper around the AWS S3 client. It only exists to enable stubbing of the AWS S3 client in tests. */ package s3 import ( "bytes" "context" "crypto/md5" "encoding/base64" "fmt" "time" "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" ) // Client is a wrapper around the AWS S3 client. type Client struct { s3client *s3.Client } // NewClient creates a new AWS S3 client. func NewClient(region string) (*Client, error) { // Use context.Background here because this context will not influence the later operations of the client. // The context given here is used for http requests that are made during client construction. // Client construction happens once during proxy setup. clientCfg, err := config.LoadDefaultConfig( context.Background(), config.WithRegion(region), ) if err != nil { return nil, fmt.Errorf("loading AWS S3 client config: %w", err) } client := s3.NewFromConfig(clientCfg) return &Client{client}, nil } // GetObject returns the object with the given key from the given bucket. // If a versionID is given, the specific version of the object is returned. func (c Client) GetObject(ctx context.Context, bucket, key, versionID, sseCustomerAlgorithm, sseCustomerKey, sseCustomerKeyMD5 string) (*s3.GetObjectOutput, error) { getObjectInput := &s3.GetObjectInput{ Bucket: &bucket, Key: &key, } if versionID != "" { getObjectInput.VersionId = &versionID } if sseCustomerAlgorithm != "" { getObjectInput.SSECustomerAlgorithm = &sseCustomerAlgorithm } if sseCustomerKey != "" { getObjectInput.SSECustomerKey = &sseCustomerKey } if sseCustomerKeyMD5 != "" { getObjectInput.SSECustomerKeyMD5 = &sseCustomerKeyMD5 } return c.s3client.GetObject(ctx, getObjectInput) } // PutObject creates a new object in the given bucket with the given key and body. // Various optional parameters can be set. func (c Client) PutObject(ctx context.Context, bucket, key, tags, contentType, objectLockLegalHoldStatus, objectLockMode, sseCustomerAlgorithm, sseCustomerKey, sseCustomerKeyMD5 string, objectLockRetainUntilDate time.Time, metadata map[string]string, body []byte) (*s3.PutObjectOutput, error) { // The AWS Go SDK has two versions. V1 does not set the Content-Type header. // V2 always sets the Content-Type header. We use V2. // The s3 API sets an object's content-type to binary/octet-stream if // it receives a request without a Content-Type header set. // Since a client using V1 may depend on the Content-Type binary/octet-stream // we have to explicitly emulate the S3 API behavior, if we receive a request // without a Content-Type. if contentType == "" { contentType = "binary/octet-stream" } contentMD5 := md5.Sum(body) encodedContentMD5 := base64.StdEncoding.EncodeToString(contentMD5[:]) putObjectInput := &s3.PutObjectInput{ Bucket: &bucket, Key: &key, Body: bytes.NewReader(body), Tagging: &tags, Metadata: metadata, ContentMD5: &encodedContentMD5, ContentType: &contentType, ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatus(objectLockLegalHoldStatus), } if sseCustomerAlgorithm != "" { putObjectInput.SSECustomerAlgorithm = &sseCustomerAlgorithm } if sseCustomerKey != "" { putObjectInput.SSECustomerKey = &sseCustomerKey } if sseCustomerKeyMD5 != "" { putObjectInput.SSECustomerKeyMD5 = &sseCustomerKeyMD5 } // It is not allowed to only set one of these two properties. if objectLockMode != "" && !objectLockRetainUntilDate.IsZero() { putObjectInput.ObjectLockMode = types.ObjectLockMode(objectLockMode) putObjectInput.ObjectLockRetainUntilDate = &objectLockRetainUntilDate } return c.s3client.PutObject(ctx, putObjectInput) }