package services import ( "bytes" "context" "fmt" "io" "net/url" "strings" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) type s3StorageBackend struct { cfg StorageBackendConfig client *minio.Client } func newS3StorageBackend(cfg StorageBackendConfig) (*s3StorageBackend, error) { endpoint := normalizeS3Endpoint(cfg.Endpoint) client, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), Secure: cfg.UseSSL, Region: cfg.Region, BucketLookup: s3BucketLookup(cfg.PathStyle), }) if err != nil { return nil, err } return &s3StorageBackend{cfg: cfg, client: client}, nil } func (b *s3StorageBackend) ID() string { return b.cfg.ID } func (b *s3StorageBackend) Type() string { return StorageBackendS3 } func (b *s3StorageBackend) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) error { opts := minio.PutObjectOptions{ContentType: contentType} _, err := b.client.PutObject(ctx, b.cfg.Bucket, cleanObjectKey(key), body, size, opts) return err } func (b *s3StorageBackend) Get(ctx context.Context, key string) (StorageObject, error) { object, err := b.client.GetObject(ctx, b.cfg.Bucket, cleanObjectKey(key), minio.GetObjectOptions{}) if err != nil { return StorageObject{}, err } info, err := object.Stat() if err != nil { object.Close() return StorageObject{}, err } return StorageObject{Key: key, Size: info.Size, ContentType: info.ContentType, ModTime: info.LastModified, Body: object}, nil } func (b *s3StorageBackend) Delete(ctx context.Context, key string) error { return b.client.RemoveObject(ctx, b.cfg.Bucket, cleanObjectKey(key), minio.RemoveObjectOptions{}) } func (b *s3StorageBackend) DeletePrefix(ctx context.Context, prefix string) error { prefix = strings.TrimSuffix(cleanObjectKey(prefix), "/") + "/" objects := b.client.ListObjects(ctx, b.cfg.Bucket, minio.ListObjectsOptions{Prefix: prefix, Recursive: true}) for object := range objects { if object.Err != nil { return object.Err } if err := b.Delete(ctx, object.Key); err != nil { return err } } return nil } func (b *s3StorageBackend) Usage(ctx context.Context) (int64, error) { var total int64 for object := range b.client.ListObjects(ctx, b.cfg.Bucket, minio.ListObjectsOptions{Recursive: true}) { if object.Err != nil { return 0, object.Err } total += object.Size } return total, nil } func (b *s3StorageBackend) Test(ctx context.Context) error { exists, err := b.client.BucketExists(ctx, b.cfg.Bucket) if err != nil { return err } if !exists { return fmt.Errorf("bucket %q does not exist", b.cfg.Bucket) } key := ".warpbox-storage-test-" + randomID(6) if err := b.Put(ctx, key, bytes.NewReader([]byte("ok")), 2, "text/plain"); err != nil { return err } return b.Delete(ctx, key) } func s3BucketLookup(pathStyle bool) minio.BucketLookupType { if pathStyle { return minio.BucketLookupPath } return minio.BucketLookupAuto } func normalizeS3Endpoint(endpoint string) string { endpoint = strings.TrimSpace(endpoint) if parsed, err := url.Parse(endpoint); err == nil && parsed.Host != "" { return parsed.Host } return strings.TrimPrefix(strings.TrimPrefix(endpoint, "https://"), "http://") }