polite-crawler 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +557 -10
- package/build/browser-CKRNbIDT.mjs +62 -0
- package/build/browser-CKRNbIDT.mjs.map +1 -0
- package/build/cli.mjs +795 -121
- package/build/cli.mjs.map +1 -1
- package/build/diff-Dk2eSvi-.mjs +648 -0
- package/build/diff-Dk2eSvi-.mjs.map +1 -0
- package/build/duration-iIuMMofi.mjs +38 -0
- package/build/duration-iIuMMofi.mjs.map +1 -0
- package/build/extract-DcwNrTK7.mjs +1793 -0
- package/build/extract-DcwNrTK7.mjs.map +1 -0
- package/build/{html-nWYFVsRU.d.mts → html-BACljlkf.d.mts} +8 -9
- package/build/{html-oNpGAHwi.mjs → html-DkQUAE_d.mjs} +18 -39
- package/build/html-DkQUAE_d.mjs.map +1 -0
- package/build/html.d.mts +2 -1
- package/build/html.mjs +3 -1
- package/build/index.d.mts +94 -4
- package/build/index.mjs +13 -5
- package/build/log-Dx-wXjJl.mjs +342 -0
- package/build/log-Dx-wXjJl.mjs.map +1 -0
- package/build/mcp.d.mts +7 -4
- package/build/mcp.mjs +556 -32
- package/build/mcp.mjs.map +1 -1
- package/build/open-Ca1lcWR3.d.mts +2875 -0
- package/build/open-DnrWgkC0.mjs +3161 -0
- package/build/open-DnrWgkC0.mjs.map +1 -0
- package/build/{browser-gJ_h4AhB.mjs → render-C1CUraWF.mjs} +31 -74
- package/build/render-C1CUraWF.mjs.map +1 -0
- package/build/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/build/{open-DYZ7KL2A.mjs → runtime-k8mo1_7U.mjs} +986 -1657
- package/build/runtime-k8mo1_7U.mjs.map +1 -0
- package/build/urls-BHFeSyEK.mjs +62 -0
- package/build/urls-BHFeSyEK.mjs.map +1 -0
- package/build/urls-BTDSgDLC.d.mts +16 -0
- package/build/wacz-sfOLT8e9.mjs +275 -0
- package/build/wacz-sfOLT8e9.mjs.map +1 -0
- package/build/warc-VIPscWxY.mjs +728 -0
- package/build/warc-VIPscWxY.mjs.map +1 -0
- package/build/webbotauth-CjJdTAfE.mjs +568 -0
- package/build/webbotauth-CjJdTAfE.mjs.map +1 -0
- package/package.json +1 -1
- package/record.schema.json +15 -1
- package/build/browser-gJ_h4AhB.mjs.map +0 -1
- package/build/html-oNpGAHwi.mjs.map +0 -1
- package/build/open-DYZ7KL2A.mjs.map +0 -1
- package/build/open-eHcriTds.d.mts +0 -1499
- package/build/version-no12qgos.mjs +0 -11
- package/build/version-no12qgos.mjs.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/urls.ts
|
|
2
|
+
/**
|
|
3
|
+
* This module gives the canonical form of a URL. The document id of a page and the cache key of the
|
|
4
|
+
* crawler use it. Thus two spellings of one page are one document and one cached answer.
|
|
5
|
+
*/
|
|
6
|
+
const TRACKING = /^(utm_[a-z_]+|fbclid|gclid|gclsrc|dclid|gbraid|wbraid|msclkid|yclid|ttclid|twclid|li_fat_id|srsltid|mc_[a-z]+|_ga|_gl|_hsenc|_hsmi|__hstc|__hssc|__hsfp|mkt_tok|ref_src|igshid)$/i;
|
|
7
|
+
/** This function decodes the escapes of unreserved characters (`%7E` is `~`), and writes the other escapes in capitals. */
|
|
8
|
+
const normalizeEscapes = (value) => value.replace(/%[0-9a-f]{2}/gi, (escape) => {
|
|
9
|
+
const char = String.fromCharCode(Number.parseInt(escape.slice(1), 16));
|
|
10
|
+
return /^[A-Za-z0-9\-._~]$/.test(char) ? char : escape.toUpperCase();
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* This function gives one URL for each page: a lowercase host, and no fragment, tracking
|
|
14
|
+
* parameters, empty query or default port. It decodes the escapes of unreserved characters.
|
|
15
|
+
*/
|
|
16
|
+
const canonicalUrl = (value) => {
|
|
17
|
+
const url = new URL(value);
|
|
18
|
+
url.hash = "";
|
|
19
|
+
url.hostname = url.hostname.toLowerCase();
|
|
20
|
+
if (url.protocol === "https:" && url.port === "443" || url.protocol === "http:" && url.port === "80") url.port = "";
|
|
21
|
+
for (const key of Array.from(url.searchParams.keys())) if (TRACKING.test(key)) url.searchParams.delete(key);
|
|
22
|
+
if (url.pathname.includes("%")) url.pathname = normalizeEscapes(url.pathname);
|
|
23
|
+
url.search = url.search.includes("%") ? normalizeEscapes(url.search) : url.search;
|
|
24
|
+
return url.href;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* This function gives the cache key of a URL: its canonical form. A text that is not a URL stays as
|
|
28
|
+
* it is. The crawler and `reindex` use the same key, so a rebuilt index answers the same requests.
|
|
29
|
+
*/
|
|
30
|
+
const cacheKeyOf = (value) => {
|
|
31
|
+
try {
|
|
32
|
+
return canonicalUrl(value);
|
|
33
|
+
} catch {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* This function gives the site of a host name: the name in lowercase, with no final dot and no
|
|
39
|
+
* `www.` at the start. Thus `www.example.com` and `example.com` are one site.
|
|
40
|
+
*/
|
|
41
|
+
const siteOf = (host) => host.toLowerCase().replace(/\.$/, "").replace(/^www\./, "");
|
|
42
|
+
/**
|
|
43
|
+
* This function gives the test of the scope of a crawl or a discovery. With `sameSite: false`
|
|
44
|
+
* and no `allowHosts`, each http(s) URL is in scope.
|
|
45
|
+
*/
|
|
46
|
+
const scopeTest = (starts, options) => {
|
|
47
|
+
const sameSite = options.sameSite !== false;
|
|
48
|
+
const sites = new Set(starts.map((start) => siteOf(start.hostname)));
|
|
49
|
+
const hosts = (options.allowHosts ?? []).map((host) => host.trim().toLowerCase().replace(/\.$/, ""));
|
|
50
|
+
const any = !sameSite && options.allowHosts === void 0;
|
|
51
|
+
return (url) => {
|
|
52
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
53
|
+
if (any) return true;
|
|
54
|
+
const name = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
55
|
+
if (sameSite && sites.has(siteOf(name))) return true;
|
|
56
|
+
return hosts.some((host) => host.startsWith("*.") ? name.endsWith(host.slice(1)) : name === host);
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { canonicalUrl as n, scopeTest as r, cacheKeyOf as t };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=urls-BHFeSyEK.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"urls-BHFeSyEK.mjs","names":[],"sources":["../src/urls.ts"],"sourcesContent":["/**\n * This module gives the canonical form of a URL. The document id of a page and the cache key of the\n * crawler use it. Thus two spellings of one page are one document and one cached answer.\n */\n\nconst TRACKING =\n /^(utm_[a-z_]+|fbclid|gclid|gclsrc|dclid|gbraid|wbraid|msclkid|yclid|ttclid|twclid|li_fat_id|srsltid|mc_[a-z]+|_ga|_gl|_hsenc|_hsmi|__hstc|__hssc|__hsfp|mkt_tok|ref_src|igshid)$/i;\n\n/** This function decodes the escapes of unreserved characters (`%7E` is `~`), and writes the other escapes in capitals. */\nconst normalizeEscapes = (value: string): string =>\n value.replace(/%[0-9a-f]{2}/gi, (escape) => {\n const char = String.fromCharCode(Number.parseInt(escape.slice(1), 16));\n return /^[A-Za-z0-9\\-._~]$/.test(char) ? char : escape.toUpperCase();\n });\n\n/**\n * This function gives one URL for each page: a lowercase host, and no fragment, tracking\n * parameters, empty query or default port. It decodes the escapes of unreserved characters.\n */\nexport const canonicalUrl = (value: string): string => {\n const url = new URL(value);\n url.hash = \"\";\n url.hostname = url.hostname.toLowerCase();\n if (\n (url.protocol === \"https:\" && url.port === \"443\") ||\n (url.protocol === \"http:\" && url.port === \"80\")\n ) {\n url.port = \"\";\n }\n // a copy of the keys, because the loop deletes from the live list\n for (const key of Array.from(url.searchParams.keys())) {\n if (TRACKING.test(key)) url.searchParams.delete(key);\n }\n if (url.pathname.includes(\"%\")) url.pathname = normalizeEscapes(url.pathname);\n // \"/p?\" is \"/p\"\n url.search = url.search.includes(\"%\") ? normalizeEscapes(url.search) : url.search;\n return url.href;\n};\n\n/**\n * This function gives the cache key of a URL: its canonical form. A text that is not a URL stays as\n * it is. The crawler and `reindex` use the same key, so a rebuilt index answers the same requests.\n */\nexport const cacheKeyOf = (value: string): string => {\n try {\n return canonicalUrl(value);\n } catch {\n return value;\n }\n};\n\n/**\n * This function gives the site of a host name: the name in lowercase, with no final dot and no\n * `www.` at the start. Thus `www.example.com` and `example.com` are one site.\n */\nexport const siteOf = (host: string): string =>\n host\n .toLowerCase()\n .replace(/\\.$/, \"\")\n .replace(/^www\\./, \"\");\n\n/** These options set the hosts that a crawl or a discovery keeps. */\nexport interface ScopeOptions {\n /** With the default value true, the URLs on the sites of the start URLs are in scope. */\n readonly sameSite?: boolean | undefined;\n /** The URLs on these hosts are in scope too. A name that starts with `*.` names the hosts under it. */\n readonly allowHosts?: readonly string[] | undefined;\n}\n\n/**\n * This function gives the test of the scope of a crawl or a discovery. With `sameSite: false`\n * and no `allowHosts`, each http(s) URL is in scope.\n */\nexport const scopeTest = (\n starts: readonly URL[],\n options: ScopeOptions,\n): ((url: URL) => boolean) => {\n const sameSite = options.sameSite !== false;\n const sites = new Set(starts.map((start) => siteOf(start.hostname)));\n const hosts = (options.allowHosts ?? []).map((host) =>\n host.trim().toLowerCase().replace(/\\.$/, \"\"),\n );\n const any = !sameSite && options.allowHosts === undefined;\n return (url) => {\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (any) return true;\n const name = url.hostname.toLowerCase().replace(/\\.$/, \"\");\n if (sameSite && sites.has(siteOf(name))) return true;\n return hosts.some((host) =>\n host.startsWith(\"*.\") ? name.endsWith(host.slice(1)) : name === host,\n );\n };\n};\n"],"mappings":";;;;;AAKA,MAAM,WACJ;;AAGF,MAAM,oBAAoB,UACxB,MAAM,QAAQ,mBAAmB,WAAW;CAC1C,MAAM,OAAO,OAAO,aAAa,OAAO,SAAS,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC;CACrE,OAAO,qBAAqB,KAAK,IAAI,IAAI,OAAO,OAAO,YAAY;AACrE,CAAC;;;;;AAMH,MAAa,gBAAgB,UAA0B;CACrD,MAAM,MAAM,IAAI,IAAI,KAAK;CACzB,IAAI,OAAO;CACX,IAAI,WAAW,IAAI,SAAS,YAAY;CACxC,IACG,IAAI,aAAa,YAAY,IAAI,SAAS,SAC1C,IAAI,aAAa,WAAW,IAAI,SAAS,MAE1C,IAAI,OAAO;CAGb,KAAK,MAAM,OAAO,MAAM,KAAK,IAAI,aAAa,KAAK,CAAC,GAClD,IAAI,SAAS,KAAK,GAAG,GAAG,IAAI,aAAa,OAAO,GAAG;CAErD,IAAI,IAAI,SAAS,SAAS,GAAG,GAAG,IAAI,WAAW,iBAAiB,IAAI,QAAQ;CAE5E,IAAI,SAAS,IAAI,OAAO,SAAS,GAAG,IAAI,iBAAiB,IAAI,MAAM,IAAI,IAAI;CAC3E,OAAO,IAAI;AACb;;;;;AAMA,MAAa,cAAc,UAA0B;CACnD,IAAI;EACF,OAAO,aAAa,KAAK;CAC3B,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAMA,MAAa,UAAU,SACrB,KACG,YAAY,CAAC,CACb,QAAQ,OAAO,EAAE,CAAC,CAClB,QAAQ,UAAU,EAAE;;;;;AAczB,MAAa,aACX,QACA,YAC4B;CAC5B,MAAM,WAAW,QAAQ,aAAa;CACtC,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,UAAU,OAAO,MAAM,QAAQ,CAAC,CAAC;CACnE,MAAM,SAAS,QAAQ,cAAc,CAAC,EAAA,CAAG,KAAK,SAC5C,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,QAAQ,OAAO,EAAE,CAC7C;CACA,MAAM,MAAM,CAAC,YAAY,QAAQ,eAAe,KAAA;CAChD,QAAQ,QAAQ;EACd,IAAI,IAAI,aAAa,WAAW,IAAI,aAAa,UAAU,OAAO;EAClE,IAAI,KAAK,OAAO;EAChB,MAAM,OAAO,IAAI,SAAS,YAAY,CAAC,CAAC,QAAQ,OAAO,EAAE;EACzD,IAAI,YAAY,MAAM,IAAI,OAAO,IAAI,CAAC,GAAG,OAAO;EAChD,OAAO,MAAM,MAAM,SACjB,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS,KAAK,MAAM,CAAC,CAAC,IAAI,SAAS,IAClE;CACF;AACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/urls.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* This function gives one URL for each page: a lowercase host, and no fragment, tracking
|
|
4
|
+
* parameters, empty query or default port. It decodes the escapes of unreserved characters.
|
|
5
|
+
*/
|
|
6
|
+
declare const canonicalUrl: (value: string) => string;
|
|
7
|
+
/** These options set the hosts that a crawl or a discovery keeps. */
|
|
8
|
+
interface ScopeOptions {
|
|
9
|
+
/** With the default value true, the URLs on the sites of the start URLs are in scope. */
|
|
10
|
+
readonly sameSite?: boolean | undefined;
|
|
11
|
+
/** The URLs on these hosts are in scope too. A name that starts with `*.` names the hosts under it. */
|
|
12
|
+
readonly allowHosts?: readonly string[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { canonicalUrl as n, ScopeOptions as t };
|
|
16
|
+
//# sourceMappingURL=urls-BTDSgDLC.d.mts.map
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { i as VERSION } from "./duration-iIuMMofi.mjs";
|
|
3
|
+
import { t as cacheKeyOf } from "./urls-BHFeSyEK.mjs";
|
|
4
|
+
import { c as deflateFile, i as planArchive, l as memberOf, o as writeArchive, r as outputOf, s as TalliedFile, t as cdxjLine, u as writeZip } from "./warc-VIPscWxY.mjs";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { createReadStream } from "node:fs";
|
|
9
|
+
//#region src/sorter.ts
|
|
10
|
+
const NEWLINE$1 = 10;
|
|
11
|
+
/** This function compares two lines by their bytes. */
|
|
12
|
+
const byBytes = (a, b) => Buffer.compare(a, b);
|
|
13
|
+
/** This function reads a file line by line, as bytes. A line holds no line break. */
|
|
14
|
+
async function* linesOf(path) {
|
|
15
|
+
let rest;
|
|
16
|
+
for await (const chunk of createReadStream(path)) {
|
|
17
|
+
const data = rest === void 0 ? chunk : Buffer.concat([rest, chunk]);
|
|
18
|
+
rest = void 0;
|
|
19
|
+
let start = 0;
|
|
20
|
+
for (;;) {
|
|
21
|
+
const end = data.indexOf(NEWLINE$1, start);
|
|
22
|
+
if (end === -1) break;
|
|
23
|
+
yield data.subarray(start, end);
|
|
24
|
+
start = end + 1;
|
|
25
|
+
}
|
|
26
|
+
if (start < data.length) rest = Buffer.from(data.subarray(start));
|
|
27
|
+
}
|
|
28
|
+
if (rest !== void 0 && rest.length > 0) yield rest;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A line sorter sorts lines of text in the sequence of their UTF-8 bytes (the sequence of
|
|
32
|
+
* `LC_ALL=C sort`). It keeps no more than `limit` bytes of lines in memory. When it has more, it
|
|
33
|
+
* writes them to a sorted file in `dir`. At the end it merges the files. Thus its memory does not
|
|
34
|
+
* grow with the count of lines. The WACZ export sorts its index and its page list with it.
|
|
35
|
+
*
|
|
36
|
+
* A line must not hold a line break.
|
|
37
|
+
*/
|
|
38
|
+
var LineSorter = class {
|
|
39
|
+
dir;
|
|
40
|
+
limit;
|
|
41
|
+
lines = [];
|
|
42
|
+
bytes = 0;
|
|
43
|
+
runs = [];
|
|
44
|
+
constructor(dir, limit = 8388608) {
|
|
45
|
+
this.dir = dir;
|
|
46
|
+
this.limit = limit;
|
|
47
|
+
}
|
|
48
|
+
/** This value is the count of sorted files that the sorter wrote. */
|
|
49
|
+
get files() {
|
|
50
|
+
return this.runs.length;
|
|
51
|
+
}
|
|
52
|
+
async add(line) {
|
|
53
|
+
const bytes = Buffer.from(line, "utf8");
|
|
54
|
+
if (bytes.includes(NEWLINE$1)) throw new Error("a line to sort holds a line break");
|
|
55
|
+
this.lines.push(bytes);
|
|
56
|
+
this.bytes += bytes.length + 64;
|
|
57
|
+
if (this.bytes >= this.limit) await this.spill();
|
|
58
|
+
}
|
|
59
|
+
async spill() {
|
|
60
|
+
if (this.lines.length === 0) return;
|
|
61
|
+
this.lines.sort(byBytes);
|
|
62
|
+
await mkdir(this.dir, { recursive: true });
|
|
63
|
+
const path = join(this.dir, `${String(this.runs.length)}.lines`);
|
|
64
|
+
const parts = [];
|
|
65
|
+
const newline = Buffer.from([NEWLINE$1]);
|
|
66
|
+
for (const line of this.lines) parts.push(line, newline);
|
|
67
|
+
await writeFile(path, Buffer.concat(parts), { mode: 384 });
|
|
68
|
+
this.runs.push(path);
|
|
69
|
+
this.lines = [];
|
|
70
|
+
this.bytes = 0;
|
|
71
|
+
}
|
|
72
|
+
/** This method gives each line in the sorted sequence. Use it one time, after the last `add`. */
|
|
73
|
+
async *sorted() {
|
|
74
|
+
if (this.runs.length === 0) {
|
|
75
|
+
const lines = this.lines.sort(byBytes);
|
|
76
|
+
this.lines = [];
|
|
77
|
+
yield* lines;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await this.spill();
|
|
81
|
+
const readers = this.runs.map((path) => linesOf(path)[Symbol.asyncIterator]());
|
|
82
|
+
const heads = [];
|
|
83
|
+
for (const reader of readers) heads.push((await reader.next()).value ?? void 0);
|
|
84
|
+
for (;;) {
|
|
85
|
+
let pick = -1;
|
|
86
|
+
for (const [at, head] of heads.entries()) if (head !== void 0 && (pick === -1 || Buffer.compare(head, heads[pick]) < 0)) pick = at;
|
|
87
|
+
if (pick === -1) return;
|
|
88
|
+
yield heads[pick];
|
|
89
|
+
heads[pick] = (await readers[pick].next()).value ?? void 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/wacz.ts
|
|
95
|
+
var wacz_exports = /* @__PURE__ */ __exportAll({ exportWacz: () => exportWacz });
|
|
96
|
+
/** This is the version of the WACZ specification that the export follows. */
|
|
97
|
+
const WACZ_VERSION = "1.1.1";
|
|
98
|
+
const WARC_NAME = "data.warc.gz";
|
|
99
|
+
const NEWLINE = Buffer.from("\n");
|
|
100
|
+
const TAB = 9;
|
|
101
|
+
const PAGES_HEADER = {
|
|
102
|
+
format: "json-pages-1.0",
|
|
103
|
+
id: "pages",
|
|
104
|
+
title: "All Pages"
|
|
105
|
+
};
|
|
106
|
+
/** This function writes lines to a file in batches of approximately 64 KiB. It gives the count of lines. */
|
|
107
|
+
const writeLines = async (file, lines) => {
|
|
108
|
+
let batch = [];
|
|
109
|
+
let size = 0;
|
|
110
|
+
let count = 0;
|
|
111
|
+
for await (const line of lines) {
|
|
112
|
+
batch.push(line, NEWLINE);
|
|
113
|
+
size += line.length + 1;
|
|
114
|
+
count += 1;
|
|
115
|
+
if (size >= 65536) {
|
|
116
|
+
await file.write(Buffer.concat(batch));
|
|
117
|
+
batch = [];
|
|
118
|
+
size = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (batch.length > 0) await file.write(Buffer.concat(batch));
|
|
122
|
+
return count;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* This function keeps the last line of each document. Each line is the key of the document, a tab,
|
|
126
|
+
* the time, a tab and the JSON of the page. The sorter gives the lines of one key together, the
|
|
127
|
+
* oldest first.
|
|
128
|
+
*/
|
|
129
|
+
async function* latestOf(lines) {
|
|
130
|
+
let held;
|
|
131
|
+
let heldKey;
|
|
132
|
+
for await (const line of lines) {
|
|
133
|
+
const key = line.subarray(0, line.indexOf(TAB));
|
|
134
|
+
if (held !== void 0 && heldKey !== void 0 && !key.equals(heldKey)) yield held.subarray(held.indexOf(TAB, heldKey.length + 1) + 1);
|
|
135
|
+
held = line;
|
|
136
|
+
heldKey = key;
|
|
137
|
+
}
|
|
138
|
+
if (held !== void 0 && heldKey !== void 0) yield held.subarray(held.indexOf(TAB, heldKey.length + 1) + 1);
|
|
139
|
+
}
|
|
140
|
+
const resourceOf = (path, facts) => ({
|
|
141
|
+
name: basename(path).toLowerCase(),
|
|
142
|
+
path,
|
|
143
|
+
hash: `sha256:${facts.sha256}`,
|
|
144
|
+
bytes: facts.size
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* This function writes the captures of a store as a WACZ 1.1.1 file. The ZIP holds these parts:
|
|
148
|
+
*
|
|
149
|
+
* - `archive/data.warc.gz`: the WARC file of `exportWarc`, stored with no more compression, so a
|
|
150
|
+
* replay tool reads a record at its offset.
|
|
151
|
+
* - `indexes/index.cdxj`: the index of the `response` and `revisit` records, sorted by SURT key.
|
|
152
|
+
* - `pages/pages.jsonl`: one page for each document, at its latest capture.
|
|
153
|
+
* - `datapackage.json` and `datapackage-digest.json`: the SHA-256 of each part, and of the package.
|
|
154
|
+
*
|
|
155
|
+
* The index and the page list are sorted on the disk, so the memory does not grow with the store.
|
|
156
|
+
* The export writes temporary files beside `out` and removes them at the end.
|
|
157
|
+
*/
|
|
158
|
+
const exportWacz = async (source, options) => {
|
|
159
|
+
const plan = planArchive(source, options);
|
|
160
|
+
const { target, temporary } = outputOf(options);
|
|
161
|
+
const parts = `${temporary}.parts`;
|
|
162
|
+
await mkdir(parts, {
|
|
163
|
+
recursive: true,
|
|
164
|
+
mode: 448
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
const created = /* @__PURE__ */ new Date();
|
|
168
|
+
const warc = await TalliedFile.create(join(parts, WARC_NAME));
|
|
169
|
+
const index = new LineSorter(join(parts, "index"));
|
|
170
|
+
const pages = new LineSorter(join(parts, "pages"));
|
|
171
|
+
let counts;
|
|
172
|
+
try {
|
|
173
|
+
counts = await writeArchive(plan, warc, WARC_NAME, async (record) => {
|
|
174
|
+
await index.add(cdxjLine(record, WARC_NAME));
|
|
175
|
+
if (record.page) {
|
|
176
|
+
const page = JSON.stringify({
|
|
177
|
+
id: record.capture,
|
|
178
|
+
url: record.url,
|
|
179
|
+
ts: record.date
|
|
180
|
+
});
|
|
181
|
+
await pages.add(`${cacheKeyOf(record.url)}\t${record.date}\t${page}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
await warc.abandon();
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
const archive = await warc.close();
|
|
189
|
+
const indexFile = await TalliedFile.create(join(parts, "index.cdxj"));
|
|
190
|
+
try {
|
|
191
|
+
await writeLines(indexFile, index.sorted());
|
|
192
|
+
} catch (error) {
|
|
193
|
+
await indexFile.abandon();
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
const indexFacts = await indexFile.close();
|
|
197
|
+
const pagesFile = await TalliedFile.create(join(parts, "pages.jsonl"));
|
|
198
|
+
let pageCount;
|
|
199
|
+
try {
|
|
200
|
+
await pagesFile.write(Buffer.from(`${JSON.stringify(PAGES_HEADER)}\n`));
|
|
201
|
+
pageCount = await writeLines(pagesFile, latestOf(pages.sorted()));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
await pagesFile.abandon();
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const pagesFacts = await pagesFile.close();
|
|
207
|
+
const datapackage = Buffer.from(JSON.stringify({
|
|
208
|
+
profile: "data-package",
|
|
209
|
+
wacz_version: WACZ_VERSION,
|
|
210
|
+
software: `polite-crawler ${VERSION}`,
|
|
211
|
+
created: created.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
212
|
+
title: "polite-crawler export",
|
|
213
|
+
resources: [
|
|
214
|
+
resourceOf(`archive/${WARC_NAME}`, archive),
|
|
215
|
+
resourceOf("indexes/index.cdxj", indexFacts),
|
|
216
|
+
resourceOf("pages/pages.jsonl", pagesFacts)
|
|
217
|
+
]
|
|
218
|
+
}, null, 2), "utf8");
|
|
219
|
+
const digest = Buffer.from(JSON.stringify({
|
|
220
|
+
path: "datapackage.json",
|
|
221
|
+
hash: `sha256:${createHash("sha256").update(datapackage).digest("hex")}`
|
|
222
|
+
}, null, 2), "utf8");
|
|
223
|
+
const deflated = async (name, facts) => {
|
|
224
|
+
const file = `${facts.path}.deflate`;
|
|
225
|
+
return {
|
|
226
|
+
name,
|
|
227
|
+
method: 8,
|
|
228
|
+
crc: facts.crc,
|
|
229
|
+
size: facts.size,
|
|
230
|
+
compressedSize: await deflateFile(facts.path, file),
|
|
231
|
+
file
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
const members = [
|
|
235
|
+
{
|
|
236
|
+
name: `archive/${WARC_NAME}`,
|
|
237
|
+
method: 0,
|
|
238
|
+
crc: archive.crc,
|
|
239
|
+
size: archive.size,
|
|
240
|
+
compressedSize: archive.size,
|
|
241
|
+
file: archive.path
|
|
242
|
+
},
|
|
243
|
+
await deflated("indexes/index.cdxj", indexFacts),
|
|
244
|
+
await deflated("pages/pages.jsonl", pagesFacts),
|
|
245
|
+
memberOf("datapackage.json", datapackage),
|
|
246
|
+
memberOf("datapackage-digest.json", digest)
|
|
247
|
+
];
|
|
248
|
+
let bytes;
|
|
249
|
+
try {
|
|
250
|
+
bytes = await writeZip(temporary, members, { date: created });
|
|
251
|
+
await rename(temporary, target);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
format: "wacz",
|
|
258
|
+
out: target,
|
|
259
|
+
bytes,
|
|
260
|
+
pages: pageCount,
|
|
261
|
+
...counts
|
|
262
|
+
};
|
|
263
|
+
} finally {
|
|
264
|
+
await rm(parts, {
|
|
265
|
+
recursive: true,
|
|
266
|
+
force: true,
|
|
267
|
+
maxRetries: 5,
|
|
268
|
+
retryDelay: 100
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
//#endregion
|
|
273
|
+
export { wacz_exports as n, exportWacz as t };
|
|
274
|
+
|
|
275
|
+
//# sourceMappingURL=wacz-sfOLT8e9.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wacz-sfOLT8e9.mjs","names":["NEWLINE"],"sources":["../src/sorter.ts","../src/wacz.ts"],"sourcesContent":["import { createReadStream } from \"node:fs\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nconst NEWLINE = 0x0a;\n\n/** This function compares two lines by their bytes. */\nconst byBytes = (a: Buffer, b: Buffer): number => Buffer.compare(a, b);\n\n/** This function reads a file line by line, as bytes. A line holds no line break. */\nasync function* linesOf(path: string): AsyncGenerator<Buffer> {\n let rest: Buffer | undefined;\n for await (const chunk of createReadStream(path) as AsyncIterable<Buffer>) {\n const data = rest === undefined ? chunk : Buffer.concat([rest, chunk]);\n rest = undefined;\n let start = 0;\n for (;;) {\n const end = data.indexOf(NEWLINE, start);\n if (end === -1) break;\n yield data.subarray(start, end);\n start = end + 1;\n }\n if (start < data.length) rest = Buffer.from(data.subarray(start));\n }\n if (rest !== undefined && rest.length > 0) yield rest;\n}\n\n/**\n * A line sorter sorts lines of text in the sequence of their UTF-8 bytes (the sequence of\n * `LC_ALL=C sort`). It keeps no more than `limit` bytes of lines in memory. When it has more, it\n * writes them to a sorted file in `dir`. At the end it merges the files. Thus its memory does not\n * grow with the count of lines. The WACZ export sorts its index and its page list with it.\n *\n * A line must not hold a line break.\n */\nexport class LineSorter {\n private lines: Buffer[] = [];\n private bytes = 0;\n private readonly runs: string[] = [];\n\n constructor(\n private readonly dir: string,\n private readonly limit: number = 8 * 1024 * 1024,\n ) {}\n\n /** This value is the count of sorted files that the sorter wrote. */\n get files(): number {\n return this.runs.length;\n }\n\n async add(line: string): Promise<void> {\n const bytes = Buffer.from(line, \"utf8\");\n if (bytes.includes(NEWLINE)) throw new Error(\"a line to sort holds a line break\");\n this.lines.push(bytes);\n // each line also costs an object in memory, so the count adds a little for each line\n this.bytes += bytes.length + 64;\n if (this.bytes >= this.limit) await this.spill();\n }\n\n private async spill(): Promise<void> {\n if (this.lines.length === 0) return;\n this.lines.sort(byBytes);\n await mkdir(this.dir, { recursive: true });\n const path = join(this.dir, `${String(this.runs.length)}.lines`);\n const parts: Buffer[] = [];\n const newline = Buffer.from([NEWLINE]);\n for (const line of this.lines) parts.push(line, newline);\n await writeFile(path, Buffer.concat(parts), { mode: 0o600 });\n this.runs.push(path);\n this.lines = [];\n this.bytes = 0;\n }\n\n /** This method gives each line in the sorted sequence. Use it one time, after the last `add`. */\n async *sorted(): AsyncGenerator<Buffer> {\n if (this.runs.length === 0) {\n const lines = this.lines.sort(byBytes);\n this.lines = [];\n yield* lines;\n return;\n }\n await this.spill();\n const readers = this.runs.map((path) => linesOf(path)[Symbol.asyncIterator]());\n const heads: Array<Buffer | undefined> = [];\n for (const reader of readers) heads.push((await reader.next()).value ?? undefined);\n for (;;) {\n let pick = -1;\n for (const [at, head] of heads.entries()) {\n if (head !== undefined && (pick === -1 || Buffer.compare(head, heads[pick]!) < 0))\n pick = at;\n }\n if (pick === -1) return;\n yield heads[pick]!;\n heads[pick] = (await readers[pick]!.next()).value ?? undefined;\n }\n }\n}\n","import { createHash } from \"node:crypto\";\nimport { mkdir, rename, rm } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport { LineSorter } from \"./sorter.ts\";\nimport { cacheKeyOf } from \"./urls.ts\";\nimport { VERSION } from \"./version.ts\";\nimport type { ArchiveCounts, ArchiveOptions, ArchiveSource } from \"./warc.ts\";\nimport { cdxjLine, outputOf, planArchive, writeArchive } from \"./warc.ts\";\nimport type { Tallied, ZipMember } from \"./zip.ts\";\nimport { TalliedFile, deflateFile, memberOf, writeZip } from \"./zip.ts\";\n\n/*\n * This module writes a WACZ 1.1.1 file: a ZIP with the WARC file, a sorted CDXJ index, a page list\n * and the data package with the SHA-256 of each part. Replay tools (ReplayWeb.page, pywb) open it.\n */\n\n/** This is the version of the WACZ specification that the export follows. */\nconst WACZ_VERSION = \"1.1.1\";\nconst WARC_NAME = \"data.warc.gz\";\nconst NEWLINE = Buffer.from(\"\\n\");\nconst TAB = 0x09;\nconst PAGES_HEADER = { format: \"json-pages-1.0\", id: \"pages\", title: \"All Pages\" };\n\n/** This is the result of `exportWacz`. */\nexport interface WaczResult extends ArchiveCounts {\n readonly format: \"wacz\";\n /** This value is the absolute path of the archive. */\n readonly out: string;\n readonly bytes: number;\n /** This value is the count of pages in `pages/pages.jsonl`: one for each document. */\n readonly pages: number;\n}\n\n/** This function writes lines to a file in batches of approximately 64 KiB. It gives the count of lines. */\nconst writeLines = async (file: TalliedFile, lines: AsyncIterable<Uint8Array>): Promise<number> => {\n let batch: Uint8Array[] = [];\n let size = 0;\n let count = 0;\n for await (const line of lines) {\n batch.push(line, NEWLINE);\n size += line.length + 1;\n count += 1;\n if (size >= 65_536) {\n await file.write(Buffer.concat(batch));\n batch = [];\n size = 0;\n }\n }\n if (batch.length > 0) await file.write(Buffer.concat(batch));\n return count;\n};\n\n/**\n * This function keeps the last line of each document. Each line is the key of the document, a tab,\n * the time, a tab and the JSON of the page. The sorter gives the lines of one key together, the\n * oldest first.\n */\nasync function* latestOf(lines: AsyncIterable<Buffer>): AsyncGenerator<Buffer> {\n let held: Buffer | undefined;\n let heldKey: Buffer | undefined;\n for await (const line of lines) {\n const key = line.subarray(0, line.indexOf(TAB));\n if (held !== undefined && heldKey !== undefined && !key.equals(heldKey)) {\n yield held.subarray(held.indexOf(TAB, heldKey.length + 1) + 1);\n }\n held = line;\n heldKey = key;\n }\n if (held !== undefined && heldKey !== undefined)\n yield held.subarray(held.indexOf(TAB, heldKey.length + 1) + 1);\n}\n\nconst resourceOf = (\n path: string,\n facts: Tallied,\n): {\n readonly name: string;\n readonly path: string;\n readonly hash: string;\n readonly bytes: number;\n} => ({\n name: basename(path).toLowerCase(),\n path,\n hash: `sha256:${facts.sha256}`,\n bytes: facts.size,\n});\n\n/**\n * This function writes the captures of a store as a WACZ 1.1.1 file. The ZIP holds these parts:\n *\n * - `archive/data.warc.gz`: the WARC file of `exportWarc`, stored with no more compression, so a\n * replay tool reads a record at its offset.\n * - `indexes/index.cdxj`: the index of the `response` and `revisit` records, sorted by SURT key.\n * - `pages/pages.jsonl`: one page for each document, at its latest capture.\n * - `datapackage.json` and `datapackage-digest.json`: the SHA-256 of each part, and of the package.\n *\n * The index and the page list are sorted on the disk, so the memory does not grow with the store.\n * The export writes temporary files beside `out` and removes them at the end.\n */\nexport const exportWacz = async (\n source: ArchiveSource,\n options: ArchiveOptions,\n): Promise<WaczResult> => {\n const plan = planArchive(source, options);\n const { target, temporary } = outputOf(options);\n const parts = `${temporary}.parts`;\n await mkdir(parts, { recursive: true, mode: 0o700 });\n try {\n const created = new Date();\n const warc = await TalliedFile.create(join(parts, WARC_NAME));\n const index = new LineSorter(join(parts, \"index\"));\n const pages = new LineSorter(join(parts, \"pages\"));\n let counts: ArchiveCounts;\n try {\n counts = await writeArchive(plan, warc, WARC_NAME, async (record) => {\n await index.add(cdxjLine(record, WARC_NAME));\n if (record.page) {\n const page = JSON.stringify({ id: record.capture, url: record.url, ts: record.date });\n await pages.add(`${cacheKeyOf(record.url)}\\t${record.date}\\t${page}`);\n }\n });\n } catch (error) {\n await warc.abandon();\n throw error;\n }\n const archive = await warc.close();\n\n const indexFile = await TalliedFile.create(join(parts, \"index.cdxj\"));\n try {\n await writeLines(indexFile, index.sorted());\n } catch (error) {\n await indexFile.abandon();\n throw error;\n }\n const indexFacts = await indexFile.close();\n\n const pagesFile = await TalliedFile.create(join(parts, \"pages.jsonl\"));\n let pageCount: number;\n try {\n await pagesFile.write(Buffer.from(`${JSON.stringify(PAGES_HEADER)}\\n`));\n pageCount = await writeLines(pagesFile, latestOf(pages.sorted()));\n } catch (error) {\n await pagesFile.abandon();\n throw error;\n }\n const pagesFacts = await pagesFile.close();\n\n const datapackage = Buffer.from(\n JSON.stringify(\n {\n profile: \"data-package\",\n wacz_version: WACZ_VERSION,\n software: `polite-crawler ${VERSION}`,\n created: created.toISOString().replace(/\\.\\d{3}Z$/, \"Z\"),\n title: \"polite-crawler export\",\n resources: [\n resourceOf(`archive/${WARC_NAME}`, archive),\n resourceOf(\"indexes/index.cdxj\", indexFacts),\n resourceOf(\"pages/pages.jsonl\", pagesFacts),\n ],\n },\n null,\n 2,\n ),\n \"utf8\",\n );\n const digest = Buffer.from(\n JSON.stringify(\n {\n path: \"datapackage.json\",\n hash: `sha256:${createHash(\"sha256\").update(datapackage).digest(\"hex\")}`,\n },\n null,\n 2,\n ),\n \"utf8\",\n );\n\n const deflated = async (name: string, facts: Tallied): Promise<ZipMember> => {\n const file = `${facts.path}.deflate`;\n return {\n name,\n method: 8,\n crc: facts.crc,\n size: facts.size,\n compressedSize: await deflateFile(facts.path, file),\n file,\n };\n };\n const members: ZipMember[] = [\n {\n name: `archive/${WARC_NAME}`,\n method: 0,\n crc: archive.crc,\n size: archive.size,\n compressedSize: archive.size,\n file: archive.path,\n },\n await deflated(\"indexes/index.cdxj\", indexFacts),\n await deflated(\"pages/pages.jsonl\", pagesFacts),\n memberOf(\"datapackage.json\", datapackage),\n memberOf(\"datapackage-digest.json\", digest),\n ];\n let bytes: number;\n try {\n bytes = await writeZip(temporary, members, { date: created });\n await rename(temporary, target);\n } catch (error) {\n await rm(temporary, { force: true }).catch(() => undefined);\n throw error;\n }\n return { format: \"wacz\", out: target, bytes, pages: pageCount, ...counts };\n } finally {\n await rm(parts, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });\n }\n};\n"],"mappings":"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"}
|