Files

100 lines
2.7 KiB
Go

// 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
}