aenvs/internal/storage/s3.go

147 lines
3.8 KiB
Go

package storage
import (
"context"
"errors"
"io"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/user/aevs/internal/config"
)
// S3Storage implements Storage interface using AWS S3 (or compatible services)
type S3Storage struct {
client *s3.Client
bucket string
}
// NewS3Storage creates a new S3 storage instance with custom endpoint support
func NewS3Storage(cfg *config.StorageConfig) (*S3Storage, error) {
// Create AWS config with static credentials
awsCfg := aws.Config{
Region: cfg.Region,
Credentials: credentials.NewStaticCredentialsProvider(
cfg.AccessKey,
cfg.SecretKey,
"",
),
}
// Create S3 client with custom endpoint for MinIO, R2, Spaces, etc.
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
if cfg.Endpoint != "" && cfg.Endpoint != config.DefaultEndpoint {
o.BaseEndpoint = aws.String(cfg.Endpoint)
o.UsePathStyle = true // Required for MinIO and some S3 alternatives
}
})
return &S3Storage{
client: client,
bucket: cfg.Bucket,
}, nil
}
// Upload uploads data to S3
func (s *S3Storage) Upload(ctx context.Context, key string, data io.Reader, size int64) error {
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: data,
ContentLength: aws.Int64(size),
})
return err
}
// Download downloads data from S3
func (s *S3Storage) Download(ctx context.Context, key string) (io.ReadCloser, error) {
result, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
return nil, err
}
return result.Body, nil
}
// Delete deletes an object from S3
func (s *S3Storage) Delete(ctx context.Context, key string) error {
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
return err
}
// Exists checks if an object exists in S3
func (s *S3Storage) Exists(ctx context.Context, key string) (bool, error) {
_, err := s.client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
// Check if it's a "not found" error
var notFound *types.NotFound
var noSuchKey *types.NoSuchKey
if errors.As(err, &notFound) || errors.As(err, &noSuchKey) {
return false, nil
}
return false, err
}
return true, nil
}
// List lists all objects with the given prefix
func (s *S3Storage) List(ctx context.Context, prefix string) ([]string, error) {
result, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(s.bucket),
Prefix: aws.String(prefix),
})
if err != nil {
return nil, err
}
var keys []string
for _, obj := range result.Contents {
if obj.Key != nil {
keys = append(keys, *obj.Key)
}
}
return keys, nil
}
// ListProjects returns all project directories (using delimiter)
func (s *S3Storage) ListProjects(ctx context.Context) ([]string, error) {
result, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(s.bucket),
Delimiter: aws.String("/"),
})
if err != nil {
return nil, err
}
var projects []string
for _, prefix := range result.CommonPrefixes {
if prefix.Prefix != nil {
// Remove trailing slash
projectName := strings.TrimSuffix(*prefix.Prefix, "/")
projects = append(projects, projectName)
}
}
return projects, nil
}
// TestConnection tests the connection to S3 by attempting to list buckets
func (s *S3Storage) TestConnection(ctx context.Context) error {
// Try to head the bucket to verify access
_, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{
Bucket: aws.String(s.bucket),
})
return err
}