100 lines
2.7 KiB
Go
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
|
|
}
|