fix(map): fingerprint static assets on release
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;600;700;800&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="styles.css?v=20260812113000">
|
<link rel="stylesheet" href="styles.css?v=__ASSET_VERSION__">
|
||||||
|
|
||||||
<!-- 高德地图 (AutoNavi AMap) Web JS API v2.0 Configuration -->
|
<!-- 高德地图 (AutoNavi AMap) Web JS API v2.0 Configuration -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@@ -257,6 +257,6 @@
|
|||||||
|
|
||||||
<!-- 生产发布包不携带 node_modules;固定版本的 CDN 让中文地点保持拼音/首字母搜索能力。 -->
|
<!-- 生产发布包不携带 node_modules;固定版本的 CDN 让中文地点保持拼音/首字母搜索能力。 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/pinyin-pro@3.28.2/dist/index.js" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/pinyin-pro@3.28.2/dist/index.js" crossorigin="anonymous"></script>
|
||||||
<script src="app.js?v=20260812113000"></script>
|
<script src="app.js?v=__ASSET_VERSION__"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ from urllib.request import Request, urlopen
|
|||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def _static_asset_version():
|
||||||
|
"""Content fingerprint used to bypass upstream static-asset caches on release."""
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for filename in ("app.js", "styles.css"):
|
||||||
|
digest.update((ROOT / filename).read_bytes())
|
||||||
|
return digest.hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
STATIC_ASSET_VERSION = _static_asset_version()
|
||||||
HOST = os.getenv("VEHICLE_MAP_HOST", "0.0.0.0")
|
HOST = os.getenv("VEHICLE_MAP_HOST", "0.0.0.0")
|
||||||
PORT = int(os.getenv("VEHICLE_MAP_PORT", "20800"))
|
PORT = int(os.getenv("VEHICLE_MAP_PORT", "20800"))
|
||||||
OPEN_PLATFORM_BASE_URL = os.getenv("OPEN_PLATFORM_BASE_URL", "https://open.d.lnoneos.com").rstrip("/")
|
OPEN_PLATFORM_BASE_URL = os.getenv("OPEN_PLATFORM_BASE_URL", "https://open.d.lnoneos.com").rstrip("/")
|
||||||
@@ -293,6 +304,9 @@ class VehicleMapHandler(SimpleHTTPRequestHandler):
|
|||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
path = urlparse(self.path).path
|
path = urlparse(self.path).path
|
||||||
|
if path in {"/", "/index.html"}:
|
||||||
|
self._serve_index()
|
||||||
|
return
|
||||||
if path == "/api/health":
|
if path == "/api/health":
|
||||||
self._write_json(200, {
|
self._write_json(200, {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
@@ -380,6 +394,21 @@ class VehicleMapHandler(SimpleHTTPRequestHandler):
|
|||||||
})
|
})
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _serve_index(self):
|
||||||
|
try:
|
||||||
|
template = (ROOT / "index.html").read_text(encoding="utf-8")
|
||||||
|
body = template.replace("__ASSET_VERSION__", STATIC_ASSET_VERSION).encode("utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
self.log_error("index template unavailable: %s", exc)
|
||||||
|
self.send_error(500, "Map application is temporarily unavailable")
|
||||||
|
return
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.send_header("Cache-Control", "no-cache")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
def end_headers(self):
|
def end_headers(self):
|
||||||
if self.path == "/" or self.path.split("?", 1)[0].endswith((".html", ".css", ".js", ".svg")):
|
if self.path == "/" or self.path.split("?", 1)[0].endswith((".html", ".css", ".js", ".svg")):
|
||||||
self.send_header("Cache-Control", "no-cache")
|
self.send_header("Cache-Control", "no-cache")
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ class DashboardTest(unittest.TestCase):
|
|||||||
self.assertNotIn("totalHydrogenKg", station)
|
self.assertNotIn("totalHydrogenKg", station)
|
||||||
self.assertNotIn("internalOwner", station)
|
self.assertNotIn("internalOwner", station)
|
||||||
|
|
||||||
|
def test_static_asset_version_is_a_content_fingerprint(self):
|
||||||
|
self.assertEqual(len(server.STATIC_ASSET_VERSION), 16)
|
||||||
|
self.assertRegex(server.STATIC_ASSET_VERSION, r"^[0-9a-f]{16}$")
|
||||||
|
index_template = (Path(__file__).parents[1] / "index.html").read_text(encoding="utf-8")
|
||||||
|
self.assertEqual(index_template.count("__ASSET_VERSION__"), 2)
|
||||||
|
|
||||||
def test_signed_operations_session_requires_the_operations_scope(self):
|
def test_signed_operations_session_requires_the_operations_scope(self):
|
||||||
with patch.object(server, "LOCAL_ACCESS_CODE", "LN-map-test-code"), patch.object(server, "SESSION_SECRET", b"test-session-secret"), patch.object(server, "SESSION_TTL_SECONDS", 600):
|
with patch.object(server, "LOCAL_ACCESS_CODE", "LN-map-test-code"), patch.object(server, "SESSION_SECRET", b"test-session-secret"), patch.object(server, "SESSION_TTL_SECONDS", 600):
|
||||||
principal = server._authorizer().authorize_access_code("LN-map-test-code")
|
principal = server._authorizer().authorize_access_code("LN-map-test-code")
|
||||||
|
|||||||
Reference in New Issue
Block a user