photo-object-storage: MinIO on .13 (9010/9011), Go upload+serve /photos, admin S3 disk + Filament upload, migrated, deployed
This commit is contained in:
99
frontend/internal/storage/storage.go
Normal file
99
frontend/internal/storage/storage.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Package storage provides object storage for Where Woof photos (MinIO, S3-compatible).
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
// Client wraps a MinIO client and bucket for photo objects.
|
||||
type Client struct {
|
||||
mc *minio.Client
|
||||
bucket string
|
||||
}
|
||||
|
||||
// NewClient builds a storage client from MINIO_* env vars.
|
||||
// Returns (nil, nil) when storage is not configured (MINIO_ENDPOINT unset) —
|
||||
// callers should treat that as "storage unavailable".
|
||||
func NewClient() (*Client, error) {
|
||||
endpoint := os.Getenv("MINIO_ENDPOINT")
|
||||
if endpoint == "" {
|
||||
return nil, nil
|
||||
}
|
||||
access := os.Getenv("MINIO_ACCESS_KEY")
|
||||
secret := os.Getenv("MINIO_SECRET_KEY")
|
||||
bucket := os.Getenv("MINIO_BUCKET")
|
||||
if bucket == "" {
|
||||
bucket = "wherewoof"
|
||||
}
|
||||
|
||||
mc, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(access, secret, ""),
|
||||
Secure: false, // internal LAN
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minio client: %w", err)
|
||||
}
|
||||
return &Client{mc: mc, bucket: bucket}, nil
|
||||
}
|
||||
|
||||
// Put stores an object under key and returns the size.
|
||||
func (c *Client) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("storage not configured")
|
||||
}
|
||||
_, err := c.mc.PutObject(ctx, c.bucket, key, r, size, minio.PutObjectOptions{
|
||||
ContentType: contentType,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Get returns the object reader + content type for a key.
|
||||
func (c *Client) Get(ctx context.Context, key string) (io.ReadCloser, string, error) {
|
||||
if c == nil {
|
||||
return nil, "", fmt.Errorf("storage not configured")
|
||||
}
|
||||
obj, err := c.mc.GetObject(ctx, c.bucket, key, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
stat, err := obj.Stat()
|
||||
if err != nil {
|
||||
obj.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
return obj, stat.ContentType, nil
|
||||
}
|
||||
|
||||
// ListKeys returns object keys with the given prefix (used for migration).
|
||||
func (c *Client) ListKeys(ctx context.Context, prefix string) ([]string, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("storage not configured")
|
||||
}
|
||||
var keys []string
|
||||
for obj := range c.mc.ListObjects(ctx, c.bucket, minio.ListObjectsOptions{Prefix: prefix}) {
|
||||
if obj.Err != nil {
|
||||
return nil, obj.Err
|
||||
}
|
||||
keys = append(keys, obj.Key)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// PresignedURL returns a temporary URL (not used by the app; available for admin previews).
|
||||
func (c *Client) PresignedURL(ctx context.Context, key string, expiry time.Duration) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("storage not configured")
|
||||
}
|
||||
u, err := c.mc.PresignedGetObject(ctx, c.bucket, key, expiry, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user