rentease/internal/driver/storage/minio.go
Ruidy 0f327c814a
Some checks failed
CI / checks (push) Has been cancelled
fix: normalize minio endpoints
2026-03-21 10:19:33 +01:00

131 lines
3.2 KiB
Go

package storage
import (
"bytes"
"context"
"fmt"
"net/url"
"strings"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/service/booking"
)
type MinioInvoiceStorage struct {
bucket string
client *minio.Client
}
func NewMinioInvoiceStorage(cfg *config.Config) (*MinioInvoiceStorage, error) {
if cfg.MinIOEndpoint == "" && cfg.MinIOAccessKey == "" && cfg.MinIOSecretKey == "" && cfg.MinIOBucket == "" {
return nil, nil
}
switch {
case cfg.MinIOEndpoint == "":
return nil, fmt.Errorf("minio endpoint is required")
case cfg.MinIOAccessKey == "":
return nil, fmt.Errorf("minio access key is required")
case cfg.MinIOSecretKey == "":
return nil, fmt.Errorf("minio secret key is required")
case cfg.MinIOBucket == "":
return nil, fmt.Errorf("minio bucket is required")
}
endpoint, secure, err := normalizeEndpoint(cfg.MinIOEndpoint, cfg.MinIOUseSSL)
if err != nil {
return nil, fmt.Errorf("normalize minio endpoint: %w", err)
}
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.MinIOAccessKey, cfg.MinIOSecretKey, ""),
Secure: secure,
})
if err != nil {
return nil, fmt.Errorf("create minio client: %w", err)
}
return &MinioInvoiceStorage{
bucket: cfg.MinIOBucket,
client: client,
}, nil
}
func normalizeEndpoint(raw string, secure bool) (string, bool, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", secure, fmt.Errorf("endpoint is required")
}
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
u, err := url.Parse(raw)
if err != nil {
return "", secure, fmt.Errorf("parse endpoint: %w", err)
}
if u.Host == "" {
return "", secure, fmt.Errorf("endpoint host is required")
}
if u.Path != "" && u.Path != "/" {
return "", secure, fmt.Errorf("endpoint path is not supported")
}
return u.Host, u.Scheme == "https", nil
}
return raw, secure, nil
}
func (s *MinioInvoiceStorage) StoreInvoice(
ctx context.Context,
objectKey string,
file booking.GeneratedFile,
shareURLTTL time.Duration,
) (*booking.StoredInvoiceFile, error) {
if err := s.ensureBucket(ctx); err != nil {
return nil, err
}
_, err := s.client.PutObject(
ctx,
s.bucket,
objectKey,
bytes.NewReader(file.Data),
int64(len(file.Data)),
minio.PutObjectOptions{
ContentType: file.ContentType,
},
)
if err != nil {
return nil, fmt.Errorf("upload object %q: %w", objectKey, err)
}
shareURL, err := s.client.PresignedGetObject(ctx, s.bucket, objectKey, shareURLTTL, nil)
if err != nil {
return nil, fmt.Errorf("presign object %q: %w", objectKey, err)
}
return &booking.StoredInvoiceFile{
ObjectKey: objectKey,
ShareURL: shareURL.String(),
ShareURLExpiresAt: time.Now().UTC().Add(shareURLTTL),
}, nil
}
func (s *MinioInvoiceStorage) ensureBucket(ctx context.Context) error {
exists, err := s.client.BucketExists(ctx, s.bucket)
if err != nil {
return fmt.Errorf("check bucket %q: %w", s.bucket, err)
}
if exists {
return nil
}
if err := s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}); err != nil {
return fmt.Errorf("create bucket %q: %w", s.bucket, err)
}
return nil
}