@nitpicker/crawler 0.13.0 → 0.15.0
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/lib/archive/archive-accessor.d.ts +56 -0
- package/lib/archive/archive-accessor.js +72 -0
- package/lib/archive/archive.d.ts +108 -9
- package/lib/archive/archive.js +109 -7
- package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
- package/lib/archive/body-hash/compute-body-hash.js +31 -0
- package/lib/archive/body-hash/extract-body.d.ts +18 -0
- package/lib/archive/body-hash/extract-body.js +29 -0
- package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
- package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
- package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
- package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
- package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
- package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
- package/lib/archive/cache/clear-archive-cache-root.js +32 -0
- package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
- package/lib/archive/cache/list-archive-cache-entries.js +125 -0
- package/lib/archive/cache/path-exists.d.ts +9 -0
- package/lib/archive/cache/path-exists.js +18 -0
- package/lib/archive/cache/types.d.ts +33 -0
- package/lib/archive/cache/types.js +1 -0
- package/lib/archive/create-adjunct-tables.d.ts +18 -0
- package/lib/archive/create-adjunct-tables.js +277 -0
- package/lib/archive/create-entity-tables.d.ts +14 -0
- package/lib/archive/create-entity-tables.js +52 -1
- package/lib/archive/database.d.ts +91 -2
- package/lib/archive/database.js +128 -0
- package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
- package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
- package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
- package/lib/archive/db-ops/_shared/types.d.ts +2 -0
- package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
- package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +29 -0
- package/lib/archive/db-ops/analysis/replace-page-templates.js +84 -0
- package/lib/archive/db-ops/analysis/types.d.ts +61 -0
- package/lib/archive/db-ops/analysis/types.js +1 -0
- package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
- package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
- package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
- package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
- package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
- package/lib/archive/db-ops/console-logs/types.js +1 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
- package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
- package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
- package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
- package/lib/archive/db-ops/lifecycle/init.d.ts +25 -10
- package/lib/archive/db-ops/lifecycle/init.js +39 -10
- package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
- package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
- package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
- package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
- package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
- package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
- package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
- package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
- package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
- package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +18 -3
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +48 -10
- package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
- package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
- package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
- package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
- package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
- package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
- package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
- package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
- package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
- package/lib/archive/db-ops/pages/write/update-page.js +33 -0
- package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
- package/lib/archive/filesystem/output-binary.d.ts +12 -0
- package/lib/archive/filesystem/output-binary.js +17 -0
- package/lib/archive/get-failed-page-messages.d.ts +20 -8
- package/lib/archive/get-failed-page-messages.js +29 -15
- package/lib/archive/init-schema.js +2 -1
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
- package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
- package/lib/archive/meta/types.d.ts +263 -0
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
- package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
- package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
- package/lib/archive/migrate-info-main-content-selector.js +24 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
- package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
- package/lib/archive/migrate-main-contents-columns.js +50 -0
- package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
- package/lib/archive/migrate-page-meta-body-hash.js +52 -0
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
- package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
- package/lib/archive/page.d.ts +145 -3
- package/lib/archive/page.js +195 -0
- package/lib/archive/types.d.ts +86 -2
- package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
- package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
- package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
- package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
- package/lib/archive/url-alias/format-alias-key.js +14 -0
- package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
- package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
- package/lib/archive/url-alias/types.d.ts +7 -0
- package/lib/archive/url-alias/types.js +1 -0
- package/lib/crawler/choose-probe-host.d.ts +24 -0
- package/lib/crawler/choose-probe-host.js +38 -0
- package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
- package/lib/crawler/crawler.js +261 -2
- package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
- package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
- package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
- package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
- package/lib/crawler/fetch-destination.js +2 -0
- package/lib/crawler/link-to-page-data.js +2 -0
- package/lib/crawler/network-gate.d.ts +49 -0
- package/lib/crawler/network-gate.js +78 -0
- package/lib/crawler/network-outage-detector.d.ts +51 -0
- package/lib/crawler/network-outage-detector.js +81 -0
- package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
- package/lib/crawler/network-outage-summary-counter.js +18 -0
- package/lib/crawler/probe-network.d.ts +26 -0
- package/lib/crawler/probe-network.js +21 -0
- package/lib/crawler/resource-to-page-data.js +2 -0
- package/lib/crawler/types.d.ts +152 -1
- package/lib/crawler-orchestrator.d.ts +62 -18
- package/lib/crawler-orchestrator.js +163 -26
- package/lib/crawler.d.ts +21 -0
- package/lib/crawler.js +17 -0
- package/lib/is-within-outage-window.d.ts +49 -0
- package/lib/is-within-outage-window.js +33 -0
- package/lib/network-related-error-kinds.d.ts +23 -0
- package/lib/network-related-error-kinds.js +28 -0
- package/lib/types.d.ts +9 -1
- package/lib/utils/array/each-splitted.d.ts +1 -1
- package/lib/utils/compute-file-sha256.d.ts +17 -17
- package/lib/utils/compute-file-sha256.js +18 -47
- package/package.json +3 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces mixed-alphanumeric tokens of 8+ characters with a fixed
|
|
3
|
+
* placeholder, so that two bodies differing only in an embedded dynamic
|
|
4
|
+
* value (a cache-busting hash, a per-build CSS-module suffix, a session or
|
|
5
|
+
* order id) hash the same.
|
|
6
|
+
*
|
|
7
|
+
* A token is masked only when it mixes letters and digits — a pure-digit
|
|
8
|
+
* match (phone numbers, product codes, dates) or a pure-alphabetic match (an
|
|
9
|
+
* ordinary English word) is left untouched, since those are far more likely
|
|
10
|
+
* to be stable content than a dynamic identifier.
|
|
11
|
+
*
|
|
12
|
+
* This is a heuristic, not a semantic classifier, and it is accepted as such:
|
|
13
|
+
* a mixed-alphanumeric SKU/order/product code that is itself the only
|
|
14
|
+
* distinguishing content between two otherwise-identical pages (e.g.
|
|
15
|
+
* `ABC12345` vs `XYZ98765`) also gets masked, collapsing two genuinely
|
|
16
|
+
* different pages into one `computeBodyHash` duplicate-detection group — a
|
|
17
|
+
* false positive. Narrowing the pattern to reduce that risk would also
|
|
18
|
+
* narrow what it catches (the whole point of this mask is to absorb
|
|
19
|
+
* cache-busting hashes and CSS-module suffixes whose shape is
|
|
20
|
+
* indistinguishable from such a code), so this trade-off is deliberate
|
|
21
|
+
* rather than something a stricter regex would cleanly resolve.
|
|
22
|
+
*
|
|
23
|
+
* The placeholder is a fixed constant rather than a per-match incrementing
|
|
24
|
+
* id: nothing needs to reverse the substitution back to the original value —
|
|
25
|
+
* the full original HTML is already preserved verbatim in `page_html_blobs`,
|
|
26
|
+
* so recovering it here would be redundant. All that matters for duplicate
|
|
27
|
+
* detection is that every masked token collapses to the same value.
|
|
28
|
+
* @param text - Text to mask (already URL-normalized by
|
|
29
|
+
* `normalizeUrlLikeStrings`).
|
|
30
|
+
* @returns `text` with every mixed-alphanumeric 8+ character token replaced
|
|
31
|
+
* by a fixed placeholder.
|
|
32
|
+
*/
|
|
33
|
+
export declare function maskDynamicIds(text: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const DYNAMIC_ID_PATTERN = /[a-z0-9]{8,}/gi;
|
|
2
|
+
const DIGITS_ONLY_PATTERN = /^\d+$/;
|
|
3
|
+
const ALPHA_ONLY_PATTERN = /^[a-z]+$/i;
|
|
4
|
+
const MASK_PLACEHOLDER = '__MASKED_ID__';
|
|
5
|
+
/**
|
|
6
|
+
* Replaces mixed-alphanumeric tokens of 8+ characters with a fixed
|
|
7
|
+
* placeholder, so that two bodies differing only in an embedded dynamic
|
|
8
|
+
* value (a cache-busting hash, a per-build CSS-module suffix, a session or
|
|
9
|
+
* order id) hash the same.
|
|
10
|
+
*
|
|
11
|
+
* A token is masked only when it mixes letters and digits — a pure-digit
|
|
12
|
+
* match (phone numbers, product codes, dates) or a pure-alphabetic match (an
|
|
13
|
+
* ordinary English word) is left untouched, since those are far more likely
|
|
14
|
+
* to be stable content than a dynamic identifier.
|
|
15
|
+
*
|
|
16
|
+
* This is a heuristic, not a semantic classifier, and it is accepted as such:
|
|
17
|
+
* a mixed-alphanumeric SKU/order/product code that is itself the only
|
|
18
|
+
* distinguishing content between two otherwise-identical pages (e.g.
|
|
19
|
+
* `ABC12345` vs `XYZ98765`) also gets masked, collapsing two genuinely
|
|
20
|
+
* different pages into one `computeBodyHash` duplicate-detection group — a
|
|
21
|
+
* false positive. Narrowing the pattern to reduce that risk would also
|
|
22
|
+
* narrow what it catches (the whole point of this mask is to absorb
|
|
23
|
+
* cache-busting hashes and CSS-module suffixes whose shape is
|
|
24
|
+
* indistinguishable from such a code), so this trade-off is deliberate
|
|
25
|
+
* rather than something a stricter regex would cleanly resolve.
|
|
26
|
+
*
|
|
27
|
+
* The placeholder is a fixed constant rather than a per-match incrementing
|
|
28
|
+
* id: nothing needs to reverse the substitution back to the original value —
|
|
29
|
+
* the full original HTML is already preserved verbatim in `page_html_blobs`,
|
|
30
|
+
* so recovering it here would be redundant. All that matters for duplicate
|
|
31
|
+
* detection is that every masked token collapses to the same value.
|
|
32
|
+
* @param text - Text to mask (already URL-normalized by
|
|
33
|
+
* `normalizeUrlLikeStrings`).
|
|
34
|
+
* @returns `text` with every mixed-alphanumeric 8+ character token replaced
|
|
35
|
+
* by a fixed placeholder.
|
|
36
|
+
*/
|
|
37
|
+
export function maskDynamicIds(text) {
|
|
38
|
+
return text.replaceAll(DYNAMIC_ID_PATTERN, (match) => {
|
|
39
|
+
if (DIGITS_ONLY_PATTERN.test(match) || ALPHA_ONLY_PATTERN.test(match)) {
|
|
40
|
+
return match;
|
|
41
|
+
}
|
|
42
|
+
return MASK_PLACEHOLDER;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes `/index.{ext}` path suffixes (`/index.html`, `/index.php`, ...)
|
|
3
|
+
* to a bare trailing `/` throughout the given text, so that two otherwise
|
|
4
|
+
* identical bodies that differ only in which equivalent URL form a template
|
|
5
|
+
* happened to render (`/about/` vs `/about/index.html`) hash the same.
|
|
6
|
+
*
|
|
7
|
+
* Applied as a blanket string sweep over the whole body — not scoped to
|
|
8
|
+
* `href`/`src` attribute values — because the same trailing-suffix variance
|
|
9
|
+
* can appear anywhere a URL-shaped string is rendered as text (breadcrumbs,
|
|
10
|
+
* "print this page" widgets, JSON embedded in an inline `<script>`), and the
|
|
11
|
+
* body is treated as an opaque string for masking purposes (see
|
|
12
|
+
* `computeBodyHash`).
|
|
13
|
+
* @param body - The `<body>` inner HTML (or any text) to normalize.
|
|
14
|
+
* @returns `body` with every `/index.{ext}` suffix collapsed to `/`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeUrlLikeStrings(body: string): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const INDEX_SUFFIX_PATTERN = /\/index\.\w+/gi;
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes `/index.{ext}` path suffixes (`/index.html`, `/index.php`, ...)
|
|
4
|
+
* to a bare trailing `/` throughout the given text, so that two otherwise
|
|
5
|
+
* identical bodies that differ only in which equivalent URL form a template
|
|
6
|
+
* happened to render (`/about/` vs `/about/index.html`) hash the same.
|
|
7
|
+
*
|
|
8
|
+
* Applied as a blanket string sweep over the whole body — not scoped to
|
|
9
|
+
* `href`/`src` attribute values — because the same trailing-suffix variance
|
|
10
|
+
* can appear anywhere a URL-shaped string is rendered as text (breadcrumbs,
|
|
11
|
+
* "print this page" widgets, JSON embedded in an inline `<script>`), and the
|
|
12
|
+
* body is treated as an opaque string for masking purposes (see
|
|
13
|
+
* `computeBodyHash`).
|
|
14
|
+
* @param body - The `<body>` inner HTML (or any text) to normalize.
|
|
15
|
+
* @returns `body` with every `/index.{ext}` suffix collapsed to `/`.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeUrlLikeStrings(body) {
|
|
18
|
+
return body.replaceAll(INDEX_SUFFIX_PATTERN, '/');
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove a single archive cache entry (one directory previously resolved by
|
|
3
|
+
* {@link import('./resolve-archive-cache-dir.js').resolveArchiveCacheDir}).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately scoped to exactly `cacheDir` — it never touches sibling
|
|
6
|
+
* entries under the same cache root (in particular, the `@nitpicker/core`
|
|
7
|
+
* analyze `table` cache is never archive-scoped and must survive a
|
|
8
|
+
* per-archive clear).
|
|
9
|
+
* @param cacheDir - Absolute path to the single cache entry to remove.
|
|
10
|
+
* @returns `true` if `cacheDir` existed and was removed, `false` if it was
|
|
11
|
+
* already absent.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const cacheKey = await computeArchiveCacheKey(archivePath);
|
|
15
|
+
* const cacheDir = resolveArchiveCacheDir(cacheRoot, cacheKey, archivePath);
|
|
16
|
+
* const removed = await clearArchiveCacheEntry(cacheDir);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function clearArchiveCacheEntry(cacheDir: string): Promise<boolean>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { pathExists } from './path-exists.js';
|
|
3
|
+
/**
|
|
4
|
+
* Remove a single archive cache entry (one directory previously resolved by
|
|
5
|
+
* {@link import('./resolve-archive-cache-dir.js').resolveArchiveCacheDir}).
|
|
6
|
+
*
|
|
7
|
+
* Deliberately scoped to exactly `cacheDir` — it never touches sibling
|
|
8
|
+
* entries under the same cache root (in particular, the `@nitpicker/core`
|
|
9
|
+
* analyze `table` cache is never archive-scoped and must survive a
|
|
10
|
+
* per-archive clear).
|
|
11
|
+
* @param cacheDir - Absolute path to the single cache entry to remove.
|
|
12
|
+
* @returns `true` if `cacheDir` existed and was removed, `false` if it was
|
|
13
|
+
* already absent.
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const cacheKey = await computeArchiveCacheKey(archivePath);
|
|
17
|
+
* const cacheDir = resolveArchiveCacheDir(cacheRoot, cacheKey, archivePath);
|
|
18
|
+
* const removed = await clearArchiveCacheEntry(cacheDir);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export async function clearArchiveCacheEntry(cacheDir) {
|
|
22
|
+
const existedBefore = await pathExists(cacheDir);
|
|
23
|
+
await fs.rm(cacheDir, { recursive: true, force: true });
|
|
24
|
+
return existedBefore;
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove an entire archive cache root, including every tar-cache entry and
|
|
3
|
+
* any sibling directory living under it (e.g. the `@nitpicker/core` analyze
|
|
4
|
+
* `table` cache, which is resolved as a child of the same root).
|
|
5
|
+
*
|
|
6
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself, so
|
|
7
|
+
* tests can point it at a throwaway directory instead of the real OS temp
|
|
8
|
+
* cache. Mirrors the "manual `rm -rf` is safe" contract already documented
|
|
9
|
+
* for the tar cache (ARCHITECTURE.md) — no confirmation, no lock check.
|
|
10
|
+
*
|
|
11
|
+
* Why not lock-aware: a concurrent `extractArchiveToCache` elsewhere may be
|
|
12
|
+
* holding `<cacheDir>.lock` / writing `<cacheDir>.staging` inside this root
|
|
13
|
+
* when it is removed, which can surface as an ENOENT in that extraction (or,
|
|
14
|
+
* rarely, a second extractor racing into a freshly recreated `.staging`
|
|
15
|
+
* path). This is the same exposure a manual `rm -rf` already has today —
|
|
16
|
+
* this function does not add new risk, it just makes that pre-existing,
|
|
17
|
+
* accepted risk reachable via a single explicit command.
|
|
18
|
+
* @param cacheRoot - Absolute path to the cache root to remove.
|
|
19
|
+
* @returns `true` if `cacheRoot` existed and was removed, `false` if it was
|
|
20
|
+
* already absent.
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const removed = await clearArchiveCacheRoot(getArchiveCacheRoot());
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function clearArchiveCacheRoot(cacheRoot: string): Promise<boolean>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { pathExists } from './path-exists.js';
|
|
3
|
+
/**
|
|
4
|
+
* Remove an entire archive cache root, including every tar-cache entry and
|
|
5
|
+
* any sibling directory living under it (e.g. the `@nitpicker/core` analyze
|
|
6
|
+
* `table` cache, which is resolved as a child of the same root).
|
|
7
|
+
*
|
|
8
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself, so
|
|
9
|
+
* tests can point it at a throwaway directory instead of the real OS temp
|
|
10
|
+
* cache. Mirrors the "manual `rm -rf` is safe" contract already documented
|
|
11
|
+
* for the tar cache (ARCHITECTURE.md) — no confirmation, no lock check.
|
|
12
|
+
*
|
|
13
|
+
* Why not lock-aware: a concurrent `extractArchiveToCache` elsewhere may be
|
|
14
|
+
* holding `<cacheDir>.lock` / writing `<cacheDir>.staging` inside this root
|
|
15
|
+
* when it is removed, which can surface as an ENOENT in that extraction (or,
|
|
16
|
+
* rarely, a second extractor racing into a freshly recreated `.staging`
|
|
17
|
+
* path). This is the same exposure a manual `rm -rf` already has today —
|
|
18
|
+
* this function does not add new risk, it just makes that pre-existing,
|
|
19
|
+
* accepted risk reachable via a single explicit command.
|
|
20
|
+
* @param cacheRoot - Absolute path to the cache root to remove.
|
|
21
|
+
* @returns `true` if `cacheRoot` existed and was removed, `false` if it was
|
|
22
|
+
* already absent.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const removed = await clearArchiveCacheRoot(getArchiveCacheRoot());
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export async function clearArchiveCacheRoot(cacheRoot) {
|
|
29
|
+
const existedBefore = await pathExists(cacheRoot);
|
|
30
|
+
await fs.rm(cacheRoot, { recursive: true, force: true });
|
|
31
|
+
return existedBefore;
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ArchiveCacheEntry } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* List the top-level entries found directly under an archive cache root,
|
|
4
|
+
* with a recursive size total and the most recent mtime found in each.
|
|
5
|
+
*
|
|
6
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself (see
|
|
7
|
+
* {@link import('./get-archive-cache-root.js').getArchiveCacheRoot} for the
|
|
8
|
+
* production root), so tests can point it at a throwaway directory instead
|
|
9
|
+
* of the real OS temp cache.
|
|
10
|
+
*
|
|
11
|
+
* Symbolic links are reported but never followed, both at the top level and
|
|
12
|
+
* while walking a directory's contents — this avoids escaping `cacheRoot`
|
|
13
|
+
* and crashing on dangling links.
|
|
14
|
+
* @param cacheRoot - Absolute path to the cache root to inspect.
|
|
15
|
+
* @returns One entry per top-level child of `cacheRoot`, in `fs.readdir`
|
|
16
|
+
* order. Returns `[]` if `cacheRoot` does not exist.
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const entries = await listArchiveCacheEntries(getArchiveCacheRoot());
|
|
20
|
+
* const totalBytes = entries.reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function listArchiveCacheEntries(cacheRoot: string): Promise<ArchiveCacheEntry[]>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* List the top-level entries found directly under an archive cache root,
|
|
5
|
+
* with a recursive size total and the most recent mtime found in each.
|
|
6
|
+
*
|
|
7
|
+
* Pure: takes `cacheRoot` as a parameter and never resolves it itself (see
|
|
8
|
+
* {@link import('./get-archive-cache-root.js').getArchiveCacheRoot} for the
|
|
9
|
+
* production root), so tests can point it at a throwaway directory instead
|
|
10
|
+
* of the real OS temp cache.
|
|
11
|
+
*
|
|
12
|
+
* Symbolic links are reported but never followed, both at the top level and
|
|
13
|
+
* while walking a directory's contents — this avoids escaping `cacheRoot`
|
|
14
|
+
* and crashing on dangling links.
|
|
15
|
+
* @param cacheRoot - Absolute path to the cache root to inspect.
|
|
16
|
+
* @returns One entry per top-level child of `cacheRoot`, in `fs.readdir`
|
|
17
|
+
* order. Returns `[]` if `cacheRoot` does not exist.
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const entries = await listArchiveCacheEntries(getArchiveCacheRoot());
|
|
21
|
+
* const totalBytes = entries.reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export async function listArchiveCacheEntries(cacheRoot) {
|
|
25
|
+
let topLevel;
|
|
26
|
+
try {
|
|
27
|
+
topLevel = await fs.readdir(cacheRoot, { withFileTypes: true });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT') {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const dirent of topLevel) {
|
|
37
|
+
const entryPath = path.join(cacheRoot, dirent.name);
|
|
38
|
+
if (dirent.isSymbolicLink()) {
|
|
39
|
+
entries.push({
|
|
40
|
+
kind: 'unknown',
|
|
41
|
+
name: dirent.name,
|
|
42
|
+
path: entryPath,
|
|
43
|
+
sizeBytes: 0,
|
|
44
|
+
mtimeMs: 0,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (dirent.isDirectory()) {
|
|
49
|
+
const { sizeBytes, mtimeMs } = await computeDirectoryStats(entryPath);
|
|
50
|
+
entries.push({
|
|
51
|
+
kind: classifyEntryName(dirent.name),
|
|
52
|
+
name: dirent.name,
|
|
53
|
+
path: entryPath,
|
|
54
|
+
sizeBytes,
|
|
55
|
+
mtimeMs,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const stat = await fs.stat(entryPath);
|
|
60
|
+
entries.push({
|
|
61
|
+
kind: 'unknown',
|
|
62
|
+
name: dirent.name,
|
|
63
|
+
path: entryPath,
|
|
64
|
+
sizeBytes: stat.size,
|
|
65
|
+
mtimeMs: stat.mtimeMs,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Matches the corrupt-quarantine suffix `extractArchiveToCache` appends
|
|
72
|
+
* (`${cacheDir}.corrupt.${pid}.${counter}`, both segments always numeric —
|
|
73
|
+
* see `quarantineHalfPopulatedCacheDir` in `extract-archive-to-cache.ts`).
|
|
74
|
+
* Anchored and numeric-specific rather than a loose `includes('.corrupt.')`
|
|
75
|
+
* so an archive whose own (sanitized) basename merely contains the
|
|
76
|
+
* substring `.corrupt.` — e.g. `my.corrupt.report.nitpicker` — is not
|
|
77
|
+
* misclassified as an orphan and offered up for deletion by `cache list`.
|
|
78
|
+
*/
|
|
79
|
+
const CORRUPT_QUARANTINE_SUFFIX = /\.corrupt\.\d+\.\d+$/;
|
|
80
|
+
/**
|
|
81
|
+
* Classify a cache-root child by name pattern alone (no filesystem access).
|
|
82
|
+
*
|
|
83
|
+
* This is a heuristic: `.staging`/corrupt-quarantine suffixes are appended
|
|
84
|
+
* by `extractArchiveToCache` onto an existing tar-cache dir name, so an
|
|
85
|
+
* archive whose own sanitized basename happens to end in exactly one of
|
|
86
|
+
* these literal suffixes is indistinguishable from a real orphan by name
|
|
87
|
+
* alone. Disambiguating fully would require re-deriving each entry's
|
|
88
|
+
* expected name from its source archive, which `list`/`clear` (by design,
|
|
89
|
+
* see grill-me scope) never has access to. Accepted as a rare, low-cost
|
|
90
|
+
* misclassification.
|
|
91
|
+
* @param name - Base name of the top-level entry.
|
|
92
|
+
*/
|
|
93
|
+
function classifyEntryName(name) {
|
|
94
|
+
if (name === 'table') {
|
|
95
|
+
return 'table';
|
|
96
|
+
}
|
|
97
|
+
if (name.endsWith('.staging') || CORRUPT_QUARANTINE_SUFFIX.test(name)) {
|
|
98
|
+
return 'orphan';
|
|
99
|
+
}
|
|
100
|
+
return 'tar-cache';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Recursively sum file sizes and find the most recent mtime under a
|
|
104
|
+
* directory. Symbolic links are neither followed nor counted.
|
|
105
|
+
* @param dirPath - Absolute path to the directory to walk.
|
|
106
|
+
*/
|
|
107
|
+
async function computeDirectoryStats(dirPath) {
|
|
108
|
+
let sizeBytes = 0;
|
|
109
|
+
let mtimeMs = 0;
|
|
110
|
+
const children = await fs.readdir(dirPath, { recursive: true, withFileTypes: true });
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
if (!child.isFile()) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const childPath = path.join(child.parentPath, child.name);
|
|
116
|
+
const stat = await fs.stat(childPath);
|
|
117
|
+
sizeBytes += stat.size;
|
|
118
|
+
mtimeMs = Math.max(mtimeMs, stat.mtimeMs);
|
|
119
|
+
}
|
|
120
|
+
if (mtimeMs === 0) {
|
|
121
|
+
const dirStat = await fs.stat(dirPath);
|
|
122
|
+
mtimeMs = dirStat.mtimeMs;
|
|
123
|
+
}
|
|
124
|
+
return { sizeBytes, mtimeMs };
|
|
125
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async existence probe — avoids blocking the event loop on the common
|
|
3
|
+
* "check before remove" path shared by
|
|
4
|
+
* {@link import('./clear-archive-cache-root.js').clearArchiveCacheRoot} and
|
|
5
|
+
* {@link import('./clear-archive-cache-entry.js').clearArchiveCacheEntry}.
|
|
6
|
+
* @param targetPath - Absolute path to probe.
|
|
7
|
+
* @returns `true` if the path is reachable via `fs.access`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function pathExists(targetPath: string): Promise<boolean>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
/**
|
|
3
|
+
* Async existence probe — avoids blocking the event loop on the common
|
|
4
|
+
* "check before remove" path shared by
|
|
5
|
+
* {@link import('./clear-archive-cache-root.js').clearArchiveCacheRoot} and
|
|
6
|
+
* {@link import('./clear-archive-cache-entry.js').clearArchiveCacheEntry}.
|
|
7
|
+
* @param targetPath - Absolute path to probe.
|
|
8
|
+
* @returns `true` if the path is reachable via `fs.access`.
|
|
9
|
+
*/
|
|
10
|
+
export async function pathExists(targetPath) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(targetPath);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification of a top-level entry found directly under an archive cache
|
|
3
|
+
* root ({@link import('./get-archive-cache-root.js').getArchiveCacheRoot}).
|
|
4
|
+
*
|
|
5
|
+
* - `'tar-cache'`: a per-archive extraction produced by
|
|
6
|
+
* {@link import('./resolve-archive-cache-dir.js').resolveArchiveCacheDir}
|
|
7
|
+
* (`<cacheKey>-<safeBasename>`).
|
|
8
|
+
* - `'table'`: the `@nitpicker/core` analyze-plugin scratch cache
|
|
9
|
+
* (`getTableCacheRoot()`), a sibling directory literally named `table`.
|
|
10
|
+
* - `'orphan'`: a `.staging` or `.corrupt.<pid>.<n>` leftover from an
|
|
11
|
+
* interrupted {@link import('./extract-archive-to-cache.js').extractArchiveToCache}
|
|
12
|
+
* run (see that file's quarantine/staging logic).
|
|
13
|
+
* - `'unknown'`: anything else (a stray file, a foreign directory a user
|
|
14
|
+
* dropped into the cache root).
|
|
15
|
+
*
|
|
16
|
+
* `'tar-cache'` and `'table'` cannot collide: `resolveArchiveCacheDir` always
|
|
17
|
+
* prefixes the cache key (digits and hyphens), so a tar-cache directory name
|
|
18
|
+
* is never the bare literal `table`.
|
|
19
|
+
*/
|
|
20
|
+
export type ArchiveCacheEntryKind = 'tar-cache' | 'table' | 'orphan' | 'unknown';
|
|
21
|
+
/** A single top-level entry found under an archive cache root. */
|
|
22
|
+
export interface ArchiveCacheEntry {
|
|
23
|
+
/** How this entry was classified by name pattern. */
|
|
24
|
+
readonly kind: ArchiveCacheEntryKind;
|
|
25
|
+
/** Base name of the entry (its final path segment). */
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** Absolute path to the entry. */
|
|
28
|
+
readonly path: string;
|
|
29
|
+
/** Total size in bytes of all regular files found under this entry (recursive for directories). */
|
|
30
|
+
readonly sizeBytes: number;
|
|
31
|
+
/** Most recent mtime (epoch ms) found among the entry's files, or the entry's own mtime if it has none. */
|
|
32
|
+
readonly mtimeMs: number;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -9,11 +9,29 @@ import type { Knex } from 'knex';
|
|
|
9
9
|
* an external link that failed DNS, or null for a process-level error)
|
|
10
10
|
* - `page_tags` — Wappalyzer detections, FK → `content_items(id)`
|
|
11
11
|
* - `page_jsonld` — JSON-LD / SpeculationRules, FK → `content_items(id)`
|
|
12
|
+
* - `page_main_content_headings` / `_images` / `_tables` / `_buttons` /
|
|
13
|
+
* `_iframes` / `_videos` / `_audios` / `_canvases` — beholder
|
|
14
|
+
* `MainContentsData` sub-entity arrays, one row per DOM element, FK →
|
|
15
|
+
* `content_items(id)`
|
|
12
16
|
* - `inventory_runs` — `--inventory` audit log (no FK; append-only)
|
|
17
|
+
* - `network_outages` — operator-network-outage journal (no FK; append-only
|
|
18
|
+
* except `ended_at`, which is written once on recovery)
|
|
13
19
|
* - `analysis_text_refs` + `analysis_violations` — analyze-phase findings,
|
|
14
20
|
* FK → `content_items(id)`
|
|
21
|
+
* - `page_templates` — DOM-structure template classification (`--templates`,
|
|
22
|
+
* `@nitpicker/core`'s `template-classification/`), one row per classified
|
|
23
|
+
* page, FK → `content_items(id)`
|
|
24
|
+
* - `page_template_clusters` — one row per distinct `page_templates.template_key`,
|
|
25
|
+
* holding `@d-zero/page-cluster`'s cluster-selection evidence (no FK;
|
|
26
|
+
* `template_key` is not a `page_templates` FK target, so consistency is
|
|
27
|
+
* maintained by replacing both tables together, not by a foreign key)
|
|
15
28
|
* - `page_html_blobs` + `page_html_ref` — content-addressable HTML
|
|
16
29
|
* snapshots, FK → `content_items(id)`
|
|
30
|
+
* - `console_log_items` — content-addressable dictionary of distinct
|
|
31
|
+
* console messages / page errors (no FK; hash-deduplicated across every
|
|
32
|
+
* page in the archive, mirroring `text_refs` / `json_refs`)
|
|
33
|
+
* - `page_console_logs` — one row per (page, console log) occurrence, FK →
|
|
34
|
+
* `content_items(id)` and `console_log_items(id)`
|
|
17
35
|
*
|
|
18
36
|
* The DDL is shared between fresh-archive provisioning ({@link initSchema}
|
|
19
37
|
* calls this right after `createEntityTables`) and the migration script
|