website

#astro#js#html#css

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
});