Vue dézoomée en mosaïque d'overview et cache immutable des images versionnées

This commit is contained in:
Antoine Jacquin
2026-09-20 00:26:18 +02:00
parent 3236d56a4b
commit 342a0d2213
5 changed files with 563 additions and 50 deletions

View File

@ -385,9 +385,10 @@ def test_merge_remote_index_merges_files(tmp_path, monkeypatch):
"stats": {"n_tiles": 2}}), encoding="utf-8")
(tmp_path / "index.html").write_text(
"<html><script>const TILES = " + _json.dumps([locale, perimee])
+ ";</script></html>", encoding="utf-8")
+ ";</script>\nconst OVERVIEW = {};\n</script></html>", encoding="utf-8")
monkeypatch.setattr(webapp, "_remote_tiles_data", lambda: {
"tiles": [dist_neuve, dist_seule],
"overview": {"hillshade_multi": {"url": "index_overview/x.webp?v=2"}},
"viz_meta": {"aspect": {"label": "Aspect distant"}},
"stats": {"n_tiles": 2},
})
@ -398,10 +399,12 @@ def test_merge_remote_index_merges_files(tmp_path, monkeypatch):
assert "distante-neuve" in names and "locale" in names and "distante-seule" in names
assert "locale-perimee" not in names
assert data["viz_meta"]["aspect"]["label"] == "Aspect distant"
# La coquille embarque la même liste fusionnée.
# La coquille embarque la même liste fusionnée (et l'overview distante).
html = (tmp_path / "index.html").read_text(encoding="utf-8")
assert "distante-neuve" in html and "distante-seule" in html
assert "locale-perimee" not in html
assert "const OVERVIEW = {\"hillshade_multi\"" in html
assert data["overview"]["hillshade_multi"]["url"].endswith("?v=2")
# Worker injoignable : fichiers laissés intacts.
before = (tmp_path / "index_tiles.json").read_text(encoding="utf-8")
monkeypatch.setattr(webapp, "_remote_tiles_data", lambda: None)
@ -409,12 +412,13 @@ def test_merge_remote_index_merges_files(tmp_path, monkeypatch):
assert (tmp_path / "index_tiles.json").read_text(encoding="utf-8") == before
def test_static_mounts_served_no_cache(tmp_path):
"""Les montages d'images servent Cache-Control: no-cache.
def test_static_mounts_cache_headers(tmp_path):
"""Montages d'images : no-cache par défaut, immutable si l'URL porte ?v=.
Le ?v= des URLs suit la mtime de la source, pas celle du fichier servi
(vignette recalculée, cache webapp rapatrié) : sans revalidation imposée,
le cache heuristique du navigateur peut afficher l'ancien rendu.
?v= suit la mtime du fichier servi (posée par build_index) : le contenu
désigné ne change jamais → cache navigateur 1 an sans revalidation. Sans
?v= (index.html, rejeu manuel), no-cache force la revalidation 304 — un
rendu régénéré ne reste jamais périmé à l'écran.
"""
import asyncio
from starlette.routing import Mount
@ -422,7 +426,8 @@ def test_static_mounts_served_no_cache(tmp_path):
assert issubclass(webapp._OnDemandStaticFiles, webapp._NoCacheStaticFiles)
mounted = {r.path: r for r in webapp.app.routes if isinstance(r, Mount)}
for name in ("index_thumbs", "index_subtiles", "visualisations", "DTM"):
for name in ("index_thumbs", "index_subtiles", "index_overview",
"visualisations", "DTM"):
route = mounted.get(f"/{name}")
assert route is not None, f"montage /{name} absent"
assert isinstance(route.app, webapp._NoCacheStaticFiles)
@ -431,13 +436,15 @@ def test_static_mounts_served_no_cache(tmp_path):
(tmp_path / "x.jpg").write_bytes(b"1")
srv = webapp._NoCacheStaticFiles(directory=str(tmp_path))
async def fetch():
async def fetch(query):
scope = {"type": "http", "method": "GET", "path": "/x.jpg",
"headers": [], "query_string": b""}
"headers": [], "query_string": query}
return await srv.get_response("x.jpg", scope)
resp = asyncio.run(fetch())
resp = asyncio.run(fetch(b""))
assert resp.headers["cache-control"] == "no-cache, must-revalidate"
resp = asyncio.run(fetch(b"v=1712345678901"))
assert resp.headers["cache-control"] == "public, max-age=31536000, immutable"
def test_stop_generation(monkeypatch):