init
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/cors"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Bucket string
|
||||
UseSSL bool
|
||||
CORSOrigins string
|
||||
}
|
||||
|
||||
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
|
||||
bucket string
|
||||
corsOrigins 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")
|
||||
}
|
||||
return &MinIO{client: client, bucket: config.Bucket, corsOrigins: config.CORSOrigins}, 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 {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
origins := make([]string, 0)
|
||||
for _, origin := range strings.Split(s.corsOrigins, ",") {
|
||||
if value := strings.TrimSpace(origin); value != "" {
|
||||
origins = append(origins, value)
|
||||
}
|
||||
}
|
||||
if len(origins) == 0 {
|
||||
origins = []string{"*"}
|
||||
}
|
||||
if err := s.client.SetBucketCors(ctx, s.bucket, cors.NewConfig([]cors.Rule{{
|
||||
ID: "northline-browser-uploads",
|
||||
AllowedOrigin: origins,
|
||||
AllowedMethod: []string{"GET", "PUT", "POST", "PATCH", "DELETE", "HEAD"},
|
||||
AllowedHeader: []string{"*"},
|
||||
ExposeHeader: []string{"ETag", "x-amz-request-id", "x-amz-id-2"},
|
||||
MaxAgeSeconds: 3600,
|
||||
}})); err != nil {
|
||||
return fmt.Errorf("configure object storage CORS: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MinIO) CreateUploadURL(ctx context.Context, key, _ string, expiry time.Duration) (string, error) {
|
||||
url, err := s.client.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.client.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)
|
||||
Reference in New Issue
Block a user