package services import ( "context" "encoding/json" "fmt" "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) getClient(ctx context.Context) (*s3.PresignClient, string, error) { // 1. Fetch Credentials from DB (Encrypted Payload) payload, err := s.credentialsService.GetDecryptedKey(ctx, "storage") if err != nil { return nil, "", fmt.Errorf("failed to get storage credentials: %w", err) } var uCfg UploadConfig if err := json.Unmarshal([]byte(payload), &uCfg); err != nil { return nil, "", fmt.Errorf("failed to parse storage credentials: %w", err) } if uCfg.Endpoint == "" || uCfg.AccessKey == "" || uCfg.SecretKey == "" || uCfg.Bucket == "" { return nil, "", fmt.Errorf("storage credentials incomplete (all fields required)") } if uCfg.Region == "" { uCfg.Region = "auto" } // 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 payload, err := s.credentialsService.GetDecryptedKey(ctx, "storage") if err != nil { return fmt.Errorf("failed to get storage credentials: %w", err) } var uCfg UploadConfig if err := json.Unmarshal([]byte(payload), &uCfg); err != nil { return fmt.Errorf("failed to parse 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 }