feat(storage): add S3/R2 abstraction with local Rustfs dev
- Add backend/internal/s3/ with build-tag pattern (dev vs prod) - Dev: Uses local Rustfs container (S3-compatible) - Prod: Stub for R2 Cloudflare (add AWS SDK to implement) - Add S3 env vars to .env.example and .env - Add Rustfs service to compose.yml - Add Rustfs reset to local-dev-2.sh (wipes data on each run)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
//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) 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 {
|
||||
endpoint := os.Getenv("S3_ENDPOINT")
|
||||
if endpoint == "" {
|
||||
endpoint = "http://localhost:9000"
|
||||
}
|
||||
|
||||
accessKey := os.Getenv("S3_ACCESS_KEY")
|
||||
if accessKey == "" {
|
||||
accessKey = "minioadmin"
|
||||
}
|
||||
|
||||
secretKey := os.Getenv("S3_SECRET_KEY")
|
||||
if secretKey == "" {
|
||||
secretKey = "minioadmin"
|
||||
}
|
||||
|
||||
bucket := os.Getenv("S3_BUCKET")
|
||||
if bucket == "" {
|
||||
bucket = "crussell"
|
||||
}
|
||||
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
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: endpoint,
|
||||
}
|
||||
|
||||
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) error {
|
||||
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: body,
|
||||
})
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user