Add S3-compatible storage backend (B2, MinIO, AWS S3)

Introduces a StorageBackend interface with local filesystem and S3
implementations. The StorageService delegates raw I/O to the backend
while keeping validation, encryption, and URL generation unchanged.

Backend selection is config-driven: set B2_ENDPOINT + B2_KEY_ID +
B2_APP_KEY + B2_BUCKET_NAME for S3 mode, or STORAGE_UPLOAD_DIR for
local mode. STORAGE_USE_SSL=false for in-cluster MinIO (HTTP).

All existing tests pass unchanged — the local backend preserves
identical behavior to the previous direct-filesystem implementation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-30 21:31:24 -05:00
parent 34553f3bec
commit 2e10822e5a
8 changed files with 359 additions and 132 deletions

View File

@@ -138,15 +138,33 @@ type SecurityConfig struct {
TokenRefreshDays int // Token must be at least this many days old before refresh (default 60)
}
// StorageConfig holds file storage settings
// StorageConfig holds file storage settings.
// When S3Endpoint is set, files are stored in S3-compatible storage (B2, MinIO).
// When S3Endpoint is empty, files are stored on the local filesystem using UploadDir.
type StorageConfig struct {
UploadDir string // Directory to store uploaded files
BaseURL string // Public URL prefix for serving files (e.g., "/uploads")
// Local filesystem settings
UploadDir string // Directory to store uploaded files (local mode)
BaseURL string // Public URL prefix for serving files (e.g., "/uploads")
// S3-compatible storage settings (B2, MinIO)
S3Endpoint string // S3 endpoint (e.g., "s3.us-west-004.backblazeb2.com" or "minio:9000")
S3KeyID string // Access key ID
S3AppKey string // Secret access key
S3Bucket string // Bucket name
S3UseSSL bool // Use HTTPS (true for B2, false for in-cluster MinIO)
S3Region string // Region (optional, defaults to "us-east-1")
// Shared settings
MaxFileSize int64 // Max file size in bytes (default 10MB)
AllowedTypes string // Comma-separated MIME types
EncryptionKey string // 64-char hex key for file encryption at rest (optional)
}
// IsS3 returns true if S3-compatible storage is configured
func (c *StorageConfig) IsS3() bool {
return c.S3Endpoint != "" && c.S3KeyID != "" && c.S3AppKey != "" && c.S3Bucket != ""
}
// FeatureFlags holds kill switches for major subsystems.
// All default to true (enabled). Set to false via env vars to disable.
type FeatureFlags struct {
@@ -270,6 +288,12 @@ func Load() (*Config, error) {
Storage: StorageConfig{
UploadDir: viper.GetString("STORAGE_UPLOAD_DIR"),
BaseURL: viper.GetString("STORAGE_BASE_URL"),
S3Endpoint: viper.GetString("B2_ENDPOINT"),
S3KeyID: viper.GetString("B2_KEY_ID"),
S3AppKey: viper.GetString("B2_APP_KEY"),
S3Bucket: viper.GetString("B2_BUCKET_NAME"),
S3UseSSL: viper.GetString("STORAGE_USE_SSL") == "" || viper.GetBool("STORAGE_USE_SSL"),
S3Region: viper.GetString("B2_REGION"),
MaxFileSize: viper.GetInt64("STORAGE_MAX_FILE_SIZE"),
AllowedTypes: viper.GetString("STORAGE_ALLOWED_TYPES"),
EncryptionKey: viper.GetString("STORAGE_ENCRYPTION_KEY"),