constellation/s3proxy/internal/s3/s3.go

117 lines
3.9 KiB
Go
Raw Normal View History

/*
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)
}