mirror of
https://github.com/MizuchiLabs/mantrae.git
synced 2025-12-21 14:20:02 -06:00
183 lines
4.2 KiB
Go
183 lines
4.2 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"sort"
|
|
"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"
|
|
"github.com/mizuchilabs/mantrae/server/internal/settings"
|
|
)
|
|
|
|
type S3Config struct {
|
|
Bucket string
|
|
Region string
|
|
Endpoint string
|
|
AccessKey string
|
|
SecretKey string
|
|
UsePathStyle bool
|
|
}
|
|
|
|
// S3Storage implements StorageBackend for AWS S3
|
|
type S3Storage struct {
|
|
client *s3.Client
|
|
bucket string
|
|
}
|
|
|
|
func NewS3Storage(ctx context.Context, sm *settings.SettingsManager) (*S3Storage, error) {
|
|
cfgData, err := getSettings(sm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var opts []func(*config.LoadOptions) error
|
|
|
|
// Set region
|
|
if cfgData.Region != "" {
|
|
opts = append(opts, config.WithRegion(cfgData.Region))
|
|
}
|
|
|
|
// Set credentials
|
|
if cfgData.AccessKey != "" && cfgData.SecretKey != "" {
|
|
creds := credentials.NewStaticCredentialsProvider(cfgData.AccessKey, cfgData.SecretKey, "")
|
|
opts = append(opts, config.WithCredentialsProvider(creds))
|
|
}
|
|
|
|
awsCfg, err := config.LoadDefaultConfig(ctx, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build S3 options directly (the preferred modern way)
|
|
s3Options := s3.Options{
|
|
Region: awsCfg.Region,
|
|
Credentials: awsCfg.Credentials,
|
|
}
|
|
|
|
if cfgData.Endpoint != "" {
|
|
parsedURL, err := url.Parse(cfgData.Endpoint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid endpoint URL: %w", err)
|
|
}
|
|
|
|
s3Options.BaseEndpoint = aws.String(cfgData.Endpoint)
|
|
s3Options.EndpointOptions = s3.EndpointResolverOptions{
|
|
DisableHTTPS: parsedURL.Scheme == "http",
|
|
}
|
|
}
|
|
if cfgData.UsePathStyle {
|
|
s3Options.UsePathStyle = true
|
|
}
|
|
|
|
return &S3Storage{
|
|
bucket: cfgData.Bucket,
|
|
client: s3.New(s3Options),
|
|
}, nil
|
|
}
|
|
|
|
func (s *S3Storage) Store(ctx context.Context, name string, data io.Reader) error {
|
|
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(name),
|
|
Body: data,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store object in S3: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *S3Storage) Retrieve(ctx context.Context, name string) (io.ReadCloser, error) {
|
|
output, err := s.client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(name),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve object from S3: %w", err)
|
|
}
|
|
return output.Body, nil
|
|
}
|
|
|
|
// List lists the files in S3
|
|
func (s *S3Storage) List(ctx context.Context) ([]StoredFile, error) {
|
|
var files []StoredFile
|
|
var nextToken *string
|
|
|
|
for {
|
|
output, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
|
Bucket: aws.String(s.bucket),
|
|
ContinuationToken: nextToken,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list objects from S3: %w", err)
|
|
}
|
|
|
|
for _, object := range output.Contents {
|
|
// Safely dereference pointers.
|
|
key := ""
|
|
if object.Key != nil {
|
|
key = *object.Key
|
|
}
|
|
var ts time.Time
|
|
if object.LastModified != nil {
|
|
ts = *object.LastModified
|
|
}
|
|
files = append(files, StoredFile{
|
|
Name: key,
|
|
Timestamp: &ts,
|
|
Size: *object.Size,
|
|
})
|
|
}
|
|
|
|
nextToken = output.NextContinuationToken
|
|
|
|
if nextToken == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].Timestamp.After(*files[j].Timestamp)
|
|
})
|
|
|
|
return files, nil
|
|
}
|
|
|
|
func (s *S3Storage) Delete(ctx context.Context, name string) error {
|
|
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(name),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete object from S3: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getSettings(sm *settings.SettingsManager) (*S3Config, error) {
|
|
sets := sm.GetMany(context.Background(), []string{
|
|
settings.KeyS3Region,
|
|
settings.KeyS3Endpoint,
|
|
settings.KeyS3Bucket,
|
|
settings.KeyS3AccessKey,
|
|
settings.KeyS3SecretKey,
|
|
settings.KeyS3UsePathStyle,
|
|
})
|
|
|
|
cfg := S3Config{
|
|
Region: sets[settings.KeyS3Region],
|
|
Endpoint: sets[settings.KeyS3Endpoint],
|
|
Bucket: sets[settings.KeyS3Bucket],
|
|
AccessKey: sets[settings.KeyS3AccessKey],
|
|
SecretKey: sets[settings.KeyS3SecretKey],
|
|
UsePathStyle: settings.AsBool(sets[settings.KeyS3UsePathStyle]),
|
|
}
|
|
return &cfg, nil
|
|
}
|