finishing QDRO section
This commit is contained in:
86
app/services/storage.py
Normal file
86
app/services/storage.py
Normal 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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user