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:
70
internal/services/storage_backend_local.go
Normal file
70
internal/services/storage_backend_local.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// LocalBackend stores files on the local filesystem.
|
||||
type LocalBackend struct {
|
||||
baseDir string
|
||||
}
|
||||
|
||||
// NewLocalBackend creates a local filesystem storage backend.
|
||||
// It ensures the base directory and standard subdirectories exist.
|
||||
func NewLocalBackend(baseDir string) (*LocalBackend, error) {
|
||||
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create upload directory: %w", err)
|
||||
}
|
||||
|
||||
for _, subdir := range []string{"images", "documents", "completions"} {
|
||||
path := filepath.Join(baseDir, subdir)
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create subdirectory %s: %w", subdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &LocalBackend{baseDir: baseDir}, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Write(key string, data []byte) error {
|
||||
destPath, err := SafeResolvePath(b.baseDir, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid path: %w", err)
|
||||
}
|
||||
return os.WriteFile(destPath, data, 0644)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Read(key string) ([]byte, error) {
|
||||
fullPath, err := SafeResolvePath(b.baseDir, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid path: %w", err)
|
||||
}
|
||||
return os.ReadFile(fullPath)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Delete(key string) error {
|
||||
fullPath, err := SafeResolvePath(b.baseDir, key)
|
||||
if err != nil {
|
||||
return nil // invalid path = nothing to delete
|
||||
}
|
||||
if err := os.Remove(fullPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) ReadStream(key string) (io.ReadCloser, error) {
|
||||
fullPath, err := SafeResolvePath(b.baseDir, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid path: %w", err)
|
||||
}
|
||||
return os.Open(fullPath)
|
||||
}
|
||||
|
||||
// BaseDir returns the local storage base directory.
|
||||
func (b *LocalBackend) BaseDir() string {
|
||||
return b.baseDir
|
||||
}
|
||||
Reference in New Issue
Block a user