from __future__ import annotations import re from pathlib import Path from fastapi.staticfiles import StaticFiles from starlette.responses import Response from starlette.types import Scope HTML_NO_STORE = "no-store" HASHED_ASSET_IMMUTABLE = "public, max-age=31536000, immutable" _MODULE_SCRIPT = re.compile( r']*\bsrc=["\'](?P/assets/[^"\']+)["\'][^>]*>', re.IGNORECASE, ) _HASHED_ASSET = re.compile( r"^assets/(?:.+)-[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$", ) def frontend_build_id(frontend_root: Path) -> str | None: """Return the exact content-hashed module loaded by the current index.""" try: index = (frontend_root / "index.html").read_text(encoding="utf-8") except OSError: return None for match in _MODULE_SCRIPT.finditer(index): source = match.group("src") if _HASHED_ASSET.fullmatch(source.removeprefix("/")): return source return None class ControlStationStaticFiles(StaticFiles): """Serve the SPA shell fresh while retaining immutable hashed assets.""" async def get_response(self, path: str, scope: Scope) -> Response: response = await super().get_response(path, scope) if response.status_code >= 400: return response content_type = response.headers.get("content-type", "").lower() normalized = path.lstrip("/") if content_type.startswith("text/html"): response.headers["Cache-Control"] = HTML_NO_STORE elif _HASHED_ASSET.fullmatch(normalized): response.headers["Cache-Control"] = HASHED_ASSET_IMMUTABLE return response