summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormayx <mayx@outlook.com>2026-10-03 07:57:00 +0000
committermayx <mayx@outlook.com>2026-10-03 07:57:00 +0000
commit8cb782d1ffcdd439990a5e04e5d731c288319d11 (patch)
tree6e8041b66521d538989faa7d6a86faa1da082bd9
parent7659d3e9d56e65d948a1784789d9ebbeb8751de9 (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.yml1
-rw-r--r--_layouts/default.html3
-rw-r--r--_tools/envs_post-receive5
-rw-r--r--_tools/serv00_post-receive4
-rw-r--r--assets/js/offline-page.js119
-rw-r--r--assets/js/offline-unpack.js269
-rw-r--r--offline.md37
-rw-r--r--sw.js140
-rw-r--r--vendor/libarchive/LICENSE21
-rw-r--r--vendor/libarchive/wasm-gen/libarchive.js15
-rw-r--r--vendor/libarchive/wasm-gen/libarchive.wasmbin0 -> 896488 bytes
-rw-r--r--vendor/libarchive/worker-bundle.js1
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>
diff --git a/sw.js b/sw.js
new file mode 100644
index 0000000..106a0f1
--- /dev/null
+++ b/sw.js
@@ -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"]]