Files
Michilis 498d7d8a7d Add photo content-hash dedup and local↔S3 library sync.
Uploads skip per-gallery duplicates, checksums can be backfilled, and STORAGE_BACKEND plus sync tooling make switching storage backends safe.
2026-08-05 05:41:12 +00:00

48 lines
1.6 KiB
Go

// Package storage abstracts photo object storage. Selection follows the
// backend's convention (backend/src/lib/storage.ts): S3 when S3_ENDPOINT
// and S3_BUCKET are both set, local disk otherwise. Keys are identical on
// both backends: galleries/<galleryID>/<variantDir>/<photoID>.<ext>.
package storage
import (
"context"
"errors"
"fmt"
"io"
"time"
"git.azzamo.net/Michilis/Spanglish/photo-api/internal/config"
)
// ErrNoPresign is returned by backends that cannot presign (local disk);
// callers then stream the object through the API instead.
var ErrNoPresign = errors.New("presigned URLs not supported")
// ErrNotExist is what Stat reports for a missing object on either backend.
var ErrNotExist = errors.New("object does not exist")
type Storage interface {
Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error
Open(ctx context.Context, key string) (io.ReadCloser, int64, error)
// Stat returns the object's size without reading it, or ErrNotExist.
Stat(ctx context.Context, key string) (int64, error)
Delete(ctx context.Context, key string) error
PresignGet(ctx context.Context, key, downloadFilename, contentType string, expiry time.Duration) (string, error)
}
// New builds the backend that serves requests (see config.S3Enabled).
func New(cfg config.Config) (Storage, error) {
if cfg.S3Enabled() {
return NewS3(cfg)
}
return NewLocal(cfg.StoragePath)
}
// Name describes a backend for log lines.
func Name(cfg config.Config, s Storage) string {
if _, ok := s.(*s3Store); ok {
return fmt.Sprintf("S3 bucket %s at %s", cfg.S3Bucket, cfg.S3Endpoint)
}
return fmt.Sprintf("local disk at %s", cfg.StoragePath)
}