finishing QDRO section

This commit is contained in:
HotSwapp
2025-08-15 17:19:51 -05:00
parent 006ef3d7b1
commit abc7f289d1
22 changed files with 2753 additions and 46 deletions

86
app/services/storage.py Normal file
View File

@@ -0,0 +1,86 @@
"""
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()