- Add gofakes3 for in-process S3-compatible test server - Create test server that runs on localhost:9999 with temp directory - Implement basic S3 client wrapper with standard operations - Add comprehensive tests for blob and metadata storage patterns - Test cleanup properly removes temporary directories - Use AWS SDK v2 for S3 operations with proper error handling
141 lines
3.2 KiB
Go
141 lines
3.2 KiB
Go
package s3
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
)
|
|
|
|
// Client wraps the AWS S3 client for vaultik operations
|
|
type Client struct {
|
|
s3Client *s3.Client
|
|
bucket string
|
|
prefix string
|
|
}
|
|
|
|
// Config contains S3 client configuration
|
|
type Config struct {
|
|
Endpoint string
|
|
Bucket string
|
|
Prefix string
|
|
AccessKeyID string
|
|
SecretAccessKey string
|
|
Region string
|
|
}
|
|
|
|
// NewClient creates a new S3 client
|
|
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
|
|
// Create AWS config
|
|
awsCfg, err := config.LoadDefaultConfig(ctx,
|
|
config.WithRegion(cfg.Region),
|
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
|
cfg.AccessKeyID,
|
|
cfg.SecretAccessKey,
|
|
"",
|
|
)),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Configure custom endpoint if provided
|
|
s3Opts := func(o *s3.Options) {
|
|
if cfg.Endpoint != "" {
|
|
o.BaseEndpoint = aws.String(cfg.Endpoint)
|
|
o.UsePathStyle = true
|
|
}
|
|
}
|
|
|
|
s3Client := s3.NewFromConfig(awsCfg, s3Opts)
|
|
|
|
return &Client{
|
|
s3Client: s3Client,
|
|
bucket: cfg.Bucket,
|
|
prefix: cfg.Prefix,
|
|
}, nil
|
|
}
|
|
|
|
// PutObject uploads an object to S3
|
|
func (c *Client) PutObject(ctx context.Context, key string, data io.Reader) error {
|
|
fullKey := c.prefix + key
|
|
_, err := c.s3Client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(c.bucket),
|
|
Key: aws.String(fullKey),
|
|
Body: data,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// GetObject downloads an object from S3
|
|
func (c *Client) GetObject(ctx context.Context, key string) (io.ReadCloser, error) {
|
|
fullKey := c.prefix + key
|
|
result, err := c.s3Client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: aws.String(c.bucket),
|
|
Key: aws.String(fullKey),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result.Body, nil
|
|
}
|
|
|
|
// DeleteObject removes an object from S3
|
|
func (c *Client) DeleteObject(ctx context.Context, key string) error {
|
|
fullKey := c.prefix + key
|
|
_, err := c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
Bucket: aws.String(c.bucket),
|
|
Key: aws.String(fullKey),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// ListObjects lists objects with the given prefix
|
|
func (c *Client) ListObjects(ctx context.Context, prefix string) ([]string, error) {
|
|
fullPrefix := c.prefix + prefix
|
|
|
|
var keys []string
|
|
paginator := s3.NewListObjectsV2Paginator(c.s3Client, &s3.ListObjectsV2Input{
|
|
Bucket: aws.String(c.bucket),
|
|
Prefix: aws.String(fullPrefix),
|
|
})
|
|
|
|
for paginator.HasMorePages() {
|
|
page, err := paginator.NextPage(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, obj := range page.Contents {
|
|
if obj.Key != nil {
|
|
// Remove the base prefix from the key
|
|
key := *obj.Key
|
|
if len(key) > len(c.prefix) {
|
|
key = key[len(c.prefix):]
|
|
}
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
// HeadObject checks if an object exists
|
|
func (c *Client) HeadObject(ctx context.Context, key string) (bool, error) {
|
|
fullKey := c.prefix + key
|
|
_, err := c.s3Client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
Bucket: aws.String(c.bucket),
|
|
Key: aws.String(fullKey),
|
|
})
|
|
if err != nil {
|
|
// Check if it's a not found error
|
|
// TODO: Add proper error type checking
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|