Files
Crussell/backend/internal/s3/s3_dev.go
T
popertots 9259de9393 Portfolio: add filtering, URL sharing, and improved tag input
- Add category filters with dynamic counts that reduce as filters
  applied
- Add ?filter[category]=value URL params for filterable links
- Add ?img= timestamp param that bypasses filters to show specific image
- Update URL when opening/navigating/closing modal for shareable links
- Backend: add /api/portfolio/filters endpoint with filter logic
- Backend: add timestamp lookup fallback for GetImage endpoint
  Frontend:
- Portfolio page: filter dropdowns, keyboard nav, mobile improvements
- ImageUpload: live tag suggestions from API, arrow/Tab navigation,
  confirmation modal before upload, mobile-optimized touch targets
- Add scrollbar-hide utility and fix filter dropdown overflow
- Move Clear all button, add vertical separator on desktop
2026-02-20 00:32:09 +00:00

171 lines
4.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) 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"
}
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)
}
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,
ContentType: aws.String("image/jpeg"),
})
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
}