infrasynth-backend-kit/infrasynth/files/storage.py
jcv-dev 5df0be1f5c feat: uniform extensibility across every module
Close the remaining places where an extension point was hardcoded, and
expose a stable public import surface for every app.

- files: register_storage_backend(...) / STORAGE_BACKENDS[name]["CLASS"];
  unknown backends now fail loudly instead of silently using local
- files: PipelineStepRegistry + @pipeline_step; PIPELINE_EXECUTOR setting
- billing: INVOICE_PDF_BUILDER setting
- security: TWO_FACTOR_SERVICE / TWO_FACTOR_RECOVERY_SERVICE settings
- all app packages expose lazy public exports (PEP 562); infrasynth.shared
  re-exports its primitives eagerly
- README documents the per-module extension-point table
- tests/test_extensibility.py pins each hook plus the public surface
2026-09-24 11:08:08 -05:00

196 lines
6.5 KiB
Python

from django.core.files.storage import FileSystemStorage
from infrasynth.shared.settings_utils import get_setting
class _LocalStorage:
def __init__(self, config: dict):
self._storage = FileSystemStorage()
def save(self, name: str, content) -> str:
return self._storage.save(name, content)
def open(self, name: str, mode: str = "rb"):
return self._storage.open(name, mode)
def exists(self, name: str) -> bool:
return self._storage.exists(name)
def url(self, name: str) -> str:
return self._storage.url(name)
def delete(self, name: str) -> None:
self._storage.delete(name)
def generate_signed_url(self, name: str, expiry_seconds: int = 3600) -> str:
return self._storage.url(name)
class _S3Storage:
def __init__(self, config: dict):
from storages.backends.s3boto3 import S3Boto3Storage
options = {
"access_key": config.get("ACCESS_KEY"),
"secret_key": config.get("SECRET_KEY"),
"bucket_name": config.get("BUCKET_NAME"),
"region_name": config.get("REGION", "us-east-1"),
"endpoint_url": config.get("ENDPOINT_URL"),
"querystring_auth": True,
"default_acl": None,
"file_overwrite": False,
}
options = {k: v for k, v in options.items() if v is not None}
self._storage = S3Boto3Storage(**options)
def save(self, name: str, content) -> str:
return self._storage.save(name, content)
def open(self, name: str, mode: str = "rb"):
return self._storage.open(name, mode)
def exists(self, name: str) -> bool:
return self._storage.exists(name)
def url(self, name: str) -> str:
return self._storage.url(name)
def delete(self, name: str) -> None:
self._storage.delete(name)
def generate_signed_url(self, name: str, expiry_seconds: int = 3600) -> str:
return self._storage.url(name, expire=expiry_seconds)
class _GCSStorage:
def __init__(self, config: dict):
from storages.backends.gcloud import GoogleCloudStorage
self._storage = GoogleCloudStorage(
bucket_name=config.get("BUCKET_NAME"),
project_id=config.get("PROJECT_ID"),
credentials=config.get("CREDENTIALS_PATH"),
)
def save(self, name: str, content) -> str:
return self._storage.save(name, content)
def open(self, name: str, mode: str = "rb"):
return self._storage.open(name, mode)
def exists(self, name: str) -> bool:
return self._storage.exists(name)
def url(self, name: str) -> str:
return self._storage.url(name)
def delete(self, name: str) -> None:
self._storage.delete(name)
def generate_signed_url(self, name: str, expiry_seconds: int = 3600) -> str:
return self._storage.url(name, expire=expiry_seconds)
class _CloudinaryStorage:
def __init__(self, config: dict):
import cloudinary
from cloudinary.uploader import upload
cloudinary.config(
cloud_name=config.get("CLOUD_NAME"),
api_key=config.get("API_KEY"),
api_secret=config.get("API_SECRET"),
)
self._upload = upload
def save(self, name: str, content) -> str:
result = self._upload(content.read(), public_id=name, overwrite=False)
return result.get("public_id", name)
def open(self, name: str, mode: str = "rb"):
raise NotImplementedError("Cloudinary storage does not support direct file open.")
def exists(self, name: str) -> bool:
import cloudinary.api
try:
cloudinary.api.resource(name)
return True
except Exception:
return False
def url(self, name: str) -> str:
import cloudinary.utils
return cloudinary.utils.cloudinary_url(name)[0]
def delete(self, name: str) -> None:
from cloudinary.uploader import destroy
destroy(name)
def generate_signed_url(self, name: str, expiry_seconds: int = 3600) -> str:
import cloudinary.utils
return cloudinary.utils.cloudinary_url(name, sign_url=True, expires_at=expiry_seconds)[0]
_BUILTIN_BACKENDS: dict[str, type] = {
"local": _LocalStorage,
"S3": _S3Storage,
"s3": _S3Storage,
"gcs": _GCSStorage,
"cloudinary": _CloudinaryStorage,
}
# Apps register their own backends at startup (``apps.py:ready()``) so a custom
# storage backend never requires editing the kit.
_CUSTOM_BACKENDS: dict[str, object] = {}
def register_storage_backend(name: str, factory: object) -> None:
"""Registers a storage backend factory/class under ``name``.
``factory`` is called with the backend's config dict and must implement the
storage protocol (``save/open/exists/url/delete/generate_signed_url``).
"""
_CUSTOM_BACKENDS[str(name)] = factory
def get_storage_backend(backend_name: str | None = None):
"""Returns a storage backend instance by name (defaults to settings config).
Resolution order: ``STORAGE_BACKENDS[<name>]["CLASS"]`` (dotted path) →
a backend registered via :func:`register_storage_backend` → the built-in
backends. An unknown name raises rather than silently falling back to local.
"""
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
config = getattr(settings, "INFRASYNTH_FILES", {})
if not backend_name:
backend_name = get_setting("INFRASYNTH_FILES", "DEFAULT_STORAGE_BACKEND", "local")
backend_name = str(backend_name)
backend_config = (config.get("STORAGE_BACKENDS", {}) or {}).get(backend_name, {}) or {}
class_path = (backend_config or {}).get("CLASS")
if class_path:
backend_class = import_string(class_path)
elif backend_name in _CUSTOM_BACKENDS:
backend_class = _CUSTOM_BACKENDS[backend_name]
else:
backend_class = _BUILTIN_BACKENDS.get(backend_name)
if backend_class is None:
raise ImproperlyConfigured(
f"Unknown storage backend '{backend_name}'. Register it with "
"infrasynth.files.storage.register_storage_backend or set "
f'INFRASYNTH_FILES["STORAGE_BACKENDS"]["{backend_name}"]["CLASS"].'
)
return backend_class(backend_config)
def save_file(file_obj, storage_key: str, backend: str = "local"):
"""Saves a file to the given storage backend, returns the stored key."""
storage = get_storage_backend(backend)
return storage.save(storage_key, file_obj)