diff options
| author | mayx <mayx@outlook.com> | 2026-10-03 07:57:00 +0000 |
|---|---|---|
| committer | mayx <mayx@outlook.com> | 2026-10-03 07:57:00 +0000 |
| commit | 8cb782d1ffcdd439990a5e04e5d731c288319d11 (patch) | |
| tree | 6e8041b66521d538989faa7d6a86faa1da082bd9 | |
| parent | 7659d3e9d56e65d948a1784789d9ebbeb8751de9 (diff) | |
Update 12 files
- /vendor/libarchive/LICENSE
- /vendor/libarchive/worker-bundle.js
- /vendor/libarchive/wasm-gen/libarchive.js
- /vendor/libarchive/wasm-gen/libarchive.wasm
- /sw.js
- /offline.md
- /_layouts/default.html
- /_tools/serv00_post-receive
- /_tools/envs_post-receive
- /_data/proxylist.yml
- /assets/js/offline-page.js
- /assets/js/offline-unpack.js
| -rw-r--r-- | _data/proxylist.yml | 1 | ||||
| -rw-r--r-- | _layouts/default.html | 3 | ||||
| -rw-r--r-- | _tools/envs_post-receive | 5 | ||||
| -rw-r--r-- | _tools/serv00_post-receive | 4 | ||||
| -rw-r--r-- | assets/js/offline-page.js | 119 | ||||
| -rw-r--r-- | assets/js/offline-unpack.js | 269 | ||||
| -rw-r--r-- | offline.md | 37 | ||||
| -rw-r--r-- | sw.js | 140 | ||||
| -rw-r--r-- | vendor/libarchive/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/libarchive/wasm-gen/libarchive.js | 15 | ||||
| -rw-r--r-- | vendor/libarchive/wasm-gen/libarchive.wasm | bin | 0 -> 896488 bytes | |||
| -rw-r--r-- | vendor/libarchive/worker-bundle.js | 1 |
12 files changed, 610 insertions, 5 deletions
diff --git a/_data/proxylist.yml b/_data/proxylist.yml index 4cb705e..35ef0c9 100644 --- a/_data/proxylist.yml +++ b/_data/proxylist.yml @@ -17,7 +17,6 @@ mirrors: - https://mayx.vercel.app/ - https://mayx.netlify.app/ - https://mayx.gitnet.page/ -- https://mayx.stormkit.dev/ - https://mayx.grebedoc.dev/ - https://mayx.codeberg.page/ - https://mayx.tildepages.org/ diff --git a/_layouts/default.html b/_layouts/default.html index 616f962..13c7b59 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -98,7 +98,8 @@ layout: xslt_container <li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li> <li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li> {% else %} - <li style="width: 270px; border-right: none;"><a href="/MayxBlog.7z">Download <strong>7-Zip File</strong></a></li> + <li style="width: 174px;"><a href="/MayxBlog.7z">Download <strong>7-Zip File</strong></a></li> + <li style="width: 174px;"><a href="/offline.html">Manage <strong>Offline Mode</strong></a></li> {% endif %} </ul> </header> diff --git a/_tools/envs_post-receive b/_tools/envs_post-receive index fa0f7c6..1df73b4 100644 --- a/_tools/envs_post-receive +++ b/_tools/envs_post-receive @@ -7,7 +7,8 @@ mkdir Mabbs curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md bundle exec jekyll build -d ../public_html python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../public_html/ MayxBlog.7z -mv MayxBlog.7z ../public_html/ +7z a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z +mv MayxBlog-new.7z ../public_html/MayxBlog.7z cd ../public_html/ unset GIT_DIR git init @@ -18,6 +19,6 @@ git branch pages git remote add codeberg ssh://git@codeberg.org/mayx/pages.git git remote add bitbucket ssh://git@bitbucket.org/unmayx/unmayx.bitbucket.io.git git push -f codeberg main -git push -f codeberg pages +git push -f codeberg pages git push -f bitbucket main /home/mayx/blog-env/node_modules/surge/bin/surge /home/mayx/public_html/ mayx.surge.sh
\ No newline at end of file diff --git a/_tools/serv00_post-receive b/_tools/serv00_post-receive index f0192dc..4ad6077 100644 --- a/_tools/serv00_post-receive +++ b/_tools/serv00_post-receive @@ -7,13 +7,15 @@ mkdir Mabbs curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md bundle exec jekyll build -d ../domains/mayx.serv00.net/public_html/ python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../domains/mayx.serv00.net/public_html/ MayxBlog.7z -mv MayxBlog.7z ../domains/mayx.serv00.net/public_html/ +7zz a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z +mv MayxBlog-new.7z ../domains/mayx.serv00.net/public_html/MayxBlog.7z rsync -avz --delete ../domains/mayx.serv00.net/public_html/ mayx@web.sourceforge.net:/home/project-web/mayx/htdocs/ cd ../domains/mayx.serv00.net/public_html/ unset GIT_DIR git init git lfs install git lfs track "*.png" +git lfs track "*.webp" git lfs track "*.moc" git lfs track "*.tgz" git lfs track "*.jpg" diff --git a/assets/js/offline-page.js b/assets/js/offline-page.js new file mode 100644 index 0000000..8b0bdb1 --- /dev/null +++ b/assets/js/offline-page.js @@ -0,0 +1,119 @@ +/* + * Mayx的博客 · /offline.html 控制页控制器(classic script,PJAX 兼容) + * + * 本体是 IIFE,不含模块语法:PJAX 换页时 jQuery 会重新执行本文件, + * 每次执行都重新查询元素、重新绑定事件,天然幂等。真正的解包逻辑在 + * ES module /assets/js/offline-unpack.js 中,通过动态 import() 加载 + * (浏览器模块缓存保证 PJAX 反复进入本页也不会重复加载)。 + * 注意:本文件经 Liquid 原样输出,不能使用模板语法字符。 + */ + +(function () { + var MODULE_URL = '/assets/js/offline-unpack.js'; + // 压缩包大致量级(当前约 27MB),仅用于进度条比例,不影响正确性 + var EXPECTED_BYTES = 27 * 1024 * 1024; + var busy = false; + + function el(id) { return document.getElementById(id); } + function fmtMB(bytes) { return (bytes / 1048576).toFixed(1) + ' MB'; } + + function setMessage(text) { + el('offline-message').textContent = text || ''; + } + + function showProgress(show) { + el('offline-progress-box').hidden = !show; + } + + function render(meta) { + var online = !meta; + el('offline-state').textContent = online + ? '在线模式(离线未启用)' + : '离线模式(优先从本地缓存加载)'; + el('offline-version').textContent = online ? '-' : fmtMB(meta.payloadSize) + '(压缩包大小即版本号)'; + el('offline-files').textContent = online ? '-' : meta.files + ' 个文件 / ' + fmtMB(meta.bytes); + el('offline-time').textContent = online ? '-' : new Date(meta.unpackedAt).toLocaleString(); + el('offline-persist').textContent = online ? '-' : (meta.persisted ? '已授权(缓存不会被自动回收)' : '未授权(浏览器空间紧张时可能回收缓存)'); + el('offline-enable').disabled = busy || !online; + el('offline-disable').disabled = busy || online; + } + + function onProgress(info) { + if (info.done) return; + el('offline-progress').value = Math.min(100, (info.bytes / EXPECTED_BYTES) * 100); + el('offline-progress-text').textContent = + '已解包 ' + info.files + ' 个文件(' + fmtMB(info.bytes) + ')…'; + } + + function withModule(fn) { + return import(MODULE_URL).then(fn); + } + + function refresh() { + if (!('serviceWorker' in navigator) || !('caches' in window)) { + el('offline-state').textContent = '当前浏览器不支持离线模式(需要 Service Worker)'; + el('offline-enable').disabled = true; + el('offline-disable').disabled = true; + return; + } + withModule(function (mod) { return mod.ensureFresh(); }).then(function (fresh) { + render(fresh.ready ? fresh.meta : null); + if (fresh.stale) { + setMessage('检测到站点压缩包已更新,离线缓存已自动失效,当前处于在线模式。可重新启用离线模式。'); + } + }).catch(function (e) { + setMessage('状态检测失败:' + (e && e.message ? e.message : e)); + }); + } + + function onEnable() { + busy = true; + render(null); + showProgress(true); + setMessage('正在下载并解包站点压缩包,请保持网络连接…'); + withModule(function (mod) { return mod.activateOffline({ onProgress: onProgress }); }) + .then(function (meta) { + el('offline-progress').value = 100; + el('offline-progress-text').textContent = '解包完成'; + setMessage('离线模式已启用。之后访问本博客将优先使用本地缓存,断网也能阅读。'); + busy = false; + showProgress(false); + render(meta); // 立即展示离线状态,不必等后台刷新 + }) + .catch(function (e) { + setMessage('启用失败:' + (e && e.message ? e.message : e)); + busy = false; + showProgress(false); + refresh(); + }); + } + + function onDisable() { + busy = true; + setMessage(''); + withModule(function (mod) { return mod.deactivateOffline(); }) + .then(function () { + setMessage('已切回在线模式,之后页面将通过网络加载。'); + busy = false; + render(null); + if (navigator.onLine) { + setTimeout(function () { location.reload(); }, 800); + } + }) + .catch(function (e) { + setMessage('操作失败:' + (e && e.message ? e.message : e)); + busy = false; + refresh(); + }); + } + + function bind() { + var enable = el('offline-enable'); + if (!enable) return; // 不在控制页(PJAX 容错) + enable.addEventListener('click', onEnable); + el('offline-disable').addEventListener('click', onDisable); + refresh(); + } + + bind(); +})(); diff --git a/assets/js/offline-unpack.js b/assets/js/offline-unpack.js new file mode 100644 index 0000000..07893bd --- /dev/null +++ b/assets/js/offline-unpack.js @@ -0,0 +1,269 @@ +/* + * Mayx的博客 · 离线模式核心逻辑(/offline.html 控制页使用,ES module) + * + * - activateOffline():注册 SW,下载站点压缩包 /MayxBlog.7z,经 libarchive(WASM) + * 流式解包,把每个文件按站点路径写入 Cache Storage,最后写入就绪标记 + * (清单 + payloadSize 版本号),自此进入离线模式。 + * - ensureFresh():HEAD 探测线上压缩包大小,与标记中的 payloadSize 不一致 + * 说明站点已更新 → 清空缓存,回退在线模式。 + * - deactivateOffline():清空缓存,手动切回在线模式。 + * - readStatus():读取当前离线状态,供控制页展示。 + * + */ + +const ARCHIVE_URL = '/MayxBlog.7z'; +const ARCHIVE_ROOT = 'blog/'; +const CACHE_NAME = 'mayxblog-offline-v1'; +const READY_KEY = '/__offline_manifest__'; +const WORKER_URL = '/vendor/libarchive/worker-bundle.js'; + +const MIME = { + html: 'text/html; charset=utf-8', + htm: 'text/html; charset=utf-8', + css: 'text/css; charset=utf-8', + js: 'application/javascript; charset=utf-8', + mjs: 'application/javascript; charset=utf-8', + json: 'application/json; charset=utf-8', + xml: 'application/xml; charset=utf-8', + xsl: 'text/xsl; charset=utf-8', + opml: 'text/x-opml; charset=utf-8', + txt: 'text/plain; charset=utf-8', + md: 'text/markdown; charset=utf-8', + csv: 'text/csv; charset=utf-8', + svg: 'image/svg+xml', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + avif: 'image/avif', + ico: 'image/x-icon', + bmp: 'image/bmp', + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + otf: 'font/otf', + eot: 'application/vnd.ms-fontobject', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + webm: 'video/webm', + '7z': 'application/x-7z-compressed', + zip: 'application/zip', + wasm: 'application/wasm', + mtn: 'application/octet-stream', + moc: 'application/octet-stream', + pdf: 'application/pdf', +}; + +function mimeFor(pathname) { + const dot = pathname.lastIndexOf('.'); + const ext = dot === -1 ? '' : pathname.slice(dot + 1).toLowerCase(); + return MIME[ext] || 'application/octet-stream'; +} + +function sitePathFor(entryPath) { + const normalized = entryPath.replace(/\\/g, '/'); + if (!normalized.startsWith(ARCHIVE_ROOT)) return null; + const relative = normalized.slice(ARCHIVE_ROOT.length); + if (!relative) return null; + + const segments = []; + for (const segment of relative.split('/')) { + if (!segment || segment === '.') continue; + if (segment === '..') return null; + segments.push(segment); + } + if (!segments.length) return null; + return '/' + segments.join('/'); +} + +/* + * 在独立 worker 中打开一层压缩包并流式吐出条目; + * 返回其中根级嵌套压缩包(下一层包装)的原始字节。 + */ +function extractLayer(file, onEntry) { + return new Promise((resolve, reject) => { + const worker = new Worker(WORKER_URL); + const nested = []; + let settled = false; + + const finish = (error) => { + if (settled) return; + settled = true; + worker.terminate(); + if (error) reject(error); + else resolve(nested); + }; + + worker.addEventListener('error', (event) => { + finish(new Error(event.message || 'libarchive worker failed')); + }); + + worker.addEventListener('message', ({ data }) => { + if (data.type === 'ERROR') { + finish(new Error(data.error && data.error.message ? data.error.message : 'libarchive failed')); + return; + } + if (data.type === 'READY') { + worker.postMessage({ type: 'OPEN', file }); + return; + } + if (data.type === 'OPENED') { + worker.postMessage({ type: 'EXTRACT_FILES' }); + return; + } + if (data.type === 'END') { + finish(null); + return; + } + if (data.type !== 'ENTRY') return; + + const entry = data.entry; + if (entry.type !== 'FILE' || !entry.fileData) return; + + // 根级压缩包是下一层包装,不是站点内容 + if (entry.path.replace(/\\/g, '/') === 'MayxBlog.7z') { + nested.push(entry.fileData); + return; + } + onEntry(entry); + }); + }); +} + +/* 几十 MB 的缓存是浏览器常见的回收对象,而重新解包代价高,尽量申请持久存储。 */ +async function requestPersistence() { + if (!navigator.storage || !navigator.storage.persist) return false; + try { + if (await navigator.storage.persisted()) return true; + return await navigator.storage.persist(); + } catch { + return false; + } +} + +/* 读取就绪标记(= 离线模式状态);损坏或缺清单的标记视为不存在。 */ +export async function readStatus() { + const cache = await caches.open(CACHE_NAME); + const hit = await cache.match(READY_KEY); + if (!hit) return null; + const meta = await hit.json().catch(() => null); + if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null; + return meta; +} + +/* + * 线上压缩包的大小即版本号。HEAD 不被 SW 拦截,拿到的一定是线上真实值; + * 网络失败返回 null,调用方选择信任现有缓存而不是误伤。 + */ +async function payloadSize() { + try { + const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' }); + if (!response.ok) return null; + const size = Number(response.headers.get('content-length')); + return Number.isFinite(size) && size > 0 ? size : null; + } catch { + return null; + } +} + +/* + * 控制页打开时调用:未启用 → {ready:false};已启用且版本一致 → {ready:true, meta}; + * 版本不一致 → 清空缓存并返回 {ready:false, stale:true}(已回退在线模式)。 + */ +export async function ensureFresh() { + const cache = await caches.open(CACHE_NAME); + const size = await payloadSize(); + const meta = await readStatus(); + + if (!meta) return { ready: false, size, stale: false }; + if (size && meta.payloadSize !== size) { + await caches.delete(CACHE_NAME); + return { ready: false, size, stale: true }; + } + return { ready: true, size, stale: false, meta }; +} + +/* + * 启用离线模式:注册 SW → 清空旧缓存(同时清掉上次中断的残留,保证随后 + * 的压缩包下载不会被旧缓存喂旧的 quine 副本)→ 下载并逐层解包 → 写标记。 + */ +export async function activateOffline({ onProgress } = {}) { + if (!('serviceWorker' in navigator)) { + throw new Error('当前浏览器不支持 Service Worker,无法启用离线模式。'); + } + await navigator.serviceWorker.register('/sw.js', { scope: '/' }); + await navigator.serviceWorker.ready; + await caches.delete(CACHE_NAME); + + const persisted = await requestPersistence(); + + // 写操作串行链,保证按到达顺序落盘且不产生无界并发 + const cache = await caches.open(CACHE_NAME); + let writes = Promise.resolve(); + const keys = new Set(); + let files = 0; + let bytes = 0; + let skipped = 0; + + const response = await fetch(ARCHIVE_URL, { cache: 'no-store' }); + if (!response.ok) throw new Error(`无法下载站点压缩包:HTTP ${response.status}`); + const outer = await response.arrayBuffer(); + const downloadedBytes = outer.byteLength; + + let layer = [outer]; + while (layer.length) { + const bytesThisLayer = layer.shift(); + const file = new File([bytesThisLayer], 'MayxBlog.7z'); + + layer = await extractLayer(file, (entry) => { + const key = sitePathFor(entry.path); + if (!key) { + skipped += 1; + return; + } + const body = entry.fileData; + files += 1; + bytes += body.byteLength; + + writes = writes.then(async () => { + await cache.put(key, new Response(body, { + status: 200, + headers: { 'Content-Type': mimeFor(key) }, + })); + keys.add(key); + }); + + if (onProgress) onProgress({ done: false, files, bytes, skipped }); + }); + } + + await writes; + + const meta = { + files, + bytes, + skipped, + persisted, + payloadSize: downloadedBytes, + failed: files - keys.size, + keys: [...keys], + unpackedAt: new Date().toISOString(), + }; + await cache.put( + READY_KEY, + new Response(JSON.stringify(meta), { + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }) + ); + + if (onProgress) onProgress({ done: true, cached: false, ...meta }); + return meta; +} + +/* 切回在线模式:删掉缓存(含标记),SW 随即恢复纯转发。返回之前是否处于离线模式。 */ +export async function deactivateOffline() { + const had = await readStatus(); + await caches.delete(CACHE_NAME); + return Boolean(had); +} diff --git a/offline.md b/offline.md new file mode 100644 index 0000000..f571a9f --- /dev/null +++ b/offline.md @@ -0,0 +1,37 @@ +--- +layout: default +title: 离线模式 +--- + +# 离线模式 + +本页用于管理本博客的离线阅读功能: + +- **启用离线模式**:浏览器会下载整站压缩包(`MayxBlog.7z`)并解包到本地缓存,之后即使没有网络也能正常浏览博客; +- **自动回退**:当站点压缩包更新(博客重新部署)时,离线缓存会自动失效并回退到在线模式,下次重新启用即可获取最新内容; +- **手动切换**:离线模式下可以随时在本页切回在线模式,切回后页面将重新通过网络加载。 + +<table> + <tbody> + <tr><th>当前状态</th><td id="offline-state">检测中…</td></tr> + <tr><th>缓存版本</th><td id="offline-version">-</td></tr> + <tr><th>文件统计</th><td id="offline-files">-</td></tr> + <tr><th>解包时间</th><td id="offline-time">-</td></tr> + <tr><th>持久存储</th><td id="offline-persist">-</td></tr> + </tbody> +</table> + +<p id="offline-progress-box" hidden="hidden"> + <progress id="offline-progress" max="100" value="0"></progress> + + <small id="offline-progress-text">准备中…</small> +</p> + +<p><strong id="offline-message"></strong></p> + +<p> + <button type="button" id="offline-enable">启用离线模式</button> + <button type="button" id="offline-disable">切换回在线模式</button> +</p> + +<script src="/assets/js/offline-page.js"></script> @@ -0,0 +1,140 @@ +/* + * Mayx的博客 · 离线模式 Service Worker + * + * 两种模式(由 Cache Storage 中是否存在就绪标记决定): + * - 在线模式(默认,无标记):所有请求原样转发网络,SW 形同虚设。 + * - 离线模式(/offline.html 启用后写入标记):同源 GET 请求缓存优先, + * 缓存未命中且清单里没有的路径才回源网络,实现断网阅读。 + * + * 版本控制:压缩包 /MayxBlog.7z 的 content-length 即版本号(每次重新部署 + * 都会变化)。SW 激活时、以及导航请求按 RECHECK_INTERVAL 节流复核; + * 一旦发现线上压缩包与已缓存版本不一致,立即清空缓存 → 自动回退在线模式。 + * 网络不可用时复核失败,保留缓存(离线阅读本来就是离线模式的目的)。 + */ + +const CACHE_NAME = 'mayxblog-offline-v1'; +const READY_KEY = '/__offline_manifest__'; +const ARCHIVE_URL = '/MayxBlog.7z'; +const RECHECK_INTERVAL = 5 * 60 * 1000; + +let lastRecheck = 0; + +self.addEventListener('install', () => { + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys() + .then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + .then(verifyPayloadVersion) + .then(() => self.clients.claim()) + ); +}); + +/* + * HEAD 请求不被下面的 fetch 处理器拦截,这里读到的一定是线上真实压缩包, + * 而不是缓存里的 quine 副本。网络失败返回 null,调用方选择信任现有缓存。 + */ +async function payloadSize() { + try { + const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' }); + if (!response.ok) return null; + const size = Number(response.headers.get('content-length')); + return Number.isFinite(size) && size > 0 ? size : null; + } catch { + return null; + } +} + +/* + * 就绪标记同时是清单:记录了解包写入的所有路径。没有它就无法区分 + * “站点本来就没有这个页面”和“缓存条目被浏览器回收了”。 + */ +async function readManifest(cache) { + const hit = await cache.match(READY_KEY); + if (!hit) return null; + const meta = await hit.json().catch(() => null); + if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null; + return meta; +} + +/* 压缩包更新 → 清空缓存(即回退在线模式);返回是否发生了失效。 */ +async function verifyPayloadVersion() { + const cache = await caches.open(CACHE_NAME); + const meta = await readManifest(cache); + if (!meta) return false; + const size = await payloadSize(); + if (size && meta.payloadSize !== size) { + await caches.delete(CACHE_NAME); + return true; + } + return false; +} + +/* 服务器对目录索引的处理与 Jekyll 产物一致,这里补齐候选路径。 */ +function candidatesFor(pathname) { + const keys = [pathname]; + if (pathname.endsWith('/')) { + keys.push(pathname + 'index.html'); + } else { + keys.push(pathname + '/index.html'); + keys.push(pathname + '.html'); + } + return keys; +} + +self.addEventListener('fetch', (event) => { + const request = event.request; + if (request.method !== 'GET') return; // HEAD 等直通网络(版本探测依赖这一点) + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; // 跨域请求不干预 + + // 显式 no-store 的请求(重新下载压缩包等)绕过缓存,避免吃到旧的 quine 副本 + if (request.cache === 'no-store') return; + + event.respondWith(respond(request, url, event)); +}); + +async function respond(request, url, event) { + const cache = await caches.open(CACHE_NAME); + + // 导航请求顺带做节流版本复核;失效发生在后台,本次响应不受影响, + // 下一次请求自然落入在线模式。 + if (request.mode === 'navigate' && Date.now() - lastRecheck > RECHECK_INTERVAL) { + lastRecheck = Date.now(); + event.waitUntil(verifyPayloadVersion()); + } + + const candidates = candidatesFor(url.pathname); + for (const key of candidates) { + const hit = await cache.match(key); + if (hit) return hit; + } + + const meta = await readManifest(cache); + if (!meta) return fetch(request); // 在线模式:纯转发 + + // 清单承诺过这个路径但缓存里没有 → 条目被浏览器回收,缓存已不可信, + // 整体作废并回退在线模式。 + if (candidates.some((key) => meta.keys.includes(key))) { + await caches.delete(CACHE_NAME); + return fetch(request); + } + + // 清单之外的路径回源网络;断网时的导航请求用站点 404 页兜底。 + try { + return await fetch(request); + } catch (error) { + if (request.mode === 'navigate') { + const notFound = await cache.match('/404.html'); + if (notFound) { + return new Response(notFound.body, { status: 404, headers: notFound.headers }); + } + } + throw error; + } +} diff --git a/vendor/libarchive/LICENSE b/vendor/libarchive/LICENSE new file mode 100644 index 0000000..d7e8a1d --- /dev/null +++ b/vendor/libarchive/LICENSE @@ -0,0 +1,21 @@ +MIT License
+
+Copyright (c) 2018 ნიკა
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/libarchive/wasm-gen/libarchive.js b/vendor/libarchive/wasm-gen/libarchive.js new file mode 100644 index 0000000..6adea82 --- /dev/null +++ b/vendor/libarchive/wasm-gen/libarchive.js @@ -0,0 +1,15 @@ + +var libarchive = (function() { + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; + return ( +function(libarchive) { + libarchive = libarchive || {}; + +var Module=typeof libarchive!=="undefined"?libarchive:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}Module["arguments"]=[];Module["thisProgram"]="./this.program";Module["quit"]=function(status,toThrow){throw toThrow};Module["preRun"]=[];Module["postRun"]=[];var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof require==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}else{return scriptDirectory+path}}if(ENVIRONMENT_IS_NODE){scriptDirectory=__dirname+"/";var nodeFS;var nodePath;Module["read"]=function shell_read(filename,binary){var ret;if(!nodeFS)nodeFS=require("fs");if(!nodePath)nodePath=require("path");filename=nodePath["normalize"](filename);ret=nodeFS["readFileSync"](filename);return binary?ret:ret.toString()};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}Module["arguments"]=process["argv"].slice(2);process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});process["on"]("unhandledRejection",abort);Module["quit"]=function(status){process["exit"](status)};Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL){if(typeof read!="undefined"){Module["read"]=function shell_read(f){return read(f)}}Module["readBinary"]=function readBinary(f){var data;if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof quit==="function"){Module["quit"]=function(status){quit(status)}}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.lastIndexOf("/")+1)}else{scriptDirectory=""}Module["read"]=function shell_read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){Module["readBinary"]=function readBinary(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}Module["readAsync"]=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)};Module["setWindowTitle"]=function(title){document.title=title}}else{}var out=Module["print"]||(typeof console!=="undefined"?console.log.bind(console):typeof print!=="undefined"?print:null);var err=Module["printErr"]||(typeof printErr!=="undefined"?printErr:typeof console!=="undefined"&&console.warn.bind(console)||out);for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=undefined;function dynamicAlloc(size){var ret=HEAP32[DYNAMICTOP_PTR>>2];var end=ret+size+15&-16;if(end<=_emscripten_get_heap_size()){HEAP32[DYNAMICTOP_PTR>>2]=end}else{var success=_emscripten_resize_heap(end);if(!success)return 0}return ret}function getNativeTypeSize(type){switch(type){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(type[type.length-1]==="*"){return 4}else if(type[0]==="i"){var bits=parseInt(type.substr(1));assert(bits%8===0,"getNativeTypeSize invalid bits "+bits+", type "+type);return bits/8}else{return 0}}}}var asm2wasmImports={"f64-rem":function(x,y){return x%y},"debugger":function(){debugger}};var functionPointers=new Array(0);var tempRet0=0;var setTempRet0=function(value){tempRet0=value};if(typeof WebAssembly!=="object"){err("no native wasm support detected")}var wasmMemory;var wasmTable;var ABORT=false;var EXITSTATUS=0;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}function getCFunc(ident){var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func}function ccall(ident,returnType,argTypes,args,opts){var toC={"string":function(str){var ret=0;if(str!==null&&str!==undefined&&str!==0){var len=(str.length<<2)+1;ret=stackAlloc(len);stringToUTF8(str,ret,len)}return ret},"array":function(arr){var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string")return UTF8ToString(ret);if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func.apply(null,cArgs);ret=convertReturnValue(ret);if(stack!==0)stackRestore(stack);return ret}function cwrap(ident,returnType,argTypes,opts){argTypes=argTypes||[];var numericArgs=argTypes.every(function(type){return type==="number"});var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return function(){return ccall(ident,returnType,argTypes,arguments,opts)}}function setValue(ptr,value,type,noSafe){type=type||"i8";if(type.charAt(type.length-1)==="*")type="i32";switch(type){case"i1":HEAP8[ptr>>0]=value;break;case"i8":HEAP8[ptr>>0]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":tempI64=[value>>>0,(tempDouble=value,+Math_abs(tempDouble)>=1?tempDouble>0?(Math_min(+Math_floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math_ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[ptr>>2]=tempI64[0],HEAP32[ptr+4>>2]=tempI64[1];break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;default:abort("invalid type for setValue: "+type)}}var ALLOC_NORMAL=0;var ALLOC_NONE=3;function allocate(slab,types,allocator,ptr){var zeroinit,size;if(typeof slab==="number"){zeroinit=true;size=slab}else{zeroinit=false;size=slab.length}var singleType=typeof types==="string"?types:null;var ret;if(allocator==ALLOC_NONE){ret=ptr}else{ret=[_malloc,stackAlloc,dynamicAlloc][allocator](Math.max(size,singleType?1:types.length))}if(zeroinit){var stop;ptr=ret;assert((ret&3)==0);stop=ret+(size&~3);for(;ptr<stop;ptr+=4){HEAP32[ptr>>2]=0}stop=ret+size;while(ptr<stop){HEAP8[ptr++>>0]=0}return ret}if(singleType==="i8"){if(slab.subarray||slab.slice){HEAPU8.set(slab,ret)}else{HEAPU8.set(new Uint8Array(slab),ret)}return ret}var i=0,type,typeSize,previousType;while(i<size){var curr=slab[i];type=singleType||types[i];if(type===0){i++;continue}if(type=="i64")type="i32";setValue(ret+i,curr,type);if(previousType!==type){typeSize=getNativeTypeSize(type);previousType=type}i+=typeSize}return ret}function getMemory(size){if(!runtimeInitialized)return dynamicAlloc(size);return _malloc(size)}var UTF8Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(u8Array,idx,maxBytesToRead){var endIdx=idx+maxBytesToRead;var endPtr=idx;while(u8Array[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&u8Array.subarray&&UTF8Decoder){return UTF8Decoder.decode(u8Array.subarray(idx,endPtr))}else{var str="";while(idx<endPtr){var u0=u8Array[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=u8Array[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=u8Array[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|u8Array[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}function stringToUTF8Array(str,outU8Array,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;outU8Array[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;outU8Array[outIdx++]=192|u>>6;outU8Array[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;outU8Array[outIdx++]=224|u>>12;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;outU8Array[outIdx++]=240|u>>18;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}}outU8Array[outIdx]=0;return outIdx-startIdx}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function lengthBytesUTF8(str){var len=0;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127)++len;else if(u<=2047)len+=2;else if(u<=65535)len+=3;else len+=4}return len}var UTF16Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf-16le"):undefined;function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}function writeAsciiToMemory(str,buffer,dontAddNull){for(var i=0;i<str.length;++i){HEAP8[buffer++>>0]=str.charCodeAt(i)}if(!dontAddNull)HEAP8[buffer>>0]=0}function demangle(func){return func}function demangleAll(text){var regex=/__Z[\w\d_]+/g;return text.replace(regex,function(x){var y=demangle(x);return x===y?x:y+" ["+x+"]"})}function jsStackTrace(){var err=new Error;if(!err.stack){try{throw new Error(0)}catch(e){err=e}if(!err.stack){return"(no stack trace available)"}}return err.stack.toString()}function stackTrace(){var js=jsStackTrace();if(Module["extraStackTrace"])js+="\n"+Module["extraStackTrace"]();return demangleAll(js)}var WASM_PAGE_SIZE=65536;function alignUp(x,multiple){if(x%multiple>0){x+=multiple-x%multiple}return x}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferViews(){Module["HEAP8"]=HEAP8=new Int8Array(buffer);Module["HEAP16"]=HEAP16=new Int16Array(buffer);Module["HEAP32"]=HEAP32=new Int32Array(buffer);Module["HEAPU8"]=HEAPU8=new Uint8Array(buffer);Module["HEAPU16"]=HEAPU16=new Uint16Array(buffer);Module["HEAPU32"]=HEAPU32=new Uint32Array(buffer);Module["HEAPF32"]=HEAPF32=new Float32Array(buffer);Module["HEAPF64"]=HEAPF64=new Float64Array(buffer)}var DYNAMIC_BASE=5520464,DYNAMICTOP_PTR=277552;var TOTAL_STACK=5242880;var INITIAL_TOTAL_MEMORY=Module["TOTAL_MEMORY"]||16777216;if(INITIAL_TOTAL_MEMORY<TOTAL_STACK)err("TOTAL_MEMORY should be larger than TOTAL_STACK, was "+INITIAL_TOTAL_MEMORY+"! (TOTAL_STACK="+TOTAL_STACK+")");if(Module["buffer"]){buffer=Module["buffer"]}else{if(typeof WebAssembly==="object"&&typeof WebAssembly.Memory==="function"){wasmMemory=new WebAssembly.Memory({"initial":INITIAL_TOTAL_MEMORY/WASM_PAGE_SIZE});buffer=wasmMemory.buffer}else{buffer=new ArrayBuffer(INITIAL_TOTAL_MEMORY)}}updateGlobalBufferViews();HEAP32[DYNAMICTOP_PTR>>2]=DYNAMIC_BASE;function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback();continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){Module["dynCall_v"](func)}else{Module["dynCall_vi"](func,callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]] |
