feat: S3-compatible object storage backend

BACKEND_STORAGE_DRIVER=s3 swaps LocalStore for a stdlib-only SigV4
implementation — PutObject/GetObject/DeleteObject/CopyObject/ListObjectsV2
against any S3-compatible API (AWS, MinIO, Garage, R2). Storage becomes a
Store interface; storage_path records s3://bucket/key. Content-Length is
resolved by seeking or temp-file spooling — chunked uploads are rejected
by real S3 servers. Bucket auto-created on startup.

Verified live against zenko/cloudserver: bucket, put, get, copy, delete.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-24 19:38:50 +02:00
1 parent f747b33b46
commit 3aca5bd1e0
11 files changed
+810 -17

No files matched your search

+24 -3
View File
@@ -81,9 +81,30 @@ func Bootstrap(ctx context.Context) (*App, error) {
}
}
store, err := storage.NewLocalStore(cfg.StorageRoot)
if err != nil {
return nil, err
var store storage.Store
if cfg.StorageDriver == "s3" {
s3Store, err := storage.NewS3Store(storage.S3Config{
Endpoint: cfg.S3Endpoint,
Region: cfg.S3Region,
Bucket: cfg.S3Bucket,
AccessKeyID: cfg.S3AccessKeyID,
SecretAccessKey: cfg.S3SecretAccessKey,
Prefix: cfg.S3Prefix,
PathStyle: cfg.S3PathStyle,
})
if err != nil {
return nil, fmt.Errorf("s3 storage: %w", err)
}
if err := s3Store.EnsureBucket(context.Background()); err != nil {
return nil, err
}
store = s3Store
} else {
localStore, err := storage.NewLocalStore(cfg.StorageRoot)
if err != nil {
return nil, err
}
store = localStore
}
dbxClient := dbx.NewClient(logger, cfg.DBXDataDir)
repo := repositories.NewCoreRepository(dbPool)