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, ¬Found) || 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 }