package storage import ( "context" "fmt" "io" "log" "path/filepath" "strings" "github.com/google/uuid" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) type MinIOStorage struct { client *minio.Client bucket string } type MinIOConfig struct { Endpoint string AccessKey string SecretKey string Bucket string UseSSL bool } func NewMinIOStorage(cfg MinIOConfig) (*MinIOStorage, error) { client, err := minio.New(cfg.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), Secure: cfg.UseSSL, }) if err != nil { return nil, fmt.Errorf("create minio client: %w", err) } ctx := context.Background() exists, err := client.BucketExists(ctx, cfg.Bucket) if err != nil { return nil, fmt.Errorf("check bucket: %w", err) } if !exists { if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{}); err != nil { return nil, fmt.Errorf("create bucket: %w", err) } log.Printf("created minio bucket: %s", cfg.Bucket) // Set bucket policy to allow public read policy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": ["*"]}, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::%s/*"] }] }`, cfg.Bucket) if err := client.SetBucketPolicy(ctx, cfg.Bucket, policy); err != nil { log.Printf("warning: set bucket policy: %v", err) } } return &MinIOStorage{client: client, bucket: cfg.Bucket}, nil } // Upload stores a file in MinIO and returns the object path (without bucket prefix). // The returned path can be used to construct the public URL: /uploads/{path} func (s *MinIOStorage) Upload(ctx context.Context, pathPrefix string, filename string, reader io.Reader, size int64, contentType string) (string, error) { ext := filepath.Ext(filename) if ext == "" { ext = guessExt(contentType) } objectName := fmt.Sprintf("%s/%s%s", pathPrefix, uuid.New().String(), ext) _, err := s.client.PutObject(ctx, s.bucket, objectName, reader, size, minio.PutObjectOptions{ ContentType: contentType, }) if err != nil { return "", fmt.Errorf("put object: %w", err) } return objectName, nil } func guessExt(contentType string) string { switch { case strings.HasPrefix(contentType, "image/jpeg"): return ".jpg" case strings.HasPrefix(contentType, "image/png"): return ".png" case strings.HasPrefix(contentType, "image/gif"): return ".gif" case strings.HasPrefix(contentType, "image/webp"): return ".webp" case strings.HasPrefix(contentType, "image/svg"): return ".svg" default: return ".bin" } }