perf: cache media manifest revisions
This commit is contained in:
@@ -11,6 +11,22 @@ WATERBODY_MEDIA_ROLES = {"waterbody_cover", "waterbody_map", "waterbody_depth_ma
|
||||
TACKLE_MEDIA_ROLES = {"tackle_card", "tackle_detail", "rig_diagram", "tackle_screenshot"}
|
||||
KNOWN_MEDIA_ROLES = WATERBODY_MEDIA_ROLES | TACKLE_MEDIA_ROLES
|
||||
MEDIA_ROLES_BY_ENTITY = {"waterbody": WATERBODY_MEDIA_ROLES, "tackle": TACKLE_MEDIA_ROLES}
|
||||
_manifest_cache: tuple[Path, int, int, dict] | None = None
|
||||
|
||||
|
||||
def _read_manifest() -> dict:
|
||||
"""Read the manifest once per file revision in this API process."""
|
||||
global _manifest_cache
|
||||
path = (MEDIA_ROOT / "manifest.json").resolve()
|
||||
stat = path.stat()
|
||||
marker = (path, stat.st_mtime_ns, stat.st_size)
|
||||
if _manifest_cache and _manifest_cache[:3] == marker:
|
||||
return _manifest_cache[3]
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(manifest, dict):
|
||||
raise ValueError("media manifest must be an object")
|
||||
_manifest_cache = (*marker, manifest)
|
||||
return manifest
|
||||
|
||||
|
||||
def _source_system(source_page: object) -> str:
|
||||
@@ -30,7 +46,7 @@ def _source_system(source_page: object) -> str:
|
||||
|
||||
|
||||
def media_manifest_version() -> int:
|
||||
manifest = json.loads((MEDIA_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
manifest = _read_manifest()
|
||||
return max(1, int(manifest.get("version", 1)))
|
||||
|
||||
|
||||
@@ -47,7 +63,7 @@ def _public_role_allowed(entity_type: str | None, role: object) -> bool:
|
||||
|
||||
|
||||
def published_assets(entity_type: str | None = None, media_role: str | None = None) -> list[dict]:
|
||||
manifest = json.loads((MEDIA_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
manifest = _read_manifest()
|
||||
result = []
|
||||
for item in manifest.get("assets", []):
|
||||
if item.get("status") != "approved" or not item.get("sha256") or not item.get("local_path"):
|
||||
@@ -90,7 +106,7 @@ def published_assets(entity_type: str | None = None, media_role: str | None = No
|
||||
def published_file(digest: str) -> tuple[Path, str] | None:
|
||||
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
|
||||
return None
|
||||
manifest = json.loads((MEDIA_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
manifest = _read_manifest()
|
||||
item = next((row for row in manifest.get("assets", []) if row.get("status") == "approved" and row.get("sha256") == digest), None)
|
||||
media_type = None
|
||||
local_path = None
|
||||
@@ -118,7 +134,7 @@ def review_assets(
|
||||
entity_type: str | None = None, status: str | None = None, media_role: str | None = None,
|
||||
query: str | None = None,
|
||||
) -> list[dict]:
|
||||
manifest = json.loads((MEDIA_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
manifest = _read_manifest()
|
||||
result = []
|
||||
for item in manifest.get("assets", []):
|
||||
item_status = str(item.get("status") or "")
|
||||
@@ -161,7 +177,7 @@ def review_assets(
|
||||
def review_file(digest: str) -> tuple[Path, str] | None:
|
||||
if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
|
||||
return None
|
||||
manifest = json.loads((MEDIA_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
manifest = _read_manifest()
|
||||
item = next((row for row in manifest.get("assets", []) if row.get("sha256") == digest and row.get("status") in {"approved", "upgrade_queued", "upgrade_stored"}), None)
|
||||
if not item or not item.get("local_path"):
|
||||
return None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import APIRouter, HTTPException, Query, Response
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from ..media_catalog import KNOWN_MEDIA_ROLES, published_assets, published_file
|
||||
from ..media_catalog import KNOWN_MEDIA_ROLES, media_manifest_version, published_assets, published_file
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
@@ -9,6 +9,7 @@ router = APIRouter()
|
||||
|
||||
@router.get("/api/v1/media/catalog")
|
||||
def media_catalog(
|
||||
response: Response,
|
||||
entity_type: str | None = Query(None, pattern="^(fish|waterbody|tackle|reference)$"),
|
||||
media_role: str | None = Query(None, pattern="^(waterbody_cover|waterbody_map|waterbody_depth_map|waterbody_screenshot|tackle_card|tackle_detail|rig_diagram|tackle_screenshot)$"),
|
||||
) -> list[dict]:
|
||||
@@ -16,6 +17,8 @@ def media_catalog(
|
||||
raise HTTPException(status_code=422, detail="media_role requires waterbody or tackle entity_type")
|
||||
if media_role and media_role not in KNOWN_MEDIA_ROLES:
|
||||
raise HTTPException(status_code=422, detail="unknown media role")
|
||||
response.headers["Cache-Control"] = "public, max-age=60, stale-while-revalidate=60"
|
||||
response.headers["X-Media-Manifest-Version"] = str(media_manifest_version())
|
||||
return published_assets(entity_type, media_role)
|
||||
|
||||
|
||||
|
||||
@@ -32,3 +32,19 @@ def test_media_catalog_does_not_infer_official_source_from_url_substrings(tmp_pa
|
||||
|
||||
assert rows["e" * 64]["source_system"] == "unknown"
|
||||
assert rows["f" * 64]["source_system"] == "rf4db"
|
||||
|
||||
|
||||
def test_media_manifest_cache_invalidates_after_revision(tmp_path, monkeypatch) -> None:
|
||||
manifest_path = tmp_path / "manifest.json"
|
||||
manifest_path.write_text(json.dumps({"version": 1, "assets": [
|
||||
{"status": "approved", "sha256": "a" * 64, "local_path": "one.webp", "entity_type": "fish", "entity_key": "pike"},
|
||||
]}), encoding="utf-8")
|
||||
monkeypatch.setattr(media_catalog, "MEDIA_ROOT", tmp_path)
|
||||
assert [row["id"] for row in media_catalog.published_assets()] == ["a" * 64]
|
||||
|
||||
manifest_path.write_text(json.dumps({"version": 2, "assets": [
|
||||
{"status": "approved", "sha256": "b" * 64, "local_path": "two.webp", "entity_type": "fish", "entity_key": "pike"},
|
||||
]}), encoding="utf-8")
|
||||
|
||||
assert media_catalog.media_manifest_version() == 2
|
||||
assert [row["id"] for row in media_catalog.published_assets()] == ["b" * 64]
|
||||
|
||||
Reference in New Issue
Block a user