""" Storage abstraction for templates/documents. MVP: Local filesystem implementation; S3-compatible interface ready. """ from __future__ import annotations import os import uuid from typing import Optional from app.config import settings class StorageAdapter: """Abstract storage adapter.""" def save_bytes(self, *, content: bytes, filename_hint: str, subdir: Optional[str] = None, content_type: Optional[str] = None) -> str: raise NotImplementedError def open_bytes(self, storage_path: str) -> bytes: raise NotImplementedError def delete(self, storage_path: str) -> bool: raise NotImplementedError def exists(self, storage_path: str) -> bool: raise NotImplementedError def public_url(self, storage_path: str) -> Optional[str]: return None class LocalStorageAdapter(StorageAdapter): """Store bytes under settings.upload_dir using relative storage_path.""" def __init__(self, base_dir: Optional[str] = None) -> None: self.base_dir = os.path.abspath(base_dir or settings.upload_dir) def _ensure_dir(self, directory: str) -> None: os.makedirs(directory, exist_ok=True) def save_bytes(self, *, content: bytes, filename_hint: str, subdir: Optional[str] = None, content_type: Optional[str] = None) -> str: safe_name = filename_hint.replace("/", "_").replace("\\", "_") if not os.path.splitext(safe_name)[1]: # Ensure a default extension when missing safe_name = f"{safe_name}.bin" unique = uuid.uuid4().hex directory = os.path.join(self.base_dir, subdir) if subdir else self.base_dir self._ensure_dir(directory) final_name = f"{unique}_{safe_name}" abs_path = os.path.join(directory, final_name) with open(abs_path, "wb") as f: f.write(content) # Return storage path relative to base_dir for portability rel_path = os.path.relpath(abs_path, self.base_dir) return rel_path def open_bytes(self, storage_path: str) -> bytes: abs_path = os.path.join(self.base_dir, storage_path) with open(abs_path, "rb") as f: return f.read() def delete(self, storage_path: str) -> bool: abs_path = os.path.join(self.base_dir, storage_path) try: os.remove(abs_path) return True except FileNotFoundError: return False def exists(self, storage_path: str) -> bool: abs_path = os.path.join(self.base_dir, storage_path) return os.path.exists(abs_path) def public_url(self, storage_path: str) -> Optional[str]: # Uploads are mounted at /uploads in FastAPI main # Map base_dir to /uploads; when base_dir is settings.upload_dir, this works. return f"/uploads/{storage_path}".replace("\\", "/") def get_default_storage() -> StorageAdapter: # MVP: always local storage return LocalStorageAdapter()