Files
warmbly/internal/infrastructure/storage/factory.go
T
Matthew Meszaros 368c1f7886 infra(storage): Store interface + Filesystem impl + S3-compatible factory
One Store interface (Get/Put/Delete/Has/PresignedGetURL/Name) covers
both the AWS S3 client (now exposes high-level methods alongside the
legacy embedded *s3.Client) and a new FilesystemStore for self-hosters.

FilesystemStore writes atomically via temp-file + rename, rejects '..'
in keys before path normalization, returns ErrUnsupported from
PresignedGetURL.

The S3 impl works against AWS S3, MinIO, Cloudflare R2, Backblaze B2,
and Hetzner Object Storage via standard AWS endpoint-URL config.

Factory NewFromEnv selects via BLOB_PROVIDER (defaults to s3).

14 tests cover round-trip, ErrNotFound, atomic-write cleanup, traversal
rejection, factory env-selection paths.
2026-05-27 14:41:40 +00:00

58 lines
1.7 KiB
Go

package storage
import (
"context"
"fmt"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
)
// NewFromEnv constructs the active blob store from environment variables.
//
// BLOB_PROVIDER=s3 -> existing S3 client (works for AWS / MinIO /
// R2 / B2 / Hetzner Object Storage)
// BLOB_PROVIDER=filesystem -> NewFilesystem at BLOB_FS_ROOT
// (unset) -> defaults to "s3" for backwards compatibility
//
// awscfg + defaultBucket are only consulted when the S3 provider is selected.
// For non-AWS S3-compatible endpoints, the operator sets standard AWS env vars:
//
// AWS_ENDPOINT_URL_S3 -> override endpoint (MinIO, R2, etc.)
// AWS_REGION -> required by SDK; arbitrary value for MinIO
// AWS_ACCESS_KEY_ID -> S3 credential
// AWS_SECRET_ACCESS_KEY -> S3 credential
//
// BLOB_BUCKET overrides defaultBucket when set.
func NewFromEnv(ctx context.Context, awscfg aws.Config, defaultBucket string) (Store, error) {
provider := os.Getenv("BLOB_PROVIDER")
if provider == "" {
provider = "s3"
}
switch provider {
case "s3":
bucket := os.Getenv("BLOB_BUCKET")
if bucket == "" {
bucket = defaultBucket
}
if bucket == "" {
return nil, fmt.Errorf("storage: s3 provider requires BLOB_BUCKET or default bucket")
}
return NewClient(ctx, awscfg, bucket)
case "filesystem", "fs":
root := os.Getenv("BLOB_FS_ROOT")
if root == "" {
return nil, fmt.Errorf("storage: filesystem provider requires BLOB_FS_ROOT")
}
return NewFilesystem(root)
default:
return nil, fmt.Errorf("storage: unknown BLOB_PROVIDER %q (want: s3, filesystem)", provider)
}
}
// Compile-time interface checks.
var (
_ Store = (*Client)(nil)
_ Store = (*FilesystemStore)(nil)
)