package storage import ( "context" "fmt" "os" "time" "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" ) // S3Storage handles S3-compatible object storage operations type S3Storage struct { client *s3.Client presigner *s3.PresignClient bucket string endpoint string } // NewS3Storage creates a new S3 storage service func NewS3Storage() (*S3Storage, error) { region := os.Getenv("AWS_REGION") if region == "" { region = "us-east-1" } accessKey := os.Getenv("AWS_ACCESS_KEY_ID") secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") endpoint := os.Getenv("AWS_ENDPOINT") bucket := os.Getenv("S3_BUCKET") if accessKey == "" || secretKey == "" || bucket == "" { return nil, fmt.Errorf("missing required S3 configuration (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET)") } // Create custom credentials provider creds := credentials.NewStaticCredentialsProvider(accessKey, secretKey, "") // Build S3 config cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region), config.WithCredentialsProvider(creds), ) if err != nil { return nil, fmt.Errorf("failed to load AWS config: %w", err) } // Create S3 client with custom endpoint for S3-compatible storage (like Civo) var client *s3.Client if endpoint != "" { client = s3.NewFromConfig(cfg, func(o *s3.Options) { o.BaseEndpoint = aws.String(endpoint) o.UsePathStyle = true // Required for most S3-compatible services }) } else { client = s3.NewFromConfig(cfg) } presigner := s3.NewPresignClient(client) return &S3Storage{ client: client, presigner: presigner, bucket: bucket, endpoint: endpoint, }, nil } // GenerateUploadURL generates a pre-signed URL for uploading a file func (s *S3Storage) GenerateUploadURL(key string, contentType string, expiryMinutes int) (string, error) { if expiryMinutes <= 0 { expiryMinutes = 15 // Default 15 minutes } input := &s3.PutObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), ContentType: aws.String(contentType), } presignResult, err := s.presigner.PresignPutObject(context.Background(), input, s3.WithPresignExpires(time.Duration(expiryMinutes)*time.Minute)) if err != nil { return "", fmt.Errorf("failed to generate upload URL: %w", err) } return presignResult.URL, nil } // GenerateDownloadURL generates a pre-signed URL for downloading a file func (s *S3Storage) GenerateDownloadURL(key string, expiryMinutes int) (string, error) { if expiryMinutes <= 0 { expiryMinutes = 60 // Default 1 hour } input := &s3.GetObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), } presignResult, err := s.presigner.PresignGetObject(context.Background(), input, s3.WithPresignExpires(time.Duration(expiryMinutes)*time.Minute)) if err != nil { return "", fmt.Errorf("failed to generate download URL: %w", err) } return presignResult.URL, nil } // DeleteObject deletes an object from the bucket func (s *S3Storage) DeleteObject(key string) error { input := &s3.DeleteObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), } _, err := s.client.DeleteObject(context.Background(), input) if err != nil { return fmt.Errorf("failed to delete object: %w", err) } return nil } // GetPublicURL returns the public URL for an object (if bucket is public) func (s *S3Storage) GetPublicURL(key string) string { if s.endpoint != "" { return fmt.Sprintf("%s/%s/%s", s.endpoint, s.bucket, key) } return fmt.Sprintf("https://%s.s3.amazonaws.com/%s", s.bucket, key) } // GetBucket returns the bucket name func (s *S3Storage) GetBucket() string { return s.bucket }