@nitpicker/crawler 0.18.0 → 0.19.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/append-retry-failed-common-setup-phases.d.ts +15 -0
- package/lib/append-retry-failed-common-setup-phases.js +21 -0
- package/lib/append-setup-phases.d.ts +17 -0
- package/lib/append-setup-phases.js +22 -0
- package/lib/archive/archive-accessor.d.ts +31 -5
- package/lib/archive/archive-accessor.js +46 -24
- package/lib/archive/archive.d.ts +115 -37
- package/lib/archive/archive.js +118 -52
- package/lib/archive/cache/extract-archive-to-cache.d.ts +10 -1
- package/lib/archive/cache/extract-archive-to-cache.js +14 -4
- package/lib/archive/create-adjunct-tables.d.ts +12 -2
- package/lib/archive/create-adjunct-tables.js +91 -29
- package/lib/archive/create-entity-tables.d.ts +5 -1
- package/lib/archive/create-entity-tables.js +6 -1
- package/lib/archive/database.d.ts +39 -9
- package/lib/archive/database.js +63 -18
- package/lib/archive/db-ops/lifecycle/init.d.ts +14 -2
- package/lib/archive/db-ops/lifecycle/init.js +29 -11
- package/lib/archive/db-ops/meta/get-custom-elements-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-custom-elements-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-page-technologies-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-page-technologies-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-technology-signals-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-technology-signals-of-page.js +15 -0
- package/lib/archive/db-ops/pages/order/set-url-order.d.ts +6 -1
- package/lib/archive/db-ops/pages/order/set-url-order.js +7 -1
- package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +6 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +12 -3
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +6 -1
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +10 -2
- package/lib/archive/db-ops/pages/write/insert-custom-elements.d.ts +27 -0
- package/lib/archive/db-ops/pages/write/insert-custom-elements.js +40 -0
- package/lib/archive/db-ops/pages/write/insert-page.d.ts +24 -4
- package/lib/archive/db-ops/pages/write/insert-page.js +40 -4
- package/lib/archive/db-ops/pages/write/insert-technologies.d.ts +25 -0
- package/lib/archive/db-ops/pages/write/insert-technologies.js +45 -0
- package/lib/archive/db-ops/pages/write/link-redirect-sources.d.ts +7 -1
- package/lib/archive/db-ops/pages/write/link-redirect-sources.js +8 -2
- package/lib/archive/db-ops/pages/write/update-page.d.ts +4 -1
- package/lib/archive/db-ops/pages/write/update-page.js +38 -16
- package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +12 -1
- package/lib/archive/db-ops/resources/get-resource-url-list.js +43 -5
- package/lib/archive/filesystem/copy-file-with-progress.d.ts +20 -0
- package/lib/archive/filesystem/copy-file-with-progress.js +34 -0
- package/lib/archive/filesystem/parse-pax-path.d.ts +18 -0
- package/lib/archive/filesystem/parse-pax-path.js +47 -0
- package/lib/archive/filesystem/parse-tar-size-field.d.ts +13 -0
- package/lib/archive/filesystem/parse-tar-size-field.js +40 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +10 -3
- package/lib/archive/filesystem/peek-tar-top-dir.js +166 -16
- package/lib/archive/filesystem/tar.d.ts +13 -1
- package/lib/archive/filesystem/tar.js +63 -7
- package/lib/archive/filesystem/untar.d.ts +13 -0
- package/lib/archive/filesystem/untar.js +68 -8
- package/lib/archive/init-schema.d.ts +1 -1
- package/lib/archive/init-schema.js +6 -4
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +13 -2
- package/lib/archive/meta/compute-main-contents-denormalized.js +15 -2
- package/lib/archive/meta/technologies/combine-technology-confidence.d.ts +46 -0
- package/lib/archive/meta/technologies/combine-technology-confidence.js +93 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.d.ts +45 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.js +53 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.d.ts +33 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.js +46 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.d.ts +30 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.js +27 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.d.ts +13 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.js +44 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.d.ts +25 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.js +35 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.d.ts +43 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.js +193 -0
- package/lib/archive/meta/technologies/types.d.ts +68 -0
- package/lib/archive/meta/technologies/types.js +13 -0
- package/lib/archive/meta/types.d.ts +97 -30
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-alias-of-id.js +14 -3
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.js +14 -3
- package/lib/archive/migrate-info-main-content-selector.d.ts +6 -1
- package/lib/archive/migrate-info-main-content-selector.js +14 -3
- package/lib/archive/migrate-info-roots.d.ts +6 -1
- package/lib/archive/migrate-info-roots.js +14 -3
- package/lib/archive/migrate-inventory-runs-exclude-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-exclude-skipped.js +14 -3
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +14 -3
- package/lib/archive/migrate-main-contents-columns.d.ts +6 -1
- package/lib/archive/migrate-main-contents-columns.js +14 -3
- package/lib/archive/migrate-page-meta-body-hash.d.ts +6 -1
- package/lib/archive/migrate-page-meta-body-hash.js +14 -3
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +6 -1
- package/lib/archive/migrate-page-meta-console-error-count.js +14 -3
- package/lib/archive/migrate-page-meta-custom-element-count.d.ts +33 -0
- package/lib/archive/migrate-page-meta-custom-element-count.js +51 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.d.ts +39 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.js +79 -0
- package/lib/archive/page.d.ts +37 -13
- package/lib/archive/page.js +45 -17
- package/lib/archive/retarget-legacy-fk-tables.d.ts +27 -14
- package/lib/archive/retarget-legacy-fk-tables.js +61 -15
- package/lib/archive/types.d.ts +18 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.d.ts +24 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.js +36 -0
- package/lib/crawler/capture-custom-elements.d.ts +33 -0
- package/lib/crawler/capture-custom-elements.js +39 -0
- package/lib/crawler/close-browser-safely.d.ts +3 -3
- package/lib/crawler/close-browser-safely.js +8 -17
- package/lib/crawler/collect-custom-elements.d.ts +33 -0
- package/lib/crawler/collect-custom-elements.js +110 -0
- package/lib/crawler/crawler.js +101 -46
- package/lib/crawler/fetch-destination.js +6 -13
- package/lib/crawler/find-package-dir.d.ts +24 -0
- package/lib/crawler/find-package-dir.js +39 -0
- package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
- package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
- package/lib/crawler/types.d.ts +23 -0
- package/lib/crawler-orchestrator.d.ts +53 -9
- package/lib/crawler-orchestrator.js +223 -46
- package/lib/crawler.d.ts +11 -1
- package/lib/crawler.js +9 -0
- package/lib/inventory-setup-phases.d.ts +32 -0
- package/lib/inventory-setup-phases.js +45 -0
- package/lib/resume-setup-phases.d.ts +16 -0
- package/lib/resume-setup-phases.js +24 -0
- package/lib/retry-failed-setup-phases.d.ts +19 -0
- package/lib/retry-failed-setup-phases.js +24 -0
- package/lib/setup-recovery-phase-labels.d.ts +21 -0
- package/lib/setup-recovery-phase-labels.js +24 -0
- package/lib/types.d.ts +145 -0
- package/package.json +12 -9
- package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +0 -12
- package/lib/archive/db-ops/meta/get-tags-of-page.js +0 -28
- package/lib/archive/db-ops/pages/write/insert-tags.d.ts +0 -16
- package/lib/archive/db-ops/pages/write/insert-tags.js +0 -34
- package/lib/archive/meta/summarize-tags.d.ts +0 -16
- package/lib/archive/meta/summarize-tags.js +0 -33
|
@@ -62,9 +62,14 @@ import { listNetworkOutages } from '../../outages/list-network-outages.js';
|
|
|
62
62
|
* SELECT and UPDATE/DELETE statements are chunked to stay below SQLite's
|
|
63
63
|
* `SQLITE_LIMIT_VARIABLE_NUMBER`.
|
|
64
64
|
* @param knex - Knex query builder connected to the archive DB.
|
|
65
|
+
* @param onProgress - Called after each chunk's DELETE/UPDATE statements
|
|
66
|
+
* complete, with the pages processed so far and the total to reset (issue
|
|
67
|
+
* #294: a large `--retry-failed` can reset thousands of pages across 13
|
|
68
|
+
* tables, running for seconds to minutes with no other signal it hasn't
|
|
69
|
+
* hung). Omit for no reporting (the default; e.g. tests).
|
|
65
70
|
* @returns The URLs of the pages that were reset to pending.
|
|
66
71
|
*/
|
|
67
|
-
export async function resetFailedPages(knex) {
|
|
72
|
+
export async function resetFailedPages(knex, onProgress) {
|
|
68
73
|
const candidates = await knex('content_items')
|
|
69
74
|
.join('url_refs', 'content_items.url_id', 'url_refs.id')
|
|
70
75
|
.select('content_items.id as id', 'url_refs.url as url')
|
|
@@ -146,7 +151,8 @@ export async function resetFailedPages(knex) {
|
|
|
146
151
|
await knex('resource_ref_edges').whereIn('page_id', chunk).delete();
|
|
147
152
|
await knex('page_errors').whereIn('pageId', chunk).delete();
|
|
148
153
|
await knex('page_html_ref').whereIn('page_id', chunk).delete();
|
|
149
|
-
await knex('
|
|
154
|
+
await knex('technology_signals').whereIn('pageId', chunk).delete();
|
|
155
|
+
await knex('page_technologies').whereIn('pageId', chunk).delete();
|
|
150
156
|
await knex('page_jsonld').whereIn('pageId', chunk).delete();
|
|
151
157
|
await knex('page_main_content_headings').whereIn('pageId', chunk).delete();
|
|
152
158
|
await knex('page_main_content_images').whereIn('pageId', chunk).delete();
|
|
@@ -156,6 +162,8 @@ export async function resetFailedPages(knex) {
|
|
|
156
162
|
await knex('page_main_content_videos').whereIn('pageId', chunk).delete();
|
|
157
163
|
await knex('page_main_content_audios').whereIn('pageId', chunk).delete();
|
|
158
164
|
await knex('page_main_content_canvases').whereIn('pageId', chunk).delete();
|
|
165
|
+
await knex('page_main_content_custom_elements').whereIn('pageId', chunk).delete();
|
|
166
|
+
onProgress?.(Math.min(i + chunkSize, ids.length), ids.length);
|
|
159
167
|
}
|
|
160
168
|
dbLog('Reset %d failed pages back to pending', urls.length);
|
|
161
169
|
return urls;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MainContentCustomElementCandidate } from '../../../../crawler/types.js';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Replaces the page's `page_main_content_custom_elements` rows with the
|
|
5
|
+
* freshly captured set. Called inside `updatePage`'s transaction, gated on
|
|
6
|
+
* `page.mainContentCustomElements !== undefined` by the caller — the caller
|
|
7
|
+
* must NOT call this with a defaulted `[]` when the capture itself failed
|
|
8
|
+
* (`undefined`), since this function cannot tell "capture found nothing"
|
|
9
|
+
* apart from "capture didn't run" once it receives a plain array.
|
|
10
|
+
*
|
|
11
|
+
* Unlike `insertButtons` / `insertHeadings` etc. (whose empty-array case is
|
|
12
|
+
* genuinely ambiguous — beholder's `MainContentsData` gives no way to tell
|
|
13
|
+
* "successfully found zero" from "degraded scrape" — this function always
|
|
14
|
+
* deletes existing rows once called, including for an empty array: because
|
|
15
|
+
* the caller already resolved that ambiguity via the `!== undefined` gate,
|
|
16
|
+
* an empty array here unambiguously means "capture succeeded, found none,"
|
|
17
|
+
* so stale rows from a previous crawl must be cleared.
|
|
18
|
+
*
|
|
19
|
+
* Unlike `insertButtons`, the input is nitpicker's own
|
|
20
|
+
* `MainContentCustomElementCandidate[]` (from `capture-custom-elements.ts`),
|
|
21
|
+
* not a slice of beholder's `MainContentsData` — there is no such category
|
|
22
|
+
* in `MainContentsData`.
|
|
23
|
+
* @param pageId - The owning `content_items.id`.
|
|
24
|
+
* @param customElements - The captured custom elements, in document order.
|
|
25
|
+
* @param trx - The active transaction.
|
|
26
|
+
*/
|
|
27
|
+
export declare function insertCustomElements(pageId: number, customElements: readonly MainContentCustomElementCandidate[], trx: Knex.Transaction): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { eachSplitted } from '../../../../utils/array/each-splitted.js';
|
|
2
|
+
/**
|
|
3
|
+
* Replaces the page's `page_main_content_custom_elements` rows with the
|
|
4
|
+
* freshly captured set. Called inside `updatePage`'s transaction, gated on
|
|
5
|
+
* `page.mainContentCustomElements !== undefined` by the caller — the caller
|
|
6
|
+
* must NOT call this with a defaulted `[]` when the capture itself failed
|
|
7
|
+
* (`undefined`), since this function cannot tell "capture found nothing"
|
|
8
|
+
* apart from "capture didn't run" once it receives a plain array.
|
|
9
|
+
*
|
|
10
|
+
* Unlike `insertButtons` / `insertHeadings` etc. (whose empty-array case is
|
|
11
|
+
* genuinely ambiguous — beholder's `MainContentsData` gives no way to tell
|
|
12
|
+
* "successfully found zero" from "degraded scrape" — this function always
|
|
13
|
+
* deletes existing rows once called, including for an empty array: because
|
|
14
|
+
* the caller already resolved that ambiguity via the `!== undefined` gate,
|
|
15
|
+
* an empty array here unambiguously means "capture succeeded, found none,"
|
|
16
|
+
* so stale rows from a previous crawl must be cleared.
|
|
17
|
+
*
|
|
18
|
+
* Unlike `insertButtons`, the input is nitpicker's own
|
|
19
|
+
* `MainContentCustomElementCandidate[]` (from `capture-custom-elements.ts`),
|
|
20
|
+
* not a slice of beholder's `MainContentsData` — there is no such category
|
|
21
|
+
* in `MainContentsData`.
|
|
22
|
+
* @param pageId - The owning `content_items.id`.
|
|
23
|
+
* @param customElements - The captured custom elements, in document order.
|
|
24
|
+
* @param trx - The active transaction.
|
|
25
|
+
*/
|
|
26
|
+
export async function insertCustomElements(pageId, customElements, trx) {
|
|
27
|
+
await trx('page_main_content_custom_elements').where('pageId', pageId).delete();
|
|
28
|
+
if (customElements.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
const rows = customElements.map((el, order) => ({
|
|
31
|
+
pageId,
|
|
32
|
+
order,
|
|
33
|
+
nodeName: el.nodeName,
|
|
34
|
+
elementId: el.elementId,
|
|
35
|
+
classList: JSON.stringify(el.classList),
|
|
36
|
+
}));
|
|
37
|
+
await eachSplitted(rows, 100, async (chunk) => {
|
|
38
|
+
await trx('page_main_content_custom_elements').insert(chunk);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { PageSource } from '../../../types.js';
|
|
1
|
+
import type { PageDataWithDomPaths, PageSource } from '../../../types.js';
|
|
3
2
|
import type { WriteRefCaches } from '../../_shared/types.js';
|
|
4
3
|
import type { Knex } from 'knex';
|
|
5
4
|
/**
|
|
@@ -15,6 +14,26 @@ import type { Knex } from 'knex';
|
|
|
15
14
|
* anything reachable via the crawled chain must be labelled `'crawled'`
|
|
16
15
|
* even if previously labelled `'inventory-*'`.
|
|
17
16
|
*
|
|
17
|
+
* `is_external`, by contrast, IS overwritten on every call — but demotion is
|
|
18
|
+
* guarded. `updatePage` keys the row by the redirect DESTINATION url while
|
|
19
|
+
* passing the REQUESTING url's `isExternal`, so the value written here
|
|
20
|
+
* describes the requester, not necessarily this row. Inheriting it is
|
|
21
|
+
* deliberate when PROMOTING (an out-of-scope soft-404 page reached from an
|
|
22
|
+
* in-scope request counts as covered by the crawl, and the viewer relies on
|
|
23
|
+
* that — see `@nitpicker/query`'s `build-directory-tree-rows.ts`). It would be
|
|
24
|
+
* wrong when DEMOTING: an out-of-scope url redirecting to an in-scope page
|
|
25
|
+
* that was already taken on as a target must not flip that page to
|
|
26
|
+
* `is_external = 1` — no reading of the column justifies erasing a real
|
|
27
|
+
* observation with an inherited one. `crawler.ts`'s `#scrapedDestinations`
|
|
28
|
+
* blocks this within one run, but that is per-`#runDeal` memory, so a later
|
|
29
|
+
* `--append` / `--retry-failed` process starts blind to what the DB already
|
|
30
|
+
* knows — hence the CASE below, which checks the ROW's own prior state
|
|
31
|
+
* instead: once `scraped = 1 AND is_external = 0` is true, no later call can
|
|
32
|
+
* flip it back to `1`. Promotion (`0 → 1` before the row has been scraped, or
|
|
33
|
+
* `1 → 0` at any time) is untouched. This mirrors how `first_crawled_at`'s `COALESCE` below
|
|
34
|
+
* protects an established value — deliberately NOT by re-deriving scope from
|
|
35
|
+
* the destination url, which would also kill the wanted promoting case.
|
|
36
|
+
*
|
|
18
37
|
* The page's response headers are decomposed and written into the
|
|
19
38
|
* header dictionary tables here — per response, not deferred to
|
|
20
39
|
* crawl-end — and the resulting `header_set_id` lands on the same
|
|
@@ -26,11 +45,12 @@ import type { Knex } from 'knex';
|
|
|
26
45
|
* @param knex - Knex query builder connected to the archive DB. Used as the
|
|
27
46
|
* fallback when `trx` is not provided.
|
|
28
47
|
* @param caches - The connection's write-side id caches.
|
|
29
|
-
* @param page - The scraped page data
|
|
48
|
+
* @param page - The scraped page data, optionally carrying nitpicker's own
|
|
49
|
+
* in-browser custom-element capture (see {@link PageDataWithDomPaths}).
|
|
30
50
|
* @param isTarget - Whether this page is a crawl target.
|
|
31
51
|
* @param trx - Optional transaction all statements run through.
|
|
32
52
|
* @param source - Inventory provenance for the INSERT path. Ignored on UPDATE
|
|
33
53
|
* except through the priority lattice described above.
|
|
34
54
|
* @returns The `content_items.id` of the inserted or updated row.
|
|
35
55
|
*/
|
|
36
|
-
export declare function insertPage(knex: Knex, caches: WriteRefCaches, page:
|
|
56
|
+
export declare function insertPage(knex: Knex, caches: WriteRefCaches, page: PageDataWithDomPaths, isTarget: boolean, trx?: Knex.Transaction, source?: PageSource): Promise<number>;
|
|
@@ -24,6 +24,26 @@ import { upsertUrlRef } from '../../_shared/upsert-url-ref.js';
|
|
|
24
24
|
* anything reachable via the crawled chain must be labelled `'crawled'`
|
|
25
25
|
* even if previously labelled `'inventory-*'`.
|
|
26
26
|
*
|
|
27
|
+
* `is_external`, by contrast, IS overwritten on every call — but demotion is
|
|
28
|
+
* guarded. `updatePage` keys the row by the redirect DESTINATION url while
|
|
29
|
+
* passing the REQUESTING url's `isExternal`, so the value written here
|
|
30
|
+
* describes the requester, not necessarily this row. Inheriting it is
|
|
31
|
+
* deliberate when PROMOTING (an out-of-scope soft-404 page reached from an
|
|
32
|
+
* in-scope request counts as covered by the crawl, and the viewer relies on
|
|
33
|
+
* that — see `@nitpicker/query`'s `build-directory-tree-rows.ts`). It would be
|
|
34
|
+
* wrong when DEMOTING: an out-of-scope url redirecting to an in-scope page
|
|
35
|
+
* that was already taken on as a target must not flip that page to
|
|
36
|
+
* `is_external = 1` — no reading of the column justifies erasing a real
|
|
37
|
+
* observation with an inherited one. `crawler.ts`'s `#scrapedDestinations`
|
|
38
|
+
* blocks this within one run, but that is per-`#runDeal` memory, so a later
|
|
39
|
+
* `--append` / `--retry-failed` process starts blind to what the DB already
|
|
40
|
+
* knows — hence the CASE below, which checks the ROW's own prior state
|
|
41
|
+
* instead: once `scraped = 1 AND is_external = 0` is true, no later call can
|
|
42
|
+
* flip it back to `1`. Promotion (`0 → 1` before the row has been scraped, or
|
|
43
|
+
* `1 → 0` at any time) is untouched. This mirrors how `first_crawled_at`'s `COALESCE` below
|
|
44
|
+
* protects an established value — deliberately NOT by re-deriving scope from
|
|
45
|
+
* the destination url, which would also kill the wanted promoting case.
|
|
46
|
+
*
|
|
27
47
|
* The page's response headers are decomposed and written into the
|
|
28
48
|
* header dictionary tables here — per response, not deferred to
|
|
29
49
|
* crawl-end — and the resulting `header_set_id` lands on the same
|
|
@@ -35,7 +55,8 @@ import { upsertUrlRef } from '../../_shared/upsert-url-ref.js';
|
|
|
35
55
|
* @param knex - Knex query builder connected to the archive DB. Used as the
|
|
36
56
|
* fallback when `trx` is not provided.
|
|
37
57
|
* @param caches - The connection's write-side id caches.
|
|
38
|
-
* @param page - The scraped page data
|
|
58
|
+
* @param page - The scraped page data, optionally carrying nitpicker's own
|
|
59
|
+
* in-browser custom-element capture (see {@link PageDataWithDomPaths}).
|
|
39
60
|
* @param isTarget - Whether this page is a crawl target.
|
|
40
61
|
* @param trx - Optional transaction all statements run through.
|
|
41
62
|
* @param source - Inventory provenance for the INSERT path. Ignored on UPDATE
|
|
@@ -47,7 +68,7 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
|
|
|
47
68
|
const pageId = await resolveContentItemId(qb, caches, page.url.withoutHashAndAuth, undefined, source);
|
|
48
69
|
const flat = deriveFlatFromMeta(page.meta, page.url.href);
|
|
49
70
|
const denorm = computePageDenormalized(page.meta);
|
|
50
|
-
const mainContentsDenorm = computeMainContentsDenormalized(page.mainContents, page.scrollHeight);
|
|
71
|
+
const mainContentsDenorm = computeMainContentsDenormalized(page.mainContents, page.scrollHeight, page.mainContentCustomElements?.length);
|
|
51
72
|
const extras = deriveMetaExtras(page.meta);
|
|
52
73
|
const now = Date.now();
|
|
53
74
|
// Canonicalize so the stored dictionary value matches the exact-string
|
|
@@ -86,8 +107,23 @@ export async function insertPage(knex, caches, page, isTarget, trx, source) {
|
|
|
86
107
|
.where('id', pageId)
|
|
87
108
|
.update({
|
|
88
109
|
scraped: 1,
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
// Once a row has been scraped as a real crawl target, no later call
|
|
111
|
+
// may flip it back off — same inheritance-from-the-requester bug as
|
|
112
|
+
// `is_external` below (`setExternalPage` always passes `isTarget:
|
|
113
|
+
// false`), and the same fix: guard on the row's own prior state
|
|
114
|
+
// instead of per-run memory. Demoting is_target away from an
|
|
115
|
+
// established value would under-count `getScrapedHtmlPageCount`'s
|
|
116
|
+
// resume offset and silently break the "isTarget=1 means covered by
|
|
117
|
+
// the crawl" contract `accessor.getPages('page')` documents.
|
|
118
|
+
is_target: qb.raw('CASE WHEN scraped = 1 AND is_target = 1 THEN 1 ELSE ? END', [
|
|
119
|
+
isTarget ? 1 : 0,
|
|
120
|
+
]),
|
|
121
|
+
// Once a row has been scraped as internal, no later call may flip it
|
|
122
|
+
// back to external — see this function's docs for why the write this
|
|
123
|
+
// guards against happens at all.
|
|
124
|
+
is_external: qb.raw('CASE WHEN scraped = 1 AND is_external = 0 THEN 0 ELSE ? END', [
|
|
125
|
+
page.isExternal ? 1 : 0,
|
|
126
|
+
]),
|
|
91
127
|
status: page.status,
|
|
92
128
|
status_text: page.statusText,
|
|
93
129
|
content_type_id: contentTypeId,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PageData } from '../../../../utils/types/types.js';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Replaces the page's `technology_signals` + `page_technologies` rows with
|
|
5
|
+
* the freshly extracted set. Called inside `updatePage`'s transaction
|
|
6
|
+
* unconditionally — the `wappalyzer` signal source draws on `<script src>` /
|
|
7
|
+
* `<iframe src>` / window globals / response headers, not the HTML body, so
|
|
8
|
+
* external pages that skip rendering still contribute technology signals
|
|
9
|
+
* (structural signals naturally find nothing against an empty `html`
|
|
10
|
+
* string, which is exactly what a skipped render leaves it as).
|
|
11
|
+
*
|
|
12
|
+
* Direct replacement for `insertTags` in the live crawl write path (that
|
|
13
|
+
* file is untouched — `scripts/migrate-to-0.10.mjs` still depends on it).
|
|
14
|
+
*
|
|
15
|
+
* Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does
|
|
16
|
+
* not wipe prior rows on a degraded re-scrape. Both tables are always
|
|
17
|
+
* written together in this one call — see ARCHITECTURE.md's invariant
|
|
18
|
+
* that `technology_signals` and `page_technologies` are never updated
|
|
19
|
+
* independently.
|
|
20
|
+
* @param pageId
|
|
21
|
+
* @param html - The page's raw HTML string (empty for external/metadata-only scrapes).
|
|
22
|
+
* @param meta
|
|
23
|
+
* @param trx
|
|
24
|
+
*/
|
|
25
|
+
export declare function insertTechnologies(pageId: number, html: string, meta: PageData['meta'], trx: Knex.Transaction): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { eachSplitted } from '../../../../utils/array/each-splitted.js';
|
|
2
|
+
import { extractTechnologiesForArchive } from '../../../meta/technologies/extract-technologies-for-archive.js';
|
|
3
|
+
/**
|
|
4
|
+
* Replaces the page's `technology_signals` + `page_technologies` rows with
|
|
5
|
+
* the freshly extracted set. Called inside `updatePage`'s transaction
|
|
6
|
+
* unconditionally — the `wappalyzer` signal source draws on `<script src>` /
|
|
7
|
+
* `<iframe src>` / window globals / response headers, not the HTML body, so
|
|
8
|
+
* external pages that skip rendering still contribute technology signals
|
|
9
|
+
* (structural signals naturally find nothing against an empty `html`
|
|
10
|
+
* string, which is exactly what a skipped render leaves it as).
|
|
11
|
+
*
|
|
12
|
+
* Direct replacement for `insertTags` in the live crawl write path (that
|
|
13
|
+
* file is untouched — `scripts/migrate-to-0.10.mjs` still depends on it).
|
|
14
|
+
*
|
|
15
|
+
* Same empty-guard as `insertTags` / `insertJsonLd`: an empty array does
|
|
16
|
+
* not wipe prior rows on a degraded re-scrape. Both tables are always
|
|
17
|
+
* written together in this one call — see ARCHITECTURE.md's invariant
|
|
18
|
+
* that `technology_signals` and `page_technologies` are never updated
|
|
19
|
+
* independently.
|
|
20
|
+
* @param pageId
|
|
21
|
+
* @param html - The page's raw HTML string (empty for external/metadata-only scrapes).
|
|
22
|
+
* @param meta
|
|
23
|
+
* @param trx
|
|
24
|
+
*/
|
|
25
|
+
export async function insertTechnologies(pageId, html, meta, trx) {
|
|
26
|
+
const { signals, technologies } = extractTechnologiesForArchive(html, meta);
|
|
27
|
+
if (signals.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
await trx('technology_signals').where('pageId', pageId).delete();
|
|
30
|
+
await trx('page_technologies').where('pageId', pageId).delete();
|
|
31
|
+
await eachSplitted(signals, 100, async (chunk) => {
|
|
32
|
+
await trx('technology_signals').insert(chunk.map((s) => ({
|
|
33
|
+
pageId,
|
|
34
|
+
technology: s.technology,
|
|
35
|
+
signalType: s.signalType,
|
|
36
|
+
evidence: s.evidence,
|
|
37
|
+
weight: s.weight,
|
|
38
|
+
})));
|
|
39
|
+
});
|
|
40
|
+
if (technologies.length > 0) {
|
|
41
|
+
await eachSplitted(technologies, 100, async (chunk) => {
|
|
42
|
+
await trx('page_technologies').insert(chunk.map((t) => ({ pageId, ...t })));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -20,7 +20,13 @@ import type { Knex } from 'knex';
|
|
|
20
20
|
* @param destId - `content_items.id` of the redirect destination page.
|
|
21
21
|
* @param destUrlNormalized - Normalised destination URL, used to detect and
|
|
22
22
|
* skip self-redirects.
|
|
23
|
-
* @param isExternal - Whether the sources are external to the crawl scope
|
|
23
|
+
* @param isExternal - Whether the sources are external to the crawl scope, as
|
|
24
|
+
* a single value applied to every hop in `sources` — the same
|
|
25
|
+
* requester-describes-a-different-row shape `insertPage` guards against
|
|
26
|
+
* (see its docs in `insert-page.ts`), since a hop can independently be a
|
|
27
|
+
* real, already-scraped internal page in its own right (reached earlier as
|
|
28
|
+
* a direct crawl target) before it is later observed as an intermediate hop
|
|
29
|
+
* in some other chain. The UPDATE below carries the same CASE guard.
|
|
24
30
|
* @param chainLineageSource - Lineage label propagated to each intermediate
|
|
25
31
|
* hop's row (passed through to {@link resolveContentItemId}). Derived by
|
|
26
32
|
* the caller from the **originating** page's source (`page.url`), not from
|
|
@@ -19,7 +19,13 @@ import { resolveContentItemId } from '../../_shared/resolve-content-item-id.js';
|
|
|
19
19
|
* @param destId - `content_items.id` of the redirect destination page.
|
|
20
20
|
* @param destUrlNormalized - Normalised destination URL, used to detect and
|
|
21
21
|
* skip self-redirects.
|
|
22
|
-
* @param isExternal - Whether the sources are external to the crawl scope
|
|
22
|
+
* @param isExternal - Whether the sources are external to the crawl scope, as
|
|
23
|
+
* a single value applied to every hop in `sources` — the same
|
|
24
|
+
* requester-describes-a-different-row shape `insertPage` guards against
|
|
25
|
+
* (see its docs in `insert-page.ts`), since a hop can independently be a
|
|
26
|
+
* real, already-scraped internal page in its own right (reached earlier as
|
|
27
|
+
* a direct crawl target) before it is later observed as an intermediate hop
|
|
28
|
+
* in some other chain. The UPDATE below carries the same CASE guard.
|
|
23
29
|
* @param chainLineageSource - Lineage label propagated to each intermediate
|
|
24
30
|
* hop's row (passed through to {@link resolveContentItemId}). Derived by
|
|
25
31
|
* the caller from the **originating** page's source (`page.url`), not from
|
|
@@ -53,7 +59,7 @@ export async function linkRedirectSources(trx, caches, sources, destId, destUrlN
|
|
|
53
59
|
.update({
|
|
54
60
|
scraped: 1,
|
|
55
61
|
redirect_dest_id: destId,
|
|
56
|
-
is_external: isExternal ? 1 : 0,
|
|
62
|
+
is_external: trx.raw('CASE WHEN scraped = 1 AND is_external = 0 THEN 0 ELSE ? END', [isExternal ? 1 : 0]),
|
|
57
63
|
});
|
|
58
64
|
// Conditional `301 Moved Permanently` stamp — applied ONLY when the
|
|
59
65
|
// row carries no definitive status yet (NULL or the `-1`
|
|
@@ -24,6 +24,9 @@ import type { Knex } from 'knex';
|
|
|
24
24
|
* inserted. Existing rows keep their original `source` (this is why a
|
|
25
25
|
* second `crawl --inventory` does not "demote" an `'inventory-seed'` row
|
|
26
26
|
* that was discovered earlier).
|
|
27
|
+
* @param bodyHash - Precomputed body hash for the page's HTML (see
|
|
28
|
+
* `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
|
|
29
|
+
* computing it from the HTML instead.
|
|
27
30
|
* @returns The database `pageId` (`content_items.id`) of the inserted or updated row.
|
|
28
31
|
*/
|
|
29
|
-
export declare function updatePage(knex: Knex, caches: WriteRefCaches, page: PageDataWithDomPaths, writeHtml: boolean, isTarget: boolean, source?: PageSource): Promise<number>;
|
|
32
|
+
export declare function updatePage(knex: Knex, caches: WriteRefCaches, page: PageDataWithDomPaths, writeHtml: boolean, isTarget: boolean, source?: PageSource, bodyHash?: Buffer | null): Promise<number>;
|
|
@@ -13,13 +13,14 @@ import { resolveUrlOrBlob } from '../../_shared/resolve-url-or-blob.js';
|
|
|
13
13
|
import { insertAudios } from './insert-audios.js';
|
|
14
14
|
import { insertButtons } from './insert-buttons.js';
|
|
15
15
|
import { insertCanvases } from './insert-canvases.js';
|
|
16
|
+
import { insertCustomElements } from './insert-custom-elements.js';
|
|
16
17
|
import { insertHeadings } from './insert-headings.js';
|
|
17
18
|
import { insertIframes } from './insert-iframes.js';
|
|
18
19
|
import { insertJsonLd } from './insert-jsonld.js';
|
|
19
20
|
import { insertMainContentImages } from './insert-main-content-images.js';
|
|
20
21
|
import { insertMainContentTables } from './insert-main-content-tables.js';
|
|
21
22
|
import { insertPage } from './insert-page.js';
|
|
22
|
-
import {
|
|
23
|
+
import { insertTechnologies } from './insert-technologies.js';
|
|
23
24
|
import { insertVideos } from './insert-videos.js';
|
|
24
25
|
import { linkRedirectSources } from './link-redirect-sources.js';
|
|
25
26
|
import { writePageHtmlBlob } from './write-page-html-blob.js';
|
|
@@ -46,9 +47,12 @@ import { writePageHtmlBlob } from './write-page-html-blob.js';
|
|
|
46
47
|
* inserted. Existing rows keep their original `source` (this is why a
|
|
47
48
|
* second `crawl --inventory` does not "demote" an `'inventory-seed'` row
|
|
48
49
|
* that was discovered earlier).
|
|
50
|
+
* @param bodyHash - Precomputed body hash for the page's HTML (see
|
|
51
|
+
* `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
|
|
52
|
+
* computing it from the HTML instead.
|
|
49
53
|
* @returns The database `pageId` (`content_items.id`) of the inserted or updated row.
|
|
50
54
|
*/
|
|
51
|
-
export async function updatePage(knex, caches, page, writeHtml, isTarget, source) {
|
|
55
|
+
export async function updatePage(knex, caches, page, writeHtml, isTarget, source, bodyHash) {
|
|
52
56
|
const { destUrl, sources } = resolveRedirectChain(page.url.withoutHashAndAuth, page.redirectPaths);
|
|
53
57
|
const destUrlObject = parseUrl(destUrl);
|
|
54
58
|
if (!destUrlObject) {
|
|
@@ -56,7 +60,7 @@ export async function updatePage(knex, caches, page, writeHtml, isTarget, source
|
|
|
56
60
|
}
|
|
57
61
|
try {
|
|
58
62
|
return await knex.transaction(async (trx) => {
|
|
59
|
-
return await updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source);
|
|
63
|
+
return await updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source, bodyHash);
|
|
60
64
|
});
|
|
61
65
|
}
|
|
62
66
|
catch (error) {
|
|
@@ -83,21 +87,24 @@ export async function updatePage(knex, caches, page, writeHtml, isTarget, source
|
|
|
83
87
|
* @param writeHtml - See {@link updatePage}.
|
|
84
88
|
* @param isTarget - See {@link updatePage}.
|
|
85
89
|
* @param source - See {@link updatePage}.
|
|
90
|
+
* @param bodyHash - See {@link updatePage}.
|
|
86
91
|
* @returns The `content_items.id` of the inserted or updated row.
|
|
87
92
|
*/
|
|
88
|
-
async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source) {
|
|
93
|
+
async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, sources, writeHtml, isTarget, source, bodyHash) {
|
|
89
94
|
const pageId = await insertPage(knex, caches, {
|
|
90
95
|
...page,
|
|
91
96
|
url: destUrlObject,
|
|
92
97
|
}, isTarget, trx, source);
|
|
93
|
-
// Wappalyzer
|
|
94
|
-
// `<script src>` / `<iframe src>` / window
|
|
95
|
-
// headers
|
|
96
|
-
// metadata-only
|
|
97
|
-
//
|
|
98
|
-
// scrape
|
|
99
|
-
// below
|
|
100
|
-
|
|
98
|
+
// Technology detection combines a Wappalyzer signal source (HTML-body
|
|
99
|
+
// independent — relies on `<script src>` / `<iframe src>` / window
|
|
100
|
+
// globals / response headers, so it fires for every page including
|
|
101
|
+
// external / metadata-only) with structural signals that scan
|
|
102
|
+
// `page.html` (naturally a no-op against the empty string an
|
|
103
|
+
// external/metadata-only scrape leaves it as). Both run unconditionally,
|
|
104
|
+
// unlike JSON-LD below, which lives inside the rendered HTML body and
|
|
105
|
+
// is only written when there is HTML to scrape — see the same
|
|
106
|
+
// `writeHtml` gate as `writePageHtmlBlob` below.
|
|
107
|
+
await insertTechnologies(pageId, page.html, page.meta, trx);
|
|
101
108
|
if (writeHtml) {
|
|
102
109
|
await insertJsonLd(pageId, page.meta, trx);
|
|
103
110
|
}
|
|
@@ -113,6 +120,16 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
|
|
|
113
120
|
await insertVideos(pageId, page.mainContents, trx);
|
|
114
121
|
await insertAudios(pageId, page.mainContents, trx);
|
|
115
122
|
await insertCanvases(pageId, page.mainContents, trx);
|
|
123
|
+
// Unlike the eight calls above, the source here is nitpicker's own
|
|
124
|
+
// `capture-custom-elements.ts` capture, not `page.mainContents` — see
|
|
125
|
+
// `insert-custom-elements.ts`. That capture can fail independently
|
|
126
|
+
// of `page.mainContents` (a separate `page.evaluate()` call), so
|
|
127
|
+
// `undefined` (capture failed) must skip the call entirely — passing
|
|
128
|
+
// a defaulted `[]` would look identical to "found zero" and, per
|
|
129
|
+
// `insertCustomElements`'s docs, wipe stale-but-still-good rows.
|
|
130
|
+
if (page.mainContentCustomElements !== undefined) {
|
|
131
|
+
await insertCustomElements(pageId, page.mainContentCustomElements, trx);
|
|
132
|
+
}
|
|
116
133
|
}
|
|
117
134
|
// Chain lineage propagates FROM the originating URL
|
|
118
135
|
// (`page.url`), NOT from the destination. See the matching
|
|
@@ -142,12 +159,17 @@ async function updatePageInTransaction(trx, knex, caches, page, destUrlObject, s
|
|
|
142
159
|
// content check alone expresses the intent without a redundant term.
|
|
143
160
|
if (writeHtml && page.html.length > 0) {
|
|
144
161
|
await writePageHtmlBlob(pageId, page.html, trx);
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
162
|
+
// Prefer the caller's precomputed hash (the crawler already hashed
|
|
163
|
+
// this exact `page.html` once, before this write path ever runs) —
|
|
164
|
+
// falling back to computing it here only when the caller has not
|
|
165
|
+
// (direct callers such as tests, or a future write path that has not
|
|
166
|
+
// adopted the precompute). Either way this stays derived from the
|
|
167
|
+
// same `page.html` written above, in the same transaction, so
|
|
168
|
+
// `page_meta.body_hash` never observably lags the snapshot it was
|
|
169
|
+
// derived from.
|
|
148
170
|
await trx('page_meta')
|
|
149
171
|
.where('page_id', pageId)
|
|
150
|
-
.update({ body_hash: computeBodyHash(page.html) });
|
|
172
|
+
.update({ body_hash: bodyHash ?? computeBodyHash(page.html) });
|
|
151
173
|
}
|
|
152
174
|
else if (writeHtml &&
|
|
153
175
|
page.contentType !== null &&
|
|
@@ -3,7 +3,18 @@ import type { Knex } from 'knex';
|
|
|
3
3
|
* Retrieves a flat list of all resource URLs from the `resource_items`
|
|
4
4
|
* table. URL text is normalised into `url_refs`, so the read joins the
|
|
5
5
|
* two tables and returns the resolved strings.
|
|
6
|
+
*
|
|
7
|
+
* Read in `resource_items.id`-keyset chunks rather than a single SELECT
|
|
8
|
+
* (issue #294): on a resource-heavy archive (images/JS/CSS in the tens or
|
|
9
|
+
* hundreds of thousands) this was one unbounded, multi-second-to-minutes
|
|
10
|
+
* query with no way to report progress mid-scan. The accumulated result is
|
|
11
|
+
* identical to the previous single-SELECT read — chunking exists purely to
|
|
12
|
+
* make the scan observable, not to bound memory (the flat URL list is
|
|
13
|
+
* already fully materialised for the caller either way).
|
|
6
14
|
* @param knex - Knex query builder connected to the archive DB.
|
|
15
|
+
* @param onProgress - Called after each chunk with the `resource_items.id`
|
|
16
|
+
* scanned up to so far and the max id. Omit for no reporting (the
|
|
17
|
+
* default; e.g. tests).
|
|
7
18
|
* @returns An array of resource URL strings.
|
|
8
19
|
*/
|
|
9
|
-
export declare function getResourceUrlList(knex: Knex): Promise<string[]>;
|
|
20
|
+
export declare function getResourceUrlList(knex: Knex, onProgress?: (scannedUpToId: number, maxId: number) => void): Promise<string[]>;
|
|
@@ -1,13 +1,51 @@
|
|
|
1
|
+
/** Rows read per `resource_items.id` keyset chunk. */
|
|
2
|
+
const READ_CHUNK_SIZE = 2000;
|
|
1
3
|
/**
|
|
2
4
|
* Retrieves a flat list of all resource URLs from the `resource_items`
|
|
3
5
|
* table. URL text is normalised into `url_refs`, so the read joins the
|
|
4
6
|
* two tables and returns the resolved strings.
|
|
7
|
+
*
|
|
8
|
+
* Read in `resource_items.id`-keyset chunks rather than a single SELECT
|
|
9
|
+
* (issue #294): on a resource-heavy archive (images/JS/CSS in the tens or
|
|
10
|
+
* hundreds of thousands) this was one unbounded, multi-second-to-minutes
|
|
11
|
+
* query with no way to report progress mid-scan. The accumulated result is
|
|
12
|
+
* identical to the previous single-SELECT read — chunking exists purely to
|
|
13
|
+
* make the scan observable, not to bound memory (the flat URL list is
|
|
14
|
+
* already fully materialised for the caller either way).
|
|
5
15
|
* @param knex - Knex query builder connected to the archive DB.
|
|
16
|
+
* @param onProgress - Called after each chunk with the `resource_items.id`
|
|
17
|
+
* scanned up to so far and the max id. Omit for no reporting (the
|
|
18
|
+
* default; e.g. tests).
|
|
6
19
|
* @returns An array of resource URL strings.
|
|
7
20
|
*/
|
|
8
|
-
export async function getResourceUrlList(knex) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
export async function getResourceUrlList(knex, onProgress) {
|
|
22
|
+
// MAX() over the keyset column is an O(1) index-tail read; only fetched
|
|
23
|
+
// when someone is listening.
|
|
24
|
+
let maxId = 0;
|
|
25
|
+
if (onProgress) {
|
|
26
|
+
const [maxRow] = await knex('resource_items').max({
|
|
27
|
+
max: 'id',
|
|
28
|
+
});
|
|
29
|
+
maxId = maxRow?.max ?? 0;
|
|
30
|
+
}
|
|
31
|
+
const urls = [];
|
|
32
|
+
let lastId = 0;
|
|
33
|
+
for (;;) {
|
|
34
|
+
const rows = (await knex('resource_items')
|
|
35
|
+
.join('url_refs', 'url_refs.id', 'resource_items.url_id')
|
|
36
|
+
.where('resource_items.id', '>', lastId)
|
|
37
|
+
.orderBy('resource_items.id', 'asc')
|
|
38
|
+
.limit(READ_CHUNK_SIZE)
|
|
39
|
+
.select('resource_items.id as id', 'url_refs.url as url'));
|
|
40
|
+
if (rows.length === 0) {
|
|
41
|
+
onProgress?.(maxId, maxId);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
lastId = rows.at(-1).id;
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
urls.push(row.url);
|
|
47
|
+
}
|
|
48
|
+
onProgress?.(Math.min(lastId, maxId), maxId);
|
|
49
|
+
}
|
|
50
|
+
return urls;
|
|
13
51
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copies a file while reporting byte progress — `fs.copyFile` offers no
|
|
3
|
+
* observability, and a `.nitpicker` archive's `.bak` safety copy (taken
|
|
4
|
+
* before every mutating `viewer-build`/`crawl --append`/`--inventory`/
|
|
5
|
+
* `--retry-failed` run, and copied back on failure) can be 15 GB+, running
|
|
6
|
+
* for tens of seconds with nothing on screen (issue #294). Byte granularity
|
|
7
|
+
* is the read-stream chunk size (~64 KB); callers wanting coarser updates
|
|
8
|
+
* throttle in their own callback.
|
|
9
|
+
* @param src - The file to copy.
|
|
10
|
+
* @param dest - The destination path, overwritten if present.
|
|
11
|
+
* @param onProgress - Called as bytes are copied, with the bytes copied so
|
|
12
|
+
* far and the source file's total size. Omit for a silent copy.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* await copyFileWithProgress(archivePath, `${archivePath}.bak`, (copied, total) => {
|
|
16
|
+
* console.error(`${copied}/${total}`);
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function copyFileWithProgress(src: string, dest: string, onProgress?: (copiedBytes: number, totalBytes: number) => void): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
/**
|
|
5
|
+
* Copies a file while reporting byte progress — `fs.copyFile` offers no
|
|
6
|
+
* observability, and a `.nitpicker` archive's `.bak` safety copy (taken
|
|
7
|
+
* before every mutating `viewer-build`/`crawl --append`/`--inventory`/
|
|
8
|
+
* `--retry-failed` run, and copied back on failure) can be 15 GB+, running
|
|
9
|
+
* for tens of seconds with nothing on screen (issue #294). Byte granularity
|
|
10
|
+
* is the read-stream chunk size (~64 KB); callers wanting coarser updates
|
|
11
|
+
* throttle in their own callback.
|
|
12
|
+
* @param src - The file to copy.
|
|
13
|
+
* @param dest - The destination path, overwritten if present.
|
|
14
|
+
* @param onProgress - Called as bytes are copied, with the bytes copied so
|
|
15
|
+
* far and the source file's total size. Omit for a silent copy.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* await copyFileWithProgress(archivePath, `${archivePath}.bak`, (copied, total) => {
|
|
19
|
+
* console.error(`${copied}/${total}`);
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export async function copyFileWithProgress(src, dest, onProgress) {
|
|
24
|
+
const { size: totalBytes } = await stat(src);
|
|
25
|
+
const source = createReadStream(src);
|
|
26
|
+
let copiedBytes = 0;
|
|
27
|
+
if (onProgress) {
|
|
28
|
+
source.on('data', (chunk) => {
|
|
29
|
+
copiedBytes += chunk.length;
|
|
30
|
+
onProgress(copiedBytes, totalBytes);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
await pipeline(source, createWriteStream(dest));
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a PAX extended header record block for the `path` key.
|
|
3
|
+
*
|
|
4
|
+
* Format: a sequence of `"<record-length> <key>=<value>\n"` records, where
|
|
5
|
+
* `<record-length>` is the decimal byte length of the ENTIRE record
|
|
6
|
+
* (including the length prefix itself and the trailing newline). `tar`
|
|
7
|
+
* (node-tar) emits one of these ahead of any entry whose name doesn't fit
|
|
8
|
+
* the ustar format's 100-byte field (UTF-8 multi-byte names, or names
|
|
9
|
+
* longer than 100 bytes) — verified empirically against this package's own
|
|
10
|
+
* `tar()`/`create()` output.
|
|
11
|
+
* @param data - The PAX header entry's raw data payload.
|
|
12
|
+
* @returns The `path` value if present, otherwise `null`.
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* parsePaxPath(Buffer.from('20 path=日本語/\n', 'utf8')); // '日本語/'
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function parsePaxPath(data: Buffer): string | null;
|