package storage import ( "context" "fmt" "io" "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" ) type S3Client struct { s3 *s3.Client presign *s3.PresignClient bucket string publicURL string } type S3Config struct { Endpoint string AccessKey string SecretKey string Bucket string PublicURL string Region string } func NewS3Client(cfg S3Config) (*S3Client, error) { awsCfg, err := config.LoadDefaultConfig(context.Background(), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, "")), config.WithRegion(cfg.Region), ) if err != nil { return nil, err } client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { if cfg.Endpoint != "" { o.BaseEndpoint = aws.String(cfg.Endpoint) } }) return &S3Client{ s3: client, presign: s3.NewPresignClient(client), bucket: cfg.Bucket, publicURL: cfg.PublicURL, }, nil } func NewR2Client() (*S3Client, error) { accountID := os.Getenv("R2_ACCOUNT_ID") accessKey := os.Getenv("R2_ACCESS_KEY_ID") secretKey := os.Getenv("R2_SECRET_ACCESS_KEY") bucket := os.Getenv("R2_BUCKET") publicURL := os.Getenv("R2_PUBLIC_URL") if accountID == "" || accessKey == "" || secretKey == "" { return nil, fmt.Errorf("R2 credentials not configured") } endpoint := os.Getenv("R2_ENDPOINT") if endpoint == "" { endpoint = fmt.Sprintf("https://%s.r2.cloudflarestorage.com", accountID) } return NewS3Client(S3Config{ Endpoint: endpoint, AccessKey: accessKey, SecretKey: secretKey, Bucket: bucket, PublicURL: publicURL, Region: "auto", }) } func (c *S3Client) Upload(ctx context.Context, key string, body io.Reader, contentType string) error { _, err := c.s3.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(key), Body: body, ContentType: aws.String(contentType), }) return err } func (c *S3Client) Delete(ctx context.Context, key string) error { _, err := c.s3.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(key), }) return err } func (c *S3Client) PresignUpload(ctx context.Context, key string, contentType string, expires time.Duration) (string, error) { req, err := c.presign.PresignPutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(key), ContentType: aws.String(contentType), }, s3.WithPresignExpires(expires)) if err != nil { return "", err } return req.URL, nil } func (c *S3Client) PublicURL(key string) string { return fmt.Sprintf("%s/%s", c.publicURL, key) }