test1:subject6
Differences
This shows you the differences between two versions of the page.
| test1:subject6 [2026/04/12 02:45] – created admin | test1:subject6 [2026/04/12 02:46] (current) – admin | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| < | < | ||
| (async () => { | (async () => { | ||
| + | const rootHandle = await window.showDirectoryPicker(); | ||
| + | |||
| const origin = location.origin; | const origin = location.origin; | ||
| - | const pageUrl | + | const currentUrl |
| - | const decoder = new TextDecoder(); | + | |
| - | const encoder = new TextEncoder(); | + | |
| - | const collected | + | const textDecoder |
| - | const fileMap = new Map(); | + | |
| - | const failed = []; | + | |
| - | const normalizeUrl = (u, base = location.href) => { | + | const normalizeUrl = (u) => { |
| try { | try { | ||
| - | const x = new URL(u, | + | const x = new URL(u, |
| if (!/ | if (!/ | ||
| - | if (x.origin !== origin) return null; | + | if (x.origin !== origin) return null; // same-origin only |
| x.hash = ""; | x.hash = ""; | ||
| return x; | return x; | ||
| Line 22: | Line 20: | ||
| }; | }; | ||
| - | const hasExt | + | const toLocalPathParts |
| + | let path = decodeURIComponent(urlObj.pathname); | ||
| - | const toLocalPath = (urlObj, isHtml = false) => { | + | if (!path || path === "/" |
| - | let path = decodeURIComponent(urlObj.pathname | + | return [" |
| + | } | ||
| - | | + | |
| - | const parts = path.split("/" | ||
| const last = parts[parts.length - 1] || ""; | const last = parts[parts.length - 1] || ""; | ||
| + | const hasExt = / | ||
| if (isHtml) { | if (isHtml) { | ||
| - | if (!hasExt(last)) parts.push(" | + | if (hasExt |
| - | return parts.join("/"); | + | return |
| + | } | ||
| + | if (hasExt) { | ||
| + | return parts; | ||
| + | } | ||
| + | return | ||
| } | } | ||
| - | if (!hasExt(last)) parts.push(" | + | if (!hasExt) |
| - | | + | return [...parts, " |
| - | | + | } |
| - | const dirname = (p) => { | + | |
| - | const i = p.lastIndexOf("/" | + | |
| - | | + | |
| }; | }; | ||
| - | const relativePath | + | const getOrCreateDir |
| - | | + | |
| - | const fromParts = fromDir ? fromDir.split("/" | + | |
| - | const toParts | + | |
| - | + | ||
| - | let i = 0; | + | |
| - | while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) { | + | |
| - | i++; | + | |
| } | } | ||
| - | + | | |
| - | const up = new Array(fromParts.length - i).fill(" | + | |
| - | const down = toParts.slice(i); | + | |
| - | const out = [...up, ...down].join("/" | + | |
| - | | + | |
| }; | }; | ||
| - | const addUrl | + | const writeFile |
| - | const nu = normalizeUrl(u, base); | + | const dirs = pathParts.slice(0, -1); |
| - | | + | |
| - | | + | |
| - | | + | const fileHandle = await dirHandle.getFileHandle(fileName, { create: true }); |
| - | | + | |
| - | | + | |
| + | await writable.close(); | ||
| }; | }; | ||
| - | const cssUrlRegex | + | const collected |
| - | const extractCssUrls | + | const addUrl |
| - | const out = []; | + | const nu = normalizeUrl(u); |
| - | | + | |
| - | | + | const key = nu.href; |
| - | | + | if (!collected.has(key)) { |
| - | if (!raw || raw.startsWith(" | + | |
| - | | + | |
| - | if (nu) out.push(nu.href); | + | |
| } | } | ||
| - | return out; | ||
| }; | }; | ||
| + | // current page | ||
| addUrl(location.href, | addUrl(location.href, | ||
| - | document.querySelectorAll(" | + | |
| - | document.querySelectorAll("link[href]").forEach(el => addUrl(el.getAttribute(" | + | |
| - | document.querySelectorAll(" | + | document.querySelectorAll('link[href]').forEach(el => addUrl(el.href, |
| + | document.querySelectorAll(" | ||
| document.querySelectorAll(" | document.querySelectorAll(" | ||
| const style = el.getAttribute(" | const style = el.getAttribute(" | ||
| + | const re = / | ||
| let m; | let m; | ||
| - | while ((m = cssUrlRegex.exec(style))) | + | while ((m = re.exec(style))) addUrl(m[2], |
| - | | + | |
| - | } | + | |
| }); | }); | ||
| - | | + | // CSS rules |
| - | + | for (const | |
| - | const fetchBinary = async (url) => { | + | |
| - | const res = await fetch(url, { credentials: | + | |
| - | if (!res.ok) throw new Error(`HTTP ${res.status}`); | + | |
| - | return new Uint8Array(await res.arrayBuffer()); | + | |
| - | }; | + | |
| - | + | ||
| - | const fetchText = async (url) => { | + | |
| - | const res = await fetch(url, { credentials: | + | |
| - | if (!res.ok) throw new Error(`HTTP ${res.status}`); | + | |
| - | return await res.text(); | + | |
| - | }; | + | |
| - | + | ||
| - | const pageLocalPath = toLocalPath(pageUrl, | + | |
| - | + | ||
| - | | + | |
| - | for (const | + | |
| - | const href = url.href; | + | |
| - | const localPath = toLocalPath(url, | + | |
| try { | try { | ||
| - | | + | |
| - | | + | const txt = rule.cssText || "" |
| + | const re = / | ||
| + | | ||
| + | while ((m = re.exec(txt))) addUrl(m[2], | ||
| } | } | ||
| - | |||
| - | const bytes = await fetchBinary(href); | ||
| - | fileMap.set(href, | ||
| - | url: href, | ||
| - | localPath, | ||
| - | bytes, | ||
| - | contentType: | ||
| - | text: null | ||
| - | }); | ||
| - | console.log(" | ||
| } catch (e) { | } catch (e) { | ||
| - | | + | |
| - | console.warn(" | + | |
| } | } | ||
| } | } | ||
| - | // 2) bring CSS files as text, collect nested url(...) assets | + | // Performance resources |
| - | | + | |
| - | for (const f of cssCandidates) { | + | |
| - | try { | + | |
| - | const txt = decoder.decode(f.bytes); | + | |
| - | f.text = txt; | + | |
| - | const nested = extractCssUrls(txt, | + | |
| - | for (const u of nested) { | + | |
| - | addUrl(u, " | + | |
| - | } | + | |
| - | } catch (e) { | + | |
| - | console.warn(" | + | |
| - | } | + | |
| - | } | + | |
| - | // 3) fetch newly found nested CSS assets | + | // Save HTML of current page into path-based index.html |
| - | | + | const currentHtmlParts = toLocalPathParts(currentUrl, |
| - | | + | |
| - | if (href === pageUrl.href) continue; | + | |
| - | | + | }); |
| + | await writeFile(rootHandle, currentHtmlParts, | ||
| - | const localPath = toLocalPath(url, | + | let ok = 0; |
| - | try { | + | let fail = 0; |
| - | const bytes = await fetchBinary(href); | + | |
| - | | + | |
| - | url: href, | + | |
| - | localPath, | + | |
| - | bytes, | + | |
| - | contentType: | + | |
| - | text: null | + | |
| - | }); | + | |
| - | console.log(" | + | |
| - | } catch (e) { | + | |
| - | | + | |
| - | console.warn(" | + | |
| - | } | + | |
| - | } | + | |
| - | | + | for (const |
| - | | + | if (type === " |
| - | if (!(/ | + | |
| try { | try { | ||
| - | | + | |
| - | + | if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| - | txt = txt.replace(cssUrlRegex, (full, q, raw) => { | + | |
| - | const val = (raw || "" | + | |
| - | if (!val || val.startsWith(" | + | |
| - | + | ||
| - | const nu = normalizeUrl(val, | + | |
| - | | + | |
| - | + | ||
| - | const target = fileMap.get(nu.href); | + | |
| - | if (!target) return full; | + | |
| - | + | ||
| - | const rel = relativePath(f.localPath, | + | |
| - | return | + | |
| - | }); | + | |
| - | | + | |
| - | | + | |
| + | await writeFile(rootHandle, | ||
| + | ok++; | ||
| + | console.log(" | ||
| } catch (e) { | } catch (e) { | ||
| - | console.warn(" | + | |
| + | failed.push({ url: url.href, error: String(e) }); | ||
| + | | ||
| } | } | ||
| } | } | ||
| - | // 5) rewrite HTML to local relative paths | + | // save fail report |
| - | const htmlDoc | + | const report |
| - | + | ||
| - | const rewriteAttr = (selector, attr) => { | + | |
| - | htmlDoc.querySelectorAll(selector).forEach(el => { | + | |
| - | const raw = el.getAttribute(attr); | + | |
| - | if (!raw) return; | + | |
| - | if (raw.startsWith(" | + | |
| - | + | ||
| - | const nu = normalizeUrl(raw, | + | |
| - | if (!nu) return; | + | |
| - | + | ||
| - | const target = fileMap.get(nu.href); | + | |
| - | if (!target) return; | + | |
| - | + | ||
| - | el.setAttribute(attr, | + | |
| - | }); | + | |
| - | }; | + | |
| - | + | ||
| - | rewriteAttr(" | + | |
| - | rewriteAttr(" | + | |
| - | rewriteAttr(" | + | |
| - | rewriteAttr(" | + | |
| - | rewriteAttr(" | + | |
| - | rewriteAttr(" | + | |
| - | rewriteAttr(" | + | |
| - | rewriteAttr(" | + | |
| - | + | ||
| - | htmlDoc.querySelectorAll(" | + | |
| - | const style = el.getAttribute(" | + | |
| - | const replaced = style.replace(cssUrlRegex, | + | |
| - | const val = (raw || "" | + | |
| - | if (!val || val.startsWith(" | + | |
| - | + | ||
| - | const nu = normalizeUrl(val, | + | |
| - | if (!nu) return full; | + | |
| - | + | ||
| - | const target = fileMap.get(nu.href); | + | |
| - | if (!target) return full; | + | |
| - | + | ||
| - | return `url(" | + | |
| - | }); | + | |
| - | el.setAttribute(" | + | |
| - | }); | + | |
| - | + | ||
| - | const htmlText = "< | + | |
| - | fileMap.set(pageUrl.href, | + | |
| - | url: pageUrl.href, | + | |
| - | localPath: pageLocalPath, | + | |
| - | bytes: encoder.encode(htmlText), | + | |
| - | contentType: | + | |
| - | text: htmlText | + | |
| - | }); | + | |
| - | + | ||
| - | const reportText = JSON.stringify({ | + | |
| page: location.href, | page: location.href, | ||
| - | | + | |
| - | savedCount: | + | savedCount: |
| - | failedCount: | + | failedCount: |
| failed | failed | ||
| - | }, null, 2); | ||
| - | |||
| - | fileMap.set(" | ||
| - | url: " | ||
| - | localPath: " | ||
| - | bytes: encoder.encode(reportText), | ||
| - | contentType: | ||
| - | text: reportText | ||
| - | }); | ||
| - | |||
| - | // 6) TAR builder | ||
| - | const padOctal = (num, width) => { | ||
| - | const s = num.toString(8); | ||
| - | return s.padStart(width - 1, " | ||
| }; | }; | ||
| - | | + | |
| - | | + | |
| - | buf.set(bytes.slice(0, length), offset); | + | ["_download_report.json"], |
| - | }; | + | |
| - | + | ); | |
| - | const writeOctal = (buf, offset, num, length) => { | + | |
| - | | + | |
| - | }; | + | |
| - | + | ||
| - | const tarHeader = (name, size, mtime = Math.floor(Date.now() / 1000), mode = 0o644) => { | + | |
| - | const buf = new Uint8Array(512); | + | |
| - | writeString(buf, | + | |
| - | writeOctal(buf, | + | |
| - | writeOctal(buf, | + | |
| - | writeOctal(buf, | + | |
| - | writeOctal(buf, | + | |
| - | writeOctal(buf, | + | |
| - | for (let i = 148; i < 156; i++) buf[i] = 32; | + | |
| - | buf[156] = "0".charCodeAt(0); | + | |
| - | writeString(buf, | + | |
| - | | + | |
| - | + | ||
| - | let sum = 0; | + | |
| - | for (let i = 0; i < 512; i++) sum += buf[i]; | + | |
| - | const chk = sum.toString(8).padStart(6, " | + | |
| - | writeString(buf, 148, chk, 6); | + | |
| - | buf[154] = 0; | + | |
| - | buf[155] = 32; | + | |
| - | + | ||
| - | return buf; | + | |
| - | }; | + | |
| - | + | ||
| - | const tarParts = []; | + | |
| - | let totalBytes = 0; | + | |
| - | + | ||
| - | for (const f of [...fileMap.values()]) { | + | |
| - | const name = f.localPath; | + | |
| - | const body = f.bytes instanceof Uint8Array ? f.bytes : new Uint8Array(f.bytes); | + | |
| - | const header = tarHeader(name, | + | |
| - | + | ||
| - | tarParts.push(header); | + | |
| - | tarParts.push(body); | + | |
| - | totalBytes += 512 + body.length; | + | |
| - | + | ||
| - | const pad = (512 - (body.length % 512)) % 512; | + | |
| - | if (pad) { | + | |
| - | tarParts.push(new Uint8Array(pad)); | + | |
| - | totalBytes += pad; | + | |
| - | } | + | |
| - | } | + | |
| - | + | ||
| - | tarParts.push(new Uint8Array(1024)); | + | |
| - | totalBytes += 1024; | + | |
| - | + | ||
| - | const tarBlob = new Blob(tarParts, { type: " | + | |
| - | + | ||
| - | | + | |
| - | a.href = URL.createObjectURL(tarBlob); | + | |
| - | + | ||
| - | const safeName = (document.title || " | + | |
| - | a.download = `${safeName}.tar`; | + | |
| - | a.click(); | + | |
| - | + | ||
| - | setTimeout(() => URL.revokeObjectURL(a.href), | + | |
| console.log(" | console.log(" | ||
| - | console.log(" | + | console.log(" |
| - | console.log(" | + | console.log(" |
| - | console.log(" | + | console.log(" |
| })(); | })(); | ||
| - | |||
| </ | </ | ||
test1/subject6.1775961924.txt.gz · Last modified: by admin · Currently locked by: 127.0.0.1,192.168.0.3
