website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
scripts/submit-to-archives.js
| 09c6a56 | 1 | #!/usr/bin/env node |
| 09c6a56 | 2 | // Asks public web archives to snapshot the key pages of the site. |
| 09c6a56 | 3 | // |
| 09c6a56 | 4 | // Providers: |
| 09c6a56 | 5 | // - Wayback Machine (web.archive.org) via its "Save Page Now" endpoint. |
| 09c6a56 | 6 | // Set ARCHIVE_ORG_ACCESS_KEY / ARCHIVE_ORG_SECRET_KEY (from |
| 09c6a56 | 7 | // https://archive.org/account/s3.php) to use the authenticated SPN2 API, |
| 09c6a56 | 8 | // which is less aggressively rate-limited and reports a job status. |
| 09c6a56 | 9 | // Without them, falls back to the unauthenticated /save/<url> endpoint. |
| 09c6a56 | 10 | // - archive.today (archive.ph) via its public /submit/ form endpoint. |
| 09c6a56 | 11 | // There's no official API; archive.today occasionally interposes a |
| 09c6a56 | 12 | // captcha/"please wait" page under load, in which case the submission |
| 09c6a56 | 13 | // just gets logged as needing a manual follow-up. |
| 09c6a56 | 14 | // |
| 09c6a56 | 15 | // Usage: |
| 09c6a56 | 16 | // node scripts/submit-to-archives.js [--provider=wayback|archivetoday|all] [--dry-run] [--url=/some/path/] |
| 09c6a56 | 17 | |
| 09c6a56 | 18 | import { readdirSync } from "node:fs"; |
| 09c6a56 | 19 | import path from "node:path"; |
| 09c6a56 | 20 | import { fileURLToPath } from "node:url"; |
| 09c6a56 | 21 | import { setTimeout as sleep } from "node:timers/promises"; |
| 09c6a56 | 22 | |
| 09c6a56 | 23 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 09c6a56 | 24 | const SITE_URL = "https://pyrossh.dev"; |
| 09c6a56 | 25 | const DELAY_BETWEEN_REQUESTS_MS = 4000; |
| 09c6a56 | 26 | |
| 09c6a56 | 27 | function parseArgs(argv) { |
| 09c6a56 | 28 | const args = { provider: "all", dryRun: false, urls: [] }; |
| 09c6a56 | 29 | for (const arg of argv) { |
| 09c6a56 | 30 | if (arg === "--dry-run") args.dryRun = true; |
| 09c6a56 | 31 | else if (arg.startsWith("--provider=")) args.provider = arg.slice("--provider=".length); |
| 09c6a56 | 32 | else if (arg.startsWith("--url=")) args.urls.push(arg.slice("--url=".length)); |
| 09c6a56 | 33 | } |
| 09c6a56 | 34 | return args; |
| 09c6a56 | 35 | } |
| 09c6a56 | 36 | |
| 09c6a56 | 37 | function discoverPostSlugs() { |
| 09c6a56 | 38 | const postsDir = path.join(__dirname, "..", "src", "posts"); |
| 09c6a56 | 39 | return readdirSync(postsDir) |
| 09c6a56 | 40 | .filter((file) => file.endsWith(".md")) |
| 09c6a56 | 41 | .map((file) => file.replace(/\.md$/, "")); |
| 09c6a56 | 42 | } |
| 09c6a56 | 43 | |
| 09c6a56 | 44 | function discoverUrls() { |
| 09c6a56 | 45 | const staticPaths = [ |
| 09c6a56 | 46 | "/", |
| 09c6a56 | 47 | "/cv/", |
| 09c6a56 | 48 | "/posts/", |
| 09c6a56 | 49 | "/only-bible-app/", |
| 09c6a56 | 50 | "/only-bible-app/privacy-policy/", |
| 09c6a56 | 51 | "/only-bible-app/terms-and-conditions/", |
| 09c6a56 | 52 | ]; |
| 09c6a56 | 53 | const postPaths = discoverPostSlugs().map((slug) => `/posts/${slug}/`); |
| 09c6a56 | 54 | return [...staticPaths, ...postPaths].map((p) => `${SITE_URL}${p}`); |
| 09c6a56 | 55 | } |
| 09c6a56 | 56 | |
| 09c6a56 | 57 | async function submitToWaybackMachine(url) { |
| 09c6a56 | 58 | const accessKey = process.env.ARCHIVE_ORG_ACCESS_KEY; |
| 09c6a56 | 59 | const secretKey = process.env.ARCHIVE_ORG_SECRET_KEY; |
| 09c6a56 | 60 | |
| 09c6a56 | 61 | if (accessKey && secretKey) { |
| 09c6a56 | 62 | const response = await fetch("https://web.archive.org/save", { |
| 09c6a56 | 63 | method: "POST", |
| 09c6a56 | 64 | headers: { |
| 09c6a56 | 65 | Authorization: `LOW ${accessKey}:${secretKey}`, |
| 09c6a56 | 66 | "content-type": "application/x-www-form-urlencoded", |
| 09c6a56 | 67 | Accept: "application/json", |
| 09c6a56 | 68 | }, |
| 09c6a56 | 69 | body: new URLSearchParams({ url, skip_first_archive: "1" }), |
| 09c6a56 | 70 | }); |
| 09c6a56 | 71 | const body = await response.json().catch(() => ({})); |
| 09c6a56 | 72 | return { ok: response.ok, status: response.status, detail: body.job_id ? `job ${body.job_id}` : JSON.stringify(body) }; |
| 09c6a56 | 73 | } |
| 09c6a56 | 74 | |
| 09c6a56 | 75 | const response = await fetch(`https://web.archive.org/save/${url}`, { method: "GET", redirect: "follow" }); |
| 09c6a56 | 76 | return { ok: response.ok, status: response.status, detail: response.url }; |
| 09c6a56 | 77 | } |
| 09c6a56 | 78 | |
| 09c6a56 | 79 | async function submitToArchiveToday(url) { |
| 09c6a56 | 80 | const response = await fetch("https://archive.ph/submit/", { |
| 09c6a56 | 81 | method: "POST", |
| bf7618d | 82 | headers: { |
| bf7618d | 83 | "content-type": "application/x-www-form-urlencoded", |
| bf7618d | 84 | // archive.today's anti-bot layer rejects requests without a |
| bf7618d | 85 | // browser-like User-Agent outright, independent of any real rate limit. |
| bf7618d | 86 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", |
| bf7618d | 87 | }, |
| 09c6a56 | 88 | body: new URLSearchParams({ url, anyway: "1" }), |
| 09c6a56 | 89 | redirect: "manual", |
| 09c6a56 | 90 | }); |
| 09c6a56 | 91 | const location = response.headers.get("location"); |
| 09c6a56 | 92 | const isRedirect = response.status >= 300 && response.status < 400; |
| bf7618d | 93 | if (isRedirect) return { ok: true, status: response.status, detail: location }; |
| bf7618d | 94 | |
| bf7618d | 95 | const body = await response.text().catch(() => ""); |
| bf7618d | 96 | if (/captcha/i.test(body)) { |
| bf7618d | 97 | return { ok: false, status: response.status, detail: "blocked by archive.today's CAPTCHA/anti-bot check — submit this URL manually at https://archive.ph/" }; |
| bf7618d | 98 | } |
| bf7618d | 99 | return { ok: response.ok, status: response.status, detail: "submitted — check https://archive.ph/ manually for the resulting snapshot" }; |
| 09c6a56 | 100 | } |
| 09c6a56 | 101 | |
| 09c6a56 | 102 | const PROVIDERS = { |
| 09c6a56 | 103 | wayback: { label: "Wayback Machine", submit: submitToWaybackMachine }, |
| 09c6a56 | 104 | archivetoday: { label: "archive.today", submit: submitToArchiveToday }, |
| 09c6a56 | 105 | }; |
| 09c6a56 | 106 | |
| 09c6a56 | 107 | async function main() { |
| 09c6a56 | 108 | const args = parseArgs(process.argv.slice(2)); |
| 09c6a56 | 109 | const urls = args.urls.length ? args.urls.map((u) => (u.startsWith("http") ? u : `${SITE_URL}${u}`)) : discoverUrls(); |
| 09c6a56 | 110 | const providerNames = args.provider === "all" ? Object.keys(PROVIDERS) : [args.provider]; |
| 09c6a56 | 111 | |
| 09c6a56 | 112 | for (const name of providerNames) { |
| 09c6a56 | 113 | if (!PROVIDERS[name]) throw new Error(`Unknown provider "${name}". Expected one of: wayback, archivetoday, all`); |
| 09c6a56 | 114 | } |
| 09c6a56 | 115 | |
| 09c6a56 | 116 | console.log(`Submitting ${urls.length} URL(s) to: ${providerNames.map((n) => PROVIDERS[n].label).join(", ")}`); |
| 09c6a56 | 117 | if (args.dryRun) { |
| 09c6a56 | 118 | for (const url of urls) console.log(`[dry-run] ${url}`); |
| 09c6a56 | 119 | return; |
| 09c6a56 | 120 | } |
| 09c6a56 | 121 | |
| 09c6a56 | 122 | for (const name of providerNames) { |
| 09c6a56 | 123 | const provider = PROVIDERS[name]; |
| 09c6a56 | 124 | console.log(`\n--- ${provider.label} ---`); |
| 09c6a56 | 125 | for (const [index, url] of urls.entries()) { |
| 09c6a56 | 126 | try { |
| 09c6a56 | 127 | const result = await provider.submit(url); |
| 09c6a56 | 128 | const icon = result.ok ? "✔" : "✖"; |
| 09c6a56 | 129 | console.log(`${icon} [${result.status}] ${url} — ${result.detail}`); |
| 09c6a56 | 130 | } catch (err) { |
| 09c6a56 | 131 | console.log(`✖ [error] ${url} — ${err.message}`); |
| 09c6a56 | 132 | } |
| 09c6a56 | 133 | if (index < urls.length - 1) await sleep(DELAY_BETWEEN_REQUESTS_MS); |
| 09c6a56 | 134 | } |
| 09c6a56 | 135 | } |
| 09c6a56 | 136 | } |
| 09c6a56 | 137 | |
| 09c6a56 | 138 | main().catch((err) => { |
| 09c6a56 | 139 | console.error(err.message); |
| 09c6a56 | 140 | process.exit(1); |
| 09c6a56 | 141 | }); |