feat(storage): S3-compatible storage adapter + storage_uri rename

Add S3FileStorage adapter (any S3-compatible backend: AWS, MinIO, Garage)
alongside the local adapter, selected via STORAGE_BACKEND. Proxied range
streaming via get_object+Range; as_local_path downloads to a tempfile for
ffmpeg/fpcalc. Rename track.file_path -> storage_uri across domain entity,
ORM, repositories, port, and services, with an Alembic migration. Adds
mocked S3 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Senko-san
2026-06-08 17:11:35 +03:00
parent a8348e145a
commit 5c5df5d3cc
15 changed files with 1377 additions and 498 deletions
+20 -5
View File
@@ -1,16 +1,31 @@
"""File storage provider — singleton factory."""
"""File storage provider — singleton factory wired from config."""
from app.core.config import get_settings
from app.domain.ports import FileStorage
from app.infrastructure.storage.local import LocalFileStorage
from app.infrastructure.storage.s3 import S3FileStorage
_storage: LocalFileStorage | None = None
_storage: FileStorage | None = None
def get_file_storage() -> LocalFileStorage:
def get_file_storage() -> FileStorage:
global _storage
if _storage is None:
settings = get_settings()
if settings.storage_backend == "s3":
raise NotImplementedError("S3 storage not yet implemented.")
_storage = LocalFileStorage(settings.media_path)
if not settings.s3_bucket:
raise RuntimeError("S3_BUCKET must be set when STORAGE_BACKEND=s3")
_storage = S3FileStorage(
settings.s3_bucket,
endpoint_url=settings.s3_endpoint_url,
region_name=settings.s3_region,
access_key=settings.s3_access_key.get_secret_value()
if settings.s3_access_key
else None,
secret_key=settings.s3_secret_key.get_secret_value()
if settings.s3_secret_key
else None,
)
else:
_storage = LocalFileStorage(settings.media_path)
return _storage