Files
Tomas Dvorak 3cb40adb23 first commit
2026-04-10 12:04:09 +02:00

127 lines
3.1 KiB
Go

package filestorage
import (
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
)
type S3Storage struct {
client *s3.Client
bucket string
}
func NewS3FromEnv() (*S3Storage, error) {
bucket := strings.TrimSpace(os.Getenv("S3_BUCKET"))
accessKey := strings.TrimSpace(os.Getenv("S3_ACCESS_KEY"))
secretKey := strings.TrimSpace(os.Getenv("S3_SECRET_KEY"))
if bucket == "" || accessKey == "" || secretKey == "" {
return nil, errors.New("S3_BUCKET, S3_ACCESS_KEY, and S3_SECRET_KEY are required for FILE_STORAGE_PROVIDER=s3")
}
region := strings.TrimSpace(os.Getenv("S3_REGION"))
if region == "" {
region = "us-east-1"
}
endpoint := strings.TrimSpace(os.Getenv("S3_ENDPOINT"))
usePathStyle, _ := strconv.ParseBool(strings.TrimSpace(os.Getenv("S3_USE_PATH_STYLE")))
cfg := aws.Config{
Region: region,
Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
}
client := s3.NewFromConfig(cfg, func(options *s3.Options) {
options.UsePathStyle = usePathStyle
if endpoint != "" {
options.BaseEndpoint = aws.String(endpoint)
}
})
return &S3Storage{client: client, bucket: bucket}, nil
}
func (s *S3Storage) Provider() string {
return "s3"
}
func (s *S3Storage) Probe(ctx context.Context) error {
_, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(s.bucket)})
return err
}
func (s *S3Storage) Put(ctx context.Context, key string, reader io.Reader, contentType string, size int64) error {
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: reader,
ContentType: aws.String(contentType),
ContentLength: aws.Int64(size),
})
return err
}
func (s *S3Storage) Get(ctx context.Context, key string) (Object, error) {
response, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
if isS3NotFoundError(err) {
return Object{}, ErrNotFound
}
return Object{}, err
}
contentType := ""
if response.ContentType != nil {
contentType = *response.ContentType
}
size := int64(0)
if response.ContentLength != nil {
size = *response.ContentLength
}
return Object{
Body: response.Body,
ContentType: contentType,
Size: size,
}, nil
}
func (s *S3Storage) Delete(ctx context.Context, key string) error {
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
if isS3NotFoundError(err) {
return nil
}
return fmt.Errorf("delete s3 object: %w", err)
}
return nil
}
func isS3NotFoundError(err error) bool {
var noSuchKey *types.NoSuchKey
if errors.As(err, &noSuchKey) {
return true
}
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
switch apiErr.ErrorCode() {
case "NoSuchKey", "NotFound", "NoSuchBucket":
return true
}
}
return false
}