package media import ( "context" "errors" "fmt" "io" "strings" awsconfig "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" "github.com/aws/smithy-go" ) // S3Config holds the settings needed to talk to any S3-compatible bucket // (AWS S3 itself, MinIO, Cloudflare R2, ...). type S3Config struct { Bucket string Region string Endpoint string // leave empty for real AWS S3 AccessKeyID string SecretKey string UsePathStyle bool PublicBaseURL string // e.g. https://cdn.example.com or https://bucket.s3.region.amazonaws.com } type S3Storage struct { client *s3.Client bucket string baseURL string } func NewS3Storage(ctx context.Context, cfg S3Config) (*S3Storage, error) { awsCfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(cfg.Region), awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretKey, "")), ) if err != nil { return nil, fmt.Errorf("load aws config: %w", err) } client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { if cfg.Endpoint != "" { o.BaseEndpoint = &cfg.Endpoint } o.UsePathStyle = cfg.UsePathStyle }) baseURL := cfg.PublicBaseURL if baseURL == "" { if cfg.Endpoint != "" { baseURL = strings.TrimRight(cfg.Endpoint, "/") + "/" + cfg.Bucket } else { baseURL = fmt.Sprintf("https://%s.s3.%s.amazonaws.com", cfg.Bucket, cfg.Region) } } return &S3Storage{client: client, bucket: cfg.Bucket, baseURL: strings.TrimRight(baseURL, "/")}, nil } func (s *S3Storage) Save(ctx context.Context, key string, reader io.Reader, size int64, contentType string) (string, error) { _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: &s.bucket, Key: &key, Body: reader, ContentType: &contentType, ContentLength: &size, }) if err != nil { return "", fmt.Errorf("s3 put object: %w", err) } return s.baseURL + "/" + key, nil } func (s *S3Storage) Delete(ctx context.Context, key string) error { _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: &s.bucket, Key: &key, }) if err != nil { var apiErr smithy.APIError if errors.As(err, &apiErr) && apiErr.ErrorCode() == "NoSuchKey" { return nil } return fmt.Errorf("s3 delete object: %w", err) } return nil }