diff --git a/surfsense_backend/app/file_storage/__init__.py b/surfsense_backend/app/file_storage/__init__.py new file mode 100644 index 000000000..756f5c7b2 --- /dev/null +++ b/surfsense_backend/app/file_storage/__init__.py @@ -0,0 +1,15 @@ +"""Durable storage for original uploaded files (and future derived artifacts). + +Public surface: resolve the configured backend via :func:`get_storage_backend` +and persist/retrieve a document's files via :mod:`app.file_storage.service`. +""" + +from __future__ import annotations + +from app.file_storage.backends.base import StorageBackend +from app.file_storage.factory import get_storage_backend + +__all__ = [ + "StorageBackend", + "get_storage_backend", +] diff --git a/surfsense_backend/app/file_storage/factory.py b/surfsense_backend/app/file_storage/factory.py new file mode 100644 index 000000000..e30f6557b --- /dev/null +++ b/surfsense_backend/app/file_storage/factory.py @@ -0,0 +1,38 @@ +"""Resolve the configured :class:`StorageBackend` as a process-wide singleton.""" + +from __future__ import annotations + +from functools import lru_cache + +from app.file_storage.backends.base import StorageBackend +from app.file_storage.settings import ( + AZURE_BACKEND, + LOCAL_BACKEND, + load_storage_settings, +) + + +@lru_cache(maxsize=1) +def get_storage_backend() -> StorageBackend: + """Build the backend selected by ``FILE_STORAGE_BACKEND`` (lazy-imported).""" + settings = load_storage_settings() + + if settings.backend == AZURE_BACKEND: + if not settings.azure_connection_string or not settings.azure_container: + raise ValueError( + "Azure storage requires AZURE_STORAGE_CONNECTION_STRING and " + "AZURE_STORAGE_CONTAINER." + ) + from app.file_storage.backends.azure import AzureBlobBackend + + return AzureBlobBackend( + connection_string=settings.azure_connection_string, + container=settings.azure_container, + ) + + if settings.backend == LOCAL_BACKEND: + from app.file_storage.backends.local import LocalFileBackend + + return LocalFileBackend(settings.local_root) + + raise ValueError(f"Unknown FILE_STORAGE_BACKEND: {settings.backend!r}") diff --git a/surfsense_backend/app/file_storage/keys.py b/surfsense_backend/app/file_storage/keys.py new file mode 100644 index 000000000..22eaa9473 --- /dev/null +++ b/surfsense_backend/app/file_storage/keys.py @@ -0,0 +1,27 @@ +"""Object-key construction for stored document files.""" + +from __future__ import annotations + +import os +import uuid + +from app.file_storage.persistence.enums import DocumentFileKind + + +def build_document_file_key( + *, + search_space_id: int, + document_id: int, + kind: DocumentFileKind, + filename: str, +) -> str: + """Build the storage key for one document file. + + Shape: ``documents/{search_space_id}/{document_id}/{kind}/{uuid}{ext}``. + """ + extension = os.path.splitext(filename)[1].lower() + unique = uuid.uuid4().hex + return ( + f"documents/{search_space_id}/{document_id}/" + f"{kind.value.lower()}/{unique}{extension}" + ) diff --git a/surfsense_backend/app/file_storage/settings.py b/surfsense_backend/app/file_storage/settings.py new file mode 100644 index 000000000..612575890 --- /dev/null +++ b/surfsense_backend/app/file_storage/settings.py @@ -0,0 +1,37 @@ +"""Environment-driven configuration for the file-storage module.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path + +LOCAL_BACKEND = "local" +AZURE_BACKEND = "azure" + +# surfsense_backend/ — two levels up from app/file_storage/settings.py +_BACKEND_ROOT = Path(__file__).resolve().parents[2] +_DEFAULT_LOCAL_ROOT = str(_BACKEND_ROOT / ".local_object_store") + + +@dataclass(frozen=True) +class StorageSettings: + """Resolved storage configuration for the current process.""" + + backend: str + azure_connection_string: str | None + azure_container: str | None + local_root: str + + +def load_storage_settings() -> StorageSettings: + """Read storage settings from the environment. + + Defaults to the ``local`` backend so development needs no cloud creds. + """ + return StorageSettings( + backend=os.getenv("FILE_STORAGE_BACKEND", LOCAL_BACKEND).strip().lower(), + azure_connection_string=os.getenv("AZURE_STORAGE_CONNECTION_STRING"), + azure_container=os.getenv("AZURE_STORAGE_CONTAINER"), + local_root=os.getenv("FILE_STORAGE_LOCAL_PATH", _DEFAULT_LOCAL_ROOT), + )