Files
Crussell/backend/internal/s3/s3_dev.go
T
popertotsandSisyphus db78b5186e chore: update S3 dev configuration with AWS SDK v2
S3 dev implementation now uses AWS SDK v2 for RustFS local storage. Prod stub remains placeholder. Both implement Uploader interface with Upload, Download, Delete, GetURL methods.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-04 01:09:15 +01:00

205 lines
5.0 KiB
Go

//go:build dev
// +build dev
package s3
import (
"context"
"fmt"
"io"
"log"
"os"
"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"
)
var Client Uploader
type Uploader interface {
Upload(ctx context.Context, bucket, key string, body io.Reader, contentType string) error
Download(ctx context.Context, bucket, key string, w io.Writer) error
Delete(ctx context.Context, bucket, key string) error
GetURL(ctx context.Context, bucket, key string) (string, error)
}
type S3Client struct {
client *s3.Client
bucket string
publicURL string
}
func Connect() error {
// Check for RUSTFS_* vars first (matching compose.yml), fall back to S3_* vars
endpoint := os.Getenv("RUSTFS_ENDPOINT")
if endpoint == "" {
endpoint = os.Getenv("S3_ENDPOINT")
}
if endpoint == "" {
// Default: localhost for bare metal dev, use rustfs:9000 for docker
endpoint = "http://localhost:9000"
}
accessKey := os.Getenv("RUSTFS_ACCESS_KEY")
if accessKey == "" {
accessKey = os.Getenv("S3_ACCESS_KEY")
}
if accessKey == "" {
accessKey = "minioadmin"
}
secretKey := os.Getenv("RUSTFS_SECRET_KEY")
if secretKey == "" {
secretKey = os.Getenv("S3_SECRET_KEY")
}
if secretKey == "" {
secretKey = "minioadmin"
}
bucket := os.Getenv("RUSTFS_BUCKET")
if bucket == "" {
bucket = os.Getenv("S3_BUCKET")
}
if bucket == "" {
bucket = "crussell"
}
profilePicsBucket := os.Getenv("S3_PROFILE_PICS_BUCKET")
if profilePicsBucket == "" {
profilePicsBucket = "crussell-profile-pics"
}
region := os.Getenv("AWS_REGION")
if region == "" {
region = "eu-west-2"
}
publicURL := os.Getenv("S3_PUBLIC_URL")
if publicURL == "" {
publicURL = endpoint
}
awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(),
awsconfig.WithRegion(region),
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
accessKey,
secretKey,
"",
)),
)
if err != nil {
return fmt.Errorf("failed to load AWS config: %w", err)
}
Client = &S3Client{
client: s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(endpoint)
o.UsePathStyle = true
}),
bucket: bucket,
publicURL: publicURL,
}
// Create bucket if it doesn't exist
ctx := context.Background()
s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(endpoint)
o.UsePathStyle = true
})
_, err = s3Client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: aws.String(bucket),
})
if err != nil {
log.Printf("Bucket creation: %v (may already exist)", err)
}
// Set bucket policy for public read access
policy := fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::%s/*"
}]
}`, bucket)
_, err = s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: aws.String(bucket),
Policy: aws.String(policy),
})
if err != nil {
log.Printf("Bucket policy: %v (may already exist)", err)
}
// Create profile pics bucket if it doesn't exist
if profilePicsBucket != bucket {
_, err = s3Client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: aws.String(profilePicsBucket),
})
if err != nil {
log.Printf("Profile pics bucket creation: %v (may already exist)", err)
}
// Set bucket policy for public read access
profilePolicy := fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::%s/*"
}]
}`, profilePicsBucket)
_, err = s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: aws.String(profilePicsBucket),
Policy: aws.String(profilePolicy),
})
if err != nil {
log.Printf("Profile pics bucket policy: %v (may already exist)", err)
}
}
log.Printf("Connected to local S3 (Rustfs): bucket=%s, endpoint=%s", bucket, endpoint)
return nil
}
func (s *S3Client) Upload(ctx context.Context, bucket, key string, body io.Reader, contentType string) error {
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: body,
ContentType: aws.String(contentType),
})
return err
}
func (s *S3Client) Download(ctx context.Context, bucket, key string, w io.Writer) error {
result, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
if err != nil {
return err
}
defer result.Body.Close()
_, err = io.Copy(w, result.Body)
return err
}
func (s *S3Client) Delete(ctx context.Context, bucket, key string) error {
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
return err
}
func (s *S3Client) GetURL(ctx context.Context, bucket, key string) (string, error) {
return fmt.Sprintf("%s/%s/%s", s.publicURL, bucket, key), nil
}