- Add s3_storage.go service using AWS SDK v2 - Support custom S3-compatible endpoints (Civo) - Implement pre-signed URL generation for uploads/downloads - Add storage_handler.go with REST endpoints - Register protected storage routes in router - Graceful degradation when S3 not configured
139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
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
|
|
}
|