package storage import ( "context" "fmt" "io" "time" "github.com/aws/aws-sdk-go-v2/aws" 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" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/config" ) // s3Store targets AWS S3 or path-style compatibles (Garage/MinIO), same as // the backend's S3 backend. The bucket is never public: downloads use // short-lived presigned GETs so visibility rules keep holding on S3. type s3Store struct { client *s3.Client presign *s3.PresignClient bucket string } func newS3(cfg config.Config) (*s3Store, error) { awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(), awsconfig.WithRegion(cfg.S3Region), awsconfig.WithCredentialsProvider( credentials.NewStaticCredentialsProvider(cfg.S3AccessKeyID, cfg.S3SecretKey, "")), ) if err != nil { return nil, fmt.Errorf("s3 config: %w", err) } client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { o.BaseEndpoint = aws.String(cfg.S3Endpoint) o.UsePathStyle = cfg.S3ForcePathStyle }) return &s3Store{ client: client, presign: s3.NewPresignClient(client), bucket: cfg.S3Bucket, }, nil } func (s *s3Store) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error { _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), Body: r, ContentLength: aws.Int64(size), ContentType: aws.String(contentType), }) return err } func (s *s3Store) Open(ctx context.Context, key string) (io.ReadCloser, int64, error) { out, err := s.client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), }) if err != nil { return nil, 0, err } return out.Body, aws.ToInt64(out.ContentLength), nil } func (s *s3Store) 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 } func (s *s3Store) PresignGet(ctx context.Context, key, downloadFilename, contentType string, expiry time.Duration) (string, error) { in := &s3.GetObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), } if contentType != "" { in.ResponseContentType = aws.String(contentType) } if downloadFilename != "" { in.ResponseContentDisposition = aws.String(fmt.Sprintf("attachment; filename=%q", downloadFilename)) } req, err := s.presign.PresignGetObject(ctx, in, s3.WithPresignExpires(expiry)) if err != nil { return "", err } return req.URL, nil }