gohorsejobs/backend/internal/services/storage_service.go
2026-01-03 20:21:29 -03:00

157 lines
4.4 KiB
Go

package services
import (
"context"
"encoding/json"
"fmt"
"strings"
"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 StorageService struct {
credentialsService *CredentialsService
}
func NewStorageService(cs *CredentialsService) *StorageService {
return &StorageService{credentialsService: cs}
}
// UploadConfig holds the necessary keys.
type UploadConfig struct {
Endpoint string `json:"endpoint"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Bucket string `json:"bucket"`
Region string `json:"region"`
}
func (s *StorageService) getConfig(ctx context.Context) (UploadConfig, error) {
payload, err := s.credentialsService.GetDecryptedKey(ctx, "storage")
if err != nil {
return UploadConfig{}, fmt.Errorf("failed to get storage credentials: %w", err)
}
var uCfg UploadConfig
if err := json.Unmarshal([]byte(payload), &uCfg); err != nil {
return UploadConfig{}, fmt.Errorf("failed to parse storage credentials: %w", err)
}
if uCfg.Endpoint == "" || uCfg.AccessKey == "" || uCfg.SecretKey == "" || uCfg.Bucket == "" {
return UploadConfig{}, fmt.Errorf("storage credentials incomplete (all fields required)")
}
if uCfg.Region == "" {
uCfg.Region = "auto"
}
return uCfg, nil
}
func (s *StorageService) getClient(ctx context.Context) (*s3.PresignClient, string, error) {
uCfg, err := s.getConfig(ctx)
if err != nil {
return nil, "", err
}
// 2. Setup S3 V2 Client
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(uCfg.Region),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(uCfg.AccessKey, uCfg.SecretKey, "")),
)
if err != nil {
return nil, "", err
}
// R2/S3 specific endpoint
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(uCfg.Endpoint)
o.UsePathStyle = true // Often needed for R2/MinIO
})
psClient := s3.NewPresignClient(client)
return psClient, uCfg.Bucket, nil
}
// GetPresignedUploadURL generates a URL for PUT requests
func (s *StorageService) GetPresignedUploadURL(ctx context.Context, key string, contentType string) (string, error) {
psClient, bucket, err := s.getClient(ctx)
if err != nil {
return "", err
}
req, err := psClient.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ContentType: aws.String(contentType),
}, func(o *s3.PresignOptions) {
o.Expires = 15 * time.Minute
})
if err != nil {
return "", fmt.Errorf("failed to presign upload: %w", err)
}
return req.URL, nil
}
// TestConnection checks if the creds are valid and bucket is accessible
func (s *StorageService) TestConnection(ctx context.Context) error {
psClient, bucket, err := s.getClient(ctx)
if err != nil {
return fmt.Errorf("failed to get client: %w", err)
}
// Note: PresignClient doesn't strictly validate creds against the cloud until used,
// checking existence via HeadBucket or ListBuckets using a real S3 client would be better.
// But getClient returns a PresignClient.
// We need a standard client to Verify.
// Re-instantiating logic or Refactoring `getClient` to return `*s3.Client` is best.
// For now, let's refactor `getClient` slightly to expose specific logic or just create a one-off checker here.
// Refetch raw creds to make a standard client
uCfg, err := s.getConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get storage credentials: %w", err)
}
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(uCfg.Region),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(uCfg.AccessKey, uCfg.SecretKey, "")),
)
if err != nil {
return err
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(uCfg.Endpoint)
o.UsePathStyle = true
})
// Try HeadBucket
_, err = client.HeadBucket(ctx, &s3.HeadBucketInput{
Bucket: aws.String(uCfg.Bucket),
})
if err != nil {
return fmt.Errorf("connection failed: %w", err)
}
// Just to be sure, presign client creation (original logic)
_ = psClient
_ = bucket
return nil
}
func (s *StorageService) GetPublicURL(ctx context.Context, key string) (string, error) {
uCfg, err := s.getConfig(ctx)
if err != nil {
return "", err
}
endpoint := strings.TrimRight(uCfg.Endpoint, "/")
return fmt.Sprintf("%s/%s/%s", endpoint, uCfg.Bucket, key), nil
}