5349f21752
New FastAPI microservice (port 8020) providing unified blob storage via PUT/GET/DELETE/LIST HTTP API. Local filesystem backend is the default (zero extra deps). S3-compatible and WebDAV backends are built in. Backend is switchable at runtime via POST /migrate, which copies all objects to the new backend, verifies each one, atomically switches, then cleans up the old backend. WebDAV XML parsing uses defusedxml to prevent XXE attacks. Wired into docker-compose (storage_data volume) and registered in the backend service-health poller as 'storage-service'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
71 lines
2.7 KiB
Python
71 lines
2.7 KiB
Python
import logging
|
|
|
|
from app.core.config import settings
|
|
from app.services.backends.base import AbstractStorageBackend
|
|
from app.services.backends.local import LocalFSBackend
|
|
from app.services.backends.s3 import S3Backend
|
|
from app.services.backends.webdav import WebDAVBackend
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_active_backend: AbstractStorageBackend | None = None
|
|
|
|
|
|
def build_backend(driver: str, config: dict) -> AbstractStorageBackend:
|
|
"""Construct a backend instance from a driver name + config dict."""
|
|
if driver == "local":
|
|
return LocalFSBackend(data_dir=config.get("data_dir", settings.DATA_DIR))
|
|
if driver == "s3":
|
|
return S3Backend(
|
|
endpoint_url=config.get("endpoint_url", ""),
|
|
access_key=config.get("access_key", ""),
|
|
secret_key=config.get("secret_key", ""),
|
|
region=config.get("region", "us-east-1"),
|
|
)
|
|
if driver == "webdav":
|
|
return WebDAVBackend(
|
|
url=config.get("url", ""),
|
|
username=config.get("username", ""),
|
|
password=config.get("password", ""),
|
|
root_path=config.get("root_path", "/"),
|
|
)
|
|
raise ValueError(f"Unknown driver: {driver!r}. Valid options: local, s3, webdav")
|
|
|
|
|
|
def initialize_backend() -> None:
|
|
"""Build the initial backend from environment variables at startup."""
|
|
global _active_backend
|
|
driver = settings.STORAGE_BACKEND
|
|
config: dict = {}
|
|
if driver == "s3":
|
|
config = {
|
|
"endpoint_url": settings.S3_ENDPOINT_URL,
|
|
"access_key": settings.S3_ACCESS_KEY,
|
|
"secret_key": settings.S3_SECRET_KEY,
|
|
"region": settings.S3_REGION,
|
|
}
|
|
elif driver == "webdav":
|
|
config = {
|
|
"url": settings.WEBDAV_URL,
|
|
"username": settings.WEBDAV_USERNAME,
|
|
"password": settings.WEBDAV_PASSWORD,
|
|
"root_path": settings.WEBDAV_ROOT_PATH,
|
|
}
|
|
# local needs no extra config — DATA_DIR is read from settings inside build_backend
|
|
_active_backend = build_backend(driver, config)
|
|
logger.info("Storage backend initialized: %s", driver)
|
|
|
|
|
|
def get_backend() -> AbstractStorageBackend:
|
|
if _active_backend is None:
|
|
raise RuntimeError("Backend not initialized — call initialize_backend() at startup")
|
|
return _active_backend
|
|
|
|
|
|
def switch_backend(new_backend: AbstractStorageBackend) -> None:
|
|
"""Replace the active backend. Called by the migration job after all data is verified."""
|
|
global _active_backend
|
|
old_name = _active_backend.driver_name if _active_backend else "none"
|
|
_active_backend = new_backend
|
|
logger.info("Storage backend switched: %s → %s", old_name, new_backend.driver_name)
|