Files
wijtransferen/backend/internal/storage/storage.go
T
2026-08-22 21:25:53 +02:00

157 lines
4.4 KiB
Go

package storage
import (
"context"
"fmt"
"io"
"net/url"
"strings"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
type Config struct {
Endpoint string
PublicEndpoint string
AccessKey string
SecretKey string
Bucket string
UseSSL bool
}
type ObjectInfo struct {
Size int64
ContentType string
}
// Storage is deliberately S3-shaped so the MinIO implementation can be
// replaced by AWS S3, R2, B2, or another compatible provider later.
type Storage interface {
EnsureBucket(context.Context) error
CreateUploadURL(context.Context, string, string, time.Duration) (string, error)
CreateDownloadURL(context.Context, string, time.Duration) (string, error)
Delete(context.Context, string) error
Stat(context.Context, string) (ObjectInfo, error)
Get(context.Context, string) (io.ReadCloser, error)
Put(context.Context, string, io.Reader, int64, string) error
}
type MinIO struct {
client *minio.Client
publicClient *minio.Client
bucket string
}
func NewMinIO(config Config) (*MinIO, error) {
client, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""),
Secure: config.UseSSL,
})
if err != nil {
return nil, fmt.Errorf("create object storage client: %w", err)
}
if config.Bucket == "" {
return nil, fmt.Errorf("object storage bucket is required")
}
publicClient := client
if config.PublicEndpoint != "" {
publicEndpoint, secure, err := parseEndpoint(config.PublicEndpoint)
if err != nil {
return nil, fmt.Errorf("parse public endpoint: %w", err)
}
publicClient, err = minio.New(publicEndpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""),
Secure: secure,
})
if err != nil {
return nil, fmt.Errorf("create public object storage client: %w", err)
}
}
return &MinIO{
client: client,
publicClient: publicClient,
bucket: config.Bucket,
}, nil
}
func (s *MinIO) EnsureBucket(ctx context.Context) error {
exists, err := s.client.BucketExists(ctx, s.bucket)
if err != nil {
return fmt.Errorf("check object storage bucket: %w", err)
}
if !exists {
if err := s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}); err != nil {
response := minio.ToErrorResponse(err)
if response.Code != "BucketAlreadyExists" && response.Code != "BucketAlreadyOwnedByYou" {
return fmt.Errorf("create object storage bucket: %w", err)
}
}
}
return nil
}
func (s *MinIO) CreateUploadURL(ctx context.Context, key, _ string, expiry time.Duration) (string, error) {
url, err := s.publicClient.PresignedPutObject(ctx, s.bucket, key, expiry)
if err != nil {
return "", fmt.Errorf("create upload URL: %w", err)
}
return url.String(), nil
}
func (s *MinIO) CreateDownloadURL(ctx context.Context, key string, expiry time.Duration) (string, error) {
url, err := s.publicClient.PresignedGetObject(ctx, s.bucket, key, expiry, nil)
if err != nil {
return "", fmt.Errorf("create download URL: %w", err)
}
return url.String(), nil
}
func (s *MinIO) Delete(ctx context.Context, key string) error {
if err := s.client.RemoveObject(ctx, s.bucket, key, minio.RemoveObjectOptions{}); err != nil {
return fmt.Errorf("delete object: %w", err)
}
return nil
}
func (s *MinIO) Stat(ctx context.Context, key string) (ObjectInfo, error) {
info, err := s.client.StatObject(ctx, s.bucket, key, minio.StatObjectOptions{})
if err != nil {
return ObjectInfo{}, fmt.Errorf("stat object: %w", err)
}
return ObjectInfo{Size: info.Size, ContentType: info.ContentType}, nil
}
func (s *MinIO) Get(ctx context.Context, key string) (io.ReadCloser, error) {
object, err := s.client.GetObject(ctx, s.bucket, key, minio.GetObjectOptions{})
if err != nil {
return nil, fmt.Errorf("get object: %w", err)
}
return object, nil
}
func (s *MinIO) Put(ctx context.Context, key string, reader io.Reader, size int64, contentType string) error {
if _, err := s.client.PutObject(ctx, s.bucket, key, reader, size, minio.PutObjectOptions{ContentType: contentType}); err != nil {
return fmt.Errorf("put object: %w", err)
}
return nil
}
var _ Storage = (*MinIO)(nil)
func parseEndpoint(raw string) (host string, secure bool, err error) {
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", false, err
}
host = u.Host
secure = u.Scheme == "https"
return host, secure, nil
}