Version static asset URLs to prevent stale proxy browser resources

This commit is contained in:
Codex
2026-10-03 20:05:05 +08:00
parent 8d58ad0a2f
commit 80f8bd72af
5 changed files with 22 additions and 4 deletions
+4
View File
@@ -117,3 +117,7 @@ JSON 导出升级为格式 version 2,包含人物、关系和当前账号有
### 升级及数据 ### 升级及数据
启动时幂等创建 `family_events` 表,无需修改既有人物、账号、摄像头或录像记录。部署前使用 SQLite backup API 备份数据库。还原备份会回退备份后的数据,旧版本看不到记事表;应保留升级后的数据库备份,不以旧版本界面作为记事恢复工具。源码中不含真实家族记事和人物信息。 启动时幂等创建 `family_events` 表,无需修改既有人物、账号、摄像头或录像记录。部署前使用 SQLite backup API 备份数据库。还原备份会回退备份后的数据,旧版本看不到记事表;应保留升级后的数据库备份,不以旧版本界面作为记事恢复工具。源码中不含真实家族记事和人物信息。
### 代理缓存兼容(v0.1.17)
入口 HTML 禁止缓存;页面引用的脚本和样式自动带上发布版本号,避免 NPM 等反向代理为旧资源设置长缓存后,新页面混用旧脚本。升级时普通重新载入即可获取与页面一致的资源。
+1 -1
View File
@@ -1 +1 @@
0.1.16 0.1.17
+3 -1
View File
@@ -794,6 +794,8 @@ class Handler(BaseHTTPRequestHandler):
raise Problem('页面不存在', 404) raise Problem('页面不存在', 404)
file = ROOT / 'web' / files[path] file = ROOT / 'web' / files[path]
raw = file.read_bytes() raw = file.read_bytes()
if path == '/':
raw = raw.replace(b'__ASSET_VERSION__', (ROOT / 'VERSION').read_bytes().strip())
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', (mimetypes.guess_type(file.name)[0] or 'application/octet-stream') + '; charset=utf-8') self.send_header('Content-Type', (mimetypes.guess_type(file.name)[0] or 'application/octet-stream') + '; charset=utf-8')
self.send_header('Content-Length', str(len(raw))) self.send_header('Content-Length', str(len(raw)))
@@ -801,7 +803,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_header('X-Frame-Options', 'DENY') self.send_header('X-Frame-Options', 'DENY')
self.send_header('Referrer-Policy', 'same-origin') self.send_header('Referrer-Policy', 'same-origin')
self.send_header('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; media-src 'self' blob:; worker-src 'self' blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'") self.send_header('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; media-src 'self' blob:; worker-src 'self' blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'")
self.send_header('Cache-Control', 'no-cache') self.send_header('Cache-Control', 'no-store' if path == '/' else 'no-cache')
self.end_headers() self.end_headers()
self.wfile.write(raw) self.wfile.write(raw)
except Problem as e: except Problem as e:
+12
View File
@@ -72,6 +72,18 @@ class WebTests(unittest.TestCase):
self.assertEqual(re.findall(r'<option value="(.*?)">(.*?)</option>', select), self.assertEqual(re.findall(r'<option value="(.*?)">(.*?)</option>', select),
[('hd', '主码流'), ('compat', '子码流')]) [('hd', '主码流'), ('compat', '子码流')])
def test_asset_urls_follow_release_version(self):
code, headers, body = self.request('/', False)
self.assertEqual(code, 200)
self.assertEqual(headers['Cache-Control'], 'no-store')
self.assertNotIn(b'__ASSET_VERSION__', body)
assets = re.findall(r'(?:src|href)="(/[^"?]+\.(?:js|css)\?v=[^"]+)"', body.decode())
self.assertEqual(len(assets), 13)
version = (app.ROOT / 'VERSION').read_text().strip()
for asset in assets:
self.assertTrue(asset.endswith('?v=' + version))
self.assertEqual(self.request(asset, False)[0], 200)
def test_origin_rejected(self): def test_origin_rejected(self):
self.assertEqual(self.request('/api/storage', data={}, origin='https://example.org')[0], 403) self.assertEqual(self.request('/api/storage', data={}, origin='https://example.org')[0], 403)
+2 -2
View File
@@ -2,8 +2,8 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>赵府智家 · 家庭空间</title><link rel="icon" type="image/png" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><meta name="theme-color" content="#10191d"><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/family-map.css"> <title>赵府智家 · 家庭空间</title><link rel="icon" type="image/png" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><meta name="theme-color" content="#10191d"><link rel="stylesheet" href="/style.css?v=__ASSET_VERSION__"><link rel="stylesheet" href="/family-map.css?v=__ASSET_VERSION__">
<script defer src="/vendor/hls.min.js"></script><script defer src="/live-player.js"></script><script defer src="/kinship.js"></script><script defer src="/family-graph.js"></script><script defer src="/calendar.js"></script><script defer src="/app.js"></script><script defer src="/monitor.js"></script><script defer src="/date-fields.js"></script><script defer src="/family-map.js"></script><script defer src="/family.js"></script><script defer src="/family-events.js"></script> <script defer src="/vendor/hls.min.js?v=__ASSET_VERSION__"></script><script defer src="/live-player.js?v=__ASSET_VERSION__"></script><script defer src="/kinship.js?v=__ASSET_VERSION__"></script><script defer src="/family-graph.js?v=__ASSET_VERSION__"></script><script defer src="/calendar.js?v=__ASSET_VERSION__"></script><script defer src="/app.js?v=__ASSET_VERSION__"></script><script defer src="/monitor.js?v=__ASSET_VERSION__"></script><script defer src="/date-fields.js?v=__ASSET_VERSION__"></script><script defer src="/family-map.js?v=__ASSET_VERSION__"></script><script defer src="/family.js?v=__ASSET_VERSION__"></script><script defer src="/family-events.js?v=__ASSET_VERSION__"></script>
</head> </head>
<body> <body>
<aside class="sidebar"> <aside class="sidebar">