package storage import ( "context" "fmt" "log" "photum-backend/internal/config" "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" ) type S3Service struct { Client *s3.Client PresignClient *s3.PresignClient Bucket string Region string } func NewS3Service(cfg *config.Config) *S3Service { // Custom Resolver for Civo Object Store customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { return aws.Endpoint{ URL: cfg.S3Endpoint, SigningRegion: region, }, nil }) awsCfg, err := awsConfig.LoadDefaultConfig(context.TODO(), awsConfig.WithRegion(cfg.S3Region), awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.S3AccessKey, cfg.S3SecretKey, "")), awsConfig.WithEndpointResolverWithOptions(customResolver), ) if err != nil { log.Fatalf("unable to load SDK config, %v", err) } client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { o.UsePathStyle = true }) presignClient := s3.NewPresignClient(client) return &S3Service{ Client: client, PresignClient: presignClient, Bucket: cfg.S3Bucket, Region: cfg.S3Region, } } // GeneratePresignedURL generates a PUT presigned URL for uploading a file // returns (uploadUrl, publicUrl, error) func (s *S3Service) GeneratePresignedURL(filename string, contentType string) (string, string, error) { key := fmt.Sprintf("photum-dev/%d_%s", time.Now().Unix(), filename) req, err := s.PresignClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{ Bucket: aws.String(s.Bucket), Key: aws.String(key), ContentType: aws.String(contentType), }, s3.WithPresignExpires(15*time.Minute)) if err != nil { return "", "", fmt.Errorf("failed to sign request: %v", err) } // Construct public URL - Path Style // URL: https://objectstore.nyc1.civo.com/rede5/uploads/... // We need to clean the endpoint string if it has https:// prefix for Sprintf if we construct manually, // or just reuse the known endpoint structure. // cfg.S3Endpoint is "https://objectstore.nyc1.civo.com" // Assuming s.Client.Options().BaseEndpoint is not easily accessible here without plumbing, // we will construct it based on the hardcoded knowledge of Civo or pass endpoint to struct. // But simply: S3Endpoint + "/" + Bucket + "/" + key is the standard path style. // Note: config.S3Endpoint includes "https://" based on .env // We entered: S3_ENDPOINT=https://objectstore.nyc1.civo.com // So: https://objectstore.nyc1.civo.com/rede5/key // However, we don't have access to cfg here directly, but we rely on hardcoding for Civo in previous step or we should store Endpoint in struct. // Better to store Endpoint in struct to be clean. // For now, I'll use the domain directly as I did before, but path style. publicURL := fmt.Sprintf("https://%s/%s/%s", "objectstore.nyc1.civo.com", s.Bucket, key) return req.URL, publicURL, nil }