@govtechsg/oobee 0.10.97 → 0.11.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/AGENTS.md +33 -0
- package/CRAWL.md +260 -0
- package/README.md +4 -0
- package/dist/combine.js +9 -1
- package/dist/constants/common.js +7 -0
- package/dist/constants/constants.js +17 -0
- package/dist/crawlers/commonCrawlerFunc.js +89 -36
- package/dist/crawlers/crawlDomain.js +132 -61
- package/dist/crawlers/crawlIntelligentSitemap.js +3 -1
- package/dist/crawlers/crawlLocalFile.js +1 -0
- package/dist/crawlers/crawlRateController.js +1 -4
- package/dist/crawlers/crawlSitemap.js +134 -21
- package/dist/crawlers/custom/flagUnlabelledClickableElements.js +5 -1
- package/dist/mergeAxeResults.js +2 -1
- package/dist/utils.js +36 -9
- package/oobee-client-scanner.js +7 -3
- package/package.json +1 -1
- package/src/combine.ts +9 -0
- package/src/constants/common.ts +9 -0
- package/src/constants/constants.ts +17 -0
- package/src/crawlers/commonCrawlerFunc.ts +105 -42
- package/src/crawlers/crawlDomain.ts +135 -64
- package/src/crawlers/crawlIntelligentSitemap.ts +4 -1
- package/src/crawlers/crawlLocalFile.ts +1 -0
- package/src/crawlers/crawlRateController.ts +1 -5
- package/src/crawlers/crawlSitemap.ts +140 -21
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +7 -2
- package/src/mergeAxeResults.ts +2 -1
- package/src/utils.ts +31 -8
- /package/{4b15c550-be4f-428d-b0d0-933dafd76de3.txt → f0ce5c33-4dd0-4e14-8e67-1394f57ff517.txt} +0 -0
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import { chromium, Page } from 'playwright';
|
|
3
3
|
import { EnqueueStrategy } from 'crawlee';
|
|
4
4
|
import { createCrawleeSubFolders, splitAuthHeaders, addAuthRouteHandler } from './commonCrawlerFunc.js';
|
|
5
|
-
import constants, { FileTypes, guiInfoStatusTypes, sitemapPaths } from '../constants/constants.js';
|
|
5
|
+
import constants, { FileTypes, guiInfoStatusTypes, RuleFlags, sitemapPaths } from '../constants/constants.js';
|
|
6
6
|
import { consoleLogger, guiInfoLog } from '../logs.js';
|
|
7
7
|
import crawlDomain from './crawlDomain.js';
|
|
8
8
|
import crawlSitemap from './crawlSitemap.js';
|
|
@@ -27,6 +27,7 @@ const crawlIntelligentSitemap = async (
|
|
|
27
27
|
extraHTTPHeaders: Record<string, string>,
|
|
28
28
|
safeMode: boolean,
|
|
29
29
|
scanDuration: number,
|
|
30
|
+
ruleset: RuleFlags[] = [],
|
|
30
31
|
) => {
|
|
31
32
|
const startTime = Date.now(); // Track start time
|
|
32
33
|
|
|
@@ -191,6 +192,7 @@ const crawlIntelligentSitemap = async (
|
|
|
191
192
|
urlsCrawledFromIntelligent: urlsCrawled,
|
|
192
193
|
crawledFromLocalFile: false,
|
|
193
194
|
scanDuration: scanDuration > 0 ? remainingDuration : 0,
|
|
195
|
+
ruleset,
|
|
194
196
|
});
|
|
195
197
|
}
|
|
196
198
|
|
|
@@ -222,6 +224,7 @@ const crawlIntelligentSitemap = async (
|
|
|
222
224
|
datasetFromIntelligent: dataset,
|
|
223
225
|
urlsCrawledFromIntelligent: urlsCrawled,
|
|
224
226
|
scanDuration: remainingScanDuration,
|
|
227
|
+
ruleset,
|
|
225
228
|
});
|
|
226
229
|
} else if (!hasDurationRemaining) {
|
|
227
230
|
consoleLogger.info(
|
|
@@ -36,14 +36,10 @@ export class CrawlRateController {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
onFailure(httpStatus: number | undefined, pool?: { maxConcurrency: number }): boolean {
|
|
39
|
-
if (typeof httpStatus !== 'number' || httpStatus < 400) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
39
|
this.consecutiveSuccesses = 0;
|
|
44
40
|
this.consecutiveFailures++;
|
|
45
41
|
|
|
46
|
-
if (pool && pool.maxConcurrency > 1) {
|
|
42
|
+
if (typeof httpStatus === 'number' && httpStatus >= 400 && pool && pool.maxConcurrency > 1) {
|
|
47
43
|
pool.maxConcurrency = Math.max(1, Math.floor(pool.maxConcurrency / 2));
|
|
48
44
|
consoleLogger.info(
|
|
49
45
|
`Rate limited (HTTP ${httpStatus}) — reducing concurrency to ${pool.maxConcurrency}`,
|
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import {
|
|
5
5
|
createCrawleeSubFolders,
|
|
6
6
|
getPreLaunchHook,
|
|
7
|
+
getPostPageCloseHook,
|
|
7
8
|
preNavigationHooks,
|
|
8
9
|
runAxeScript,
|
|
9
10
|
isUrlPdf,
|
|
@@ -14,12 +15,16 @@ import constants, {
|
|
|
14
15
|
STATUS_CODE_METADATA,
|
|
15
16
|
guiInfoStatusTypes,
|
|
16
17
|
UrlsCrawled,
|
|
18
|
+
blackListedFileExtensions,
|
|
17
19
|
disallowedListOfPatterns,
|
|
20
|
+
disallowedSelectorPatterns,
|
|
18
21
|
FileTypes,
|
|
22
|
+
RuleFlags,
|
|
19
23
|
} from '../constants/constants.js';
|
|
20
24
|
import {
|
|
21
25
|
getLinksFromSitemap,
|
|
22
26
|
getPlaywrightLaunchOptions,
|
|
27
|
+
isDisallowedInRobotsTxt,
|
|
23
28
|
isSkippedUrl,
|
|
24
29
|
waitForPageLoaded,
|
|
25
30
|
isFilePath,
|
|
@@ -55,6 +60,7 @@ const crawlSitemap = async ({
|
|
|
55
60
|
datasetFromIntelligent = null,
|
|
56
61
|
urlsCrawledFromIntelligent = null,
|
|
57
62
|
crawledFromLocalFile = false,
|
|
63
|
+
ruleset = [],
|
|
58
64
|
}: {
|
|
59
65
|
sitemapUrl: string;
|
|
60
66
|
randomToken: string;
|
|
@@ -76,14 +82,18 @@ const crawlSitemap = async ({
|
|
|
76
82
|
datasetFromIntelligent?: Dataset;
|
|
77
83
|
urlsCrawledFromIntelligent?: UrlsCrawled;
|
|
78
84
|
crawledFromLocalFile?: boolean;
|
|
85
|
+
ruleset?: RuleFlags[];
|
|
79
86
|
}) => {
|
|
80
87
|
const crawlStartTime = Date.now();
|
|
81
88
|
let dataset: crawlee.Dataset;
|
|
82
89
|
let urlsCrawled: UrlsCrawled;
|
|
83
90
|
let durationExceeded = false;
|
|
84
91
|
let isAbortingScan = false;
|
|
92
|
+
const remainingBudget = fromCrawlIntelligentSitemap
|
|
93
|
+
? Math.max(0, maxRequestsPerCrawl - urlsCrawledFromIntelligent.scanned.length)
|
|
94
|
+
: maxRequestsPerCrawl;
|
|
85
95
|
const rateController = new CrawlRateController(
|
|
86
|
-
|
|
96
|
+
remainingBudget,
|
|
87
97
|
specifiedMaxConcurrency || constants.maxConcurrency,
|
|
88
98
|
);
|
|
89
99
|
const initialNoSuccessFailureAbortThreshold = Math.max(5, Math.min(maxRequestsPerCrawl, 25));
|
|
@@ -127,6 +137,12 @@ const crawlSitemap = async ({
|
|
|
127
137
|
sources: linksFromSitemap,
|
|
128
138
|
});
|
|
129
139
|
|
|
140
|
+
// Always create a request queue alongside the request list. An empty queue
|
|
141
|
+
// has zero impact on crawl behavior (Crawlee processes RequestList first).
|
|
142
|
+
// Having it available enables: download re-enqueue for PDF scanning,
|
|
143
|
+
// 403 rate-limit retry, and enqueueLinks for intelligent sitemap discovery.
|
|
144
|
+
const { requestQueue } = await createCrawleeSubFolders(randomToken);
|
|
145
|
+
|
|
130
146
|
const crawler = register(
|
|
131
147
|
new crawlee.PlaywrightCrawler({
|
|
132
148
|
launchContext: {
|
|
@@ -136,6 +152,8 @@ const crawlSitemap = async ({
|
|
|
136
152
|
retryOnBlocked: true,
|
|
137
153
|
browserPoolOptions: {
|
|
138
154
|
useFingerprints: false,
|
|
155
|
+
retireBrowserAfterPageCount: 500,
|
|
156
|
+
closeInactiveBrowserAfterSecs: 30,
|
|
139
157
|
preLaunchHooks: [
|
|
140
158
|
getPreLaunchHook(userDataDirectory),
|
|
141
159
|
async (_pageId, launchContext) => {
|
|
@@ -150,8 +168,10 @@ const crawlSitemap = async ({
|
|
|
150
168
|
};
|
|
151
169
|
},
|
|
152
170
|
],
|
|
171
|
+
postPageCloseHooks: [getPostPageCloseHook(userDataDirectory)],
|
|
153
172
|
},
|
|
154
173
|
requestList,
|
|
174
|
+
requestQueue,
|
|
155
175
|
maxRequestRetries: 3,
|
|
156
176
|
maxSessionRotations: 1,
|
|
157
177
|
postNavigationHooks: [
|
|
@@ -191,6 +211,8 @@ const crawlSitemap = async ({
|
|
|
191
211
|
const root = document.documentElement || document.body || document;
|
|
192
212
|
if (!root || typeof observer.observe !== 'function') {
|
|
193
213
|
resolve('No root node to observe.');
|
|
214
|
+
} else {
|
|
215
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
194
216
|
}
|
|
195
217
|
});
|
|
196
218
|
});
|
|
@@ -215,16 +237,22 @@ const crawlSitemap = async ({
|
|
|
215
237
|
if (isNotSupportedDocument) {
|
|
216
238
|
request.skipNavigation = true;
|
|
217
239
|
request.userData.isNotSupportedDocument = true;
|
|
218
|
-
|
|
219
|
-
// Log for verification (optional, but not required for correctness)
|
|
220
|
-
// console.log(`[SKIP] Not supported: ${request.url}`);
|
|
221
|
-
|
|
222
240
|
return;
|
|
223
241
|
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const pathname = new URL(url).pathname;
|
|
245
|
+
const ext = pathname.split('.').pop();
|
|
246
|
+
if (ext && blackListedFileExtensions.includes(ext)) {
|
|
247
|
+
request.skipNavigation = true;
|
|
248
|
+
request.userData.isNotSupportedDocument = true;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
224
252
|
},
|
|
225
253
|
],
|
|
226
254
|
requestHandlerTimeoutSecs: 90,
|
|
227
|
-
requestHandler: async ({ page, request, response, sendRequest }) => {
|
|
255
|
+
requestHandler: async ({ page, request, response, sendRequest, enqueueLinks }) => {
|
|
228
256
|
// Log documents that are not supported
|
|
229
257
|
if (request.userData?.isNotSupportedDocument) {
|
|
230
258
|
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
@@ -334,7 +362,7 @@ const crawlSitemap = async ({
|
|
|
334
362
|
return;
|
|
335
363
|
}
|
|
336
364
|
|
|
337
|
-
const results = await runAxeScript({ includeScreenshots, page, randomToken });
|
|
365
|
+
const results = await runAxeScript({ includeScreenshots, page, randomToken, ruleset });
|
|
338
366
|
|
|
339
367
|
// Detect JS redirects that fire during/after axe scan.
|
|
340
368
|
// Listen for navigation, then give a brief window for pending redirects to complete.
|
|
@@ -391,6 +419,32 @@ const crawlSitemap = async ({
|
|
|
391
419
|
results.actualUrl = actualUrl;
|
|
392
420
|
|
|
393
421
|
await dataset.pushData(results);
|
|
422
|
+
|
|
423
|
+
// Discover <a> links from this page for the intelligent sitemap flow.
|
|
424
|
+
// This eliminates the need for a separate crawlDomain supplement phase
|
|
425
|
+
// that would re-visit all these pages just to extract links.
|
|
426
|
+
if (fromCrawlIntelligentSitemap) {
|
|
427
|
+
try {
|
|
428
|
+
await enqueueLinks({
|
|
429
|
+
selector: `a:not(${disallowedSelectorPatterns})`,
|
|
430
|
+
strategy,
|
|
431
|
+
requestQueue,
|
|
432
|
+
transformRequestFunction: (req) => {
|
|
433
|
+
try {
|
|
434
|
+
req.url = req.url.replace(/(?<=&|\?)utm_.*?(&|$)/gim, '');
|
|
435
|
+
} catch {}
|
|
436
|
+
if (isDisallowedInRobotsTxt(req.url)) return null;
|
|
437
|
+
if (isUrlPdf(req.url)) {
|
|
438
|
+
req.skipNavigation = true;
|
|
439
|
+
}
|
|
440
|
+
req.label = req.url;
|
|
441
|
+
return req;
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
} catch {
|
|
445
|
+
// Best-effort link discovery; don't fail the scan
|
|
446
|
+
}
|
|
447
|
+
}
|
|
394
448
|
}
|
|
395
449
|
} else {
|
|
396
450
|
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
@@ -399,20 +453,37 @@ const crawlSitemap = async ({
|
|
|
399
453
|
});
|
|
400
454
|
|
|
401
455
|
if (isScanHtml) {
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
456
|
+
// Non-HTML content types (images, PDFs, binary files) and URLs
|
|
457
|
+
// that redirect to non-HTML resources should be classified as
|
|
458
|
+
// unsupported documents, not generic page errors.
|
|
459
|
+
const isNonHtmlContent =
|
|
460
|
+
contentType &&
|
|
461
|
+
!contentType.startsWith('text/html') &&
|
|
462
|
+
!contentType.includes('html');
|
|
463
|
+
|
|
464
|
+
if (isNonHtmlContent && status !== 0) {
|
|
465
|
+
urlsCrawled.userExcluded.push({
|
|
466
|
+
actualUrl,
|
|
467
|
+
url: request.url,
|
|
468
|
+
pageTitle: request.url,
|
|
469
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
470
|
+
httpStatusCode: 1,
|
|
471
|
+
});
|
|
472
|
+
} else {
|
|
473
|
+
const httpStatus = response?.status();
|
|
474
|
+
const metadata =
|
|
475
|
+
typeof httpStatus === 'number'
|
|
476
|
+
? STATUS_CODE_METADATA[httpStatus] || STATUS_CODE_METADATA[599]
|
|
477
|
+
: STATUS_CODE_METADATA[2];
|
|
478
|
+
|
|
479
|
+
urlsCrawled.invalid.push({
|
|
480
|
+
actualUrl,
|
|
481
|
+
url: request.url,
|
|
482
|
+
pageTitle: request.url,
|
|
483
|
+
metadata,
|
|
484
|
+
httpStatusCode: typeof httpStatus === 'number' ? httpStatus : 0,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
416
487
|
}
|
|
417
488
|
}
|
|
418
489
|
} catch (e) {
|
|
@@ -427,7 +498,55 @@ const crawlSitemap = async ({
|
|
|
427
498
|
return;
|
|
428
499
|
}
|
|
429
500
|
|
|
501
|
+
// Handle download-triggered navigation errors: Playwright throws
|
|
502
|
+
// "Download is starting" when page.goto() hits a file download URL.
|
|
503
|
+
const isDownloadError = request.errorMessages?.some(
|
|
504
|
+
(msg: string) => msg.includes('Download is starting'),
|
|
505
|
+
);
|
|
506
|
+
if (isDownloadError) {
|
|
507
|
+
if (isScanPdfs) {
|
|
508
|
+
// Re-enqueue with skipNavigation so the requestHandler's PDF download path handles it
|
|
509
|
+
try {
|
|
510
|
+
await requestQueue.addRequest({
|
|
511
|
+
url: request.url,
|
|
512
|
+
skipNavigation: true,
|
|
513
|
+
label: request.url,
|
|
514
|
+
uniqueKey: `download_${request.url}`,
|
|
515
|
+
});
|
|
516
|
+
} catch {}
|
|
517
|
+
} else {
|
|
518
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
519
|
+
numScanned: urlsCrawled.scanned.length,
|
|
520
|
+
urlScanned: request.url,
|
|
521
|
+
});
|
|
522
|
+
urlsCrawled.userExcluded.push({
|
|
523
|
+
url: request.url,
|
|
524
|
+
pageTitle: request.url,
|
|
525
|
+
actualUrl: request.url,
|
|
526
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
527
|
+
httpStatusCode: 1,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
430
533
|
const status = response?.status();
|
|
534
|
+
|
|
535
|
+
// Re-enqueue rate-limited (403) URLs once for a retry after concurrency recovers.
|
|
536
|
+
// Don't call onFailure here — the re-enqueued request gets a fresh attempt.
|
|
537
|
+
// If it fails again (rateLimitRetried=true), it falls through to the normal path.
|
|
538
|
+
if (status === 403 && !request.userData?.rateLimitRetried) {
|
|
539
|
+
try {
|
|
540
|
+
await requestQueue.addRequest({
|
|
541
|
+
url: request.url,
|
|
542
|
+
label: request.url,
|
|
543
|
+
uniqueKey: `ratelimit_${request.url}`,
|
|
544
|
+
userData: { rateLimitRetried: true },
|
|
545
|
+
});
|
|
546
|
+
} catch {}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
431
550
|
if (rateController.onFailure(status, crawler.autoscaledPool)) {
|
|
432
551
|
consoleLogger.info(
|
|
433
552
|
`Aborting crawl: consecutive HTTP failures threshold reached (site may be rate-limiting). Successfully scanned ${urlsCrawled.scanned.length} pages.`,
|
|
@@ -457,8 +457,13 @@ function hasPointerCursor(node: Node): boolean {
|
|
|
457
457
|
function isElementTooSmall(element: Element) {
|
|
458
458
|
// Get the bounding rectangle of the element
|
|
459
459
|
const rect = element.getBoundingClientRect();
|
|
460
|
-
|
|
461
|
-
//
|
|
460
|
+
|
|
461
|
+
// If either dimension is 0, the element is non-perceivable
|
|
462
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Check if the element has a valid width and height
|
|
462
467
|
if (rect.width > 0 || rect.height > 0) {
|
|
463
468
|
return false; // Element is not too small
|
|
464
469
|
}
|
package/src/mergeAxeResults.ts
CHANGED
|
@@ -1110,7 +1110,7 @@ const generateArtifacts = async (
|
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
1112
|
try {
|
|
1113
|
-
const requestQueue = await RequestQueue.open(crawleeDir);
|
|
1113
|
+
const requestQueue = await RequestQueue.open(`${crawleeDir}_rq`);
|
|
1114
1114
|
await requestQueue.drop();
|
|
1115
1115
|
} catch (error) {
|
|
1116
1116
|
consoleLogger.info(`RequestQueue drop: ${error.message}`);
|
|
@@ -1120,6 +1120,7 @@ const generateArtifacts = async (
|
|
|
1120
1120
|
const crawleePath = path.join(storagePath, 'crawlee');
|
|
1121
1121
|
try {
|
|
1122
1122
|
await fs.promises.rm(crawleePath, { recursive: true, force: true });
|
|
1123
|
+
await fs.promises.rm(`${crawleePath}_rq`, { recursive: true, force: true });
|
|
1123
1124
|
} catch {
|
|
1124
1125
|
// Best-effort; storage was already dropped via API
|
|
1125
1126
|
}
|
package/src/utils.ts
CHANGED
|
@@ -390,14 +390,31 @@ export const cleanUp = async (randomToken?: string, isError: boolean = false): P
|
|
|
390
390
|
consoleLogger.warn(`Unable to force remove userDataDirectory: ${error.message}`);
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
// Also remove _pool* sibling directories created by browser pool re-launches
|
|
394
|
-
|
|
395
|
-
|
|
393
|
+
// Also remove _pool* sibling directories created by browser pool re-launches.
|
|
394
|
+
// On Windows, Chrome writes files (optimization_guide_model_store,
|
|
395
|
+
// segmentation_platform, first_party_sets.db, etc.) during its shutdown
|
|
396
|
+
// sequence and may still hold file locks. Retry up to 10 times at 5s intervals.
|
|
397
|
+
const removePoolDirs = (): number => {
|
|
398
|
+
// On Windows, glob interprets backslashes as escape characters, not path
|
|
399
|
+
// separators. Normalize to forward slashes so the pattern actually matches.
|
|
400
|
+
const globPattern = `${constants.userDataDirectory}_pool*`.replace(/\\/g, '/');
|
|
401
|
+
const poolDirs = globSync(globPattern);
|
|
396
402
|
for (const dir of poolDirs) {
|
|
397
403
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
398
404
|
}
|
|
399
|
-
|
|
400
|
-
|
|
405
|
+
return poolDirs.length;
|
|
406
|
+
};
|
|
407
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
408
|
+
try {
|
|
409
|
+
if (removePoolDirs() === 0) break; // nothing left to clean
|
|
410
|
+
break; // rmSync succeeded for all dirs
|
|
411
|
+
} catch {
|
|
412
|
+
if (attempt < 9) {
|
|
413
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
414
|
+
} else {
|
|
415
|
+
consoleLogger.warn('Unable to remove pool directories after 10 attempts');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
401
418
|
}
|
|
402
419
|
}
|
|
403
420
|
|
|
@@ -418,7 +435,7 @@ export const cleanUp = async (randomToken?: string, isError: boolean = false): P
|
|
|
418
435
|
const crawleeDir = path.join(storagePath, 'crawlee');
|
|
419
436
|
const dataset = await Dataset.open(crawleeDir);
|
|
420
437
|
await dataset.drop();
|
|
421
|
-
const requestQueue = await RequestQueue.open(crawleeDir);
|
|
438
|
+
const requestQueue = await RequestQueue.open(`${crawleeDir}_rq`);
|
|
422
439
|
await requestQueue.drop();
|
|
423
440
|
} catch (error) {
|
|
424
441
|
consoleLogger.info(`Crawlee storage drop in cleanUp: ${error.message}`);
|
|
@@ -428,6 +445,11 @@ export const cleanUp = async (randomToken?: string, isError: boolean = false): P
|
|
|
428
445
|
} catch (error) {
|
|
429
446
|
consoleLogger.warn(`Unable to force remove crawlee folder: ${error.message}`);
|
|
430
447
|
}
|
|
448
|
+
try {
|
|
449
|
+
fs.rmSync(path.join(storagePath, 'crawlee_rq'), { recursive: true, force: true });
|
|
450
|
+
} catch (error) {
|
|
451
|
+
consoleLogger.warn(`Unable to force remove crawlee_rq folder: ${error.message}`);
|
|
452
|
+
}
|
|
431
453
|
|
|
432
454
|
try {
|
|
433
455
|
fs.rmSync(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
|
|
@@ -1022,9 +1044,10 @@ export const zipResults = async (zipName: string, resultsPath: string): Promise<
|
|
|
1022
1044
|
if (!stats.isDirectory()) {
|
|
1023
1045
|
throw new Error(`resultsPath is not a directory: ${resultsPath}`);
|
|
1024
1046
|
}
|
|
1025
|
-
async function addFolderToZip(folderPath: string, zipFolder: JSZip): Promise<void> {
|
|
1047
|
+
async function addFolderToZip(folderPath: string, zipFolder: JSZip, isRoot = false): Promise<void> {
|
|
1026
1048
|
const items = await fs.readdir(folderPath);
|
|
1027
1049
|
for (const item of items) {
|
|
1050
|
+
if (isRoot && item.startsWith('crawlee')) continue;
|
|
1028
1051
|
const fullPath = path.join(folderPath, item);
|
|
1029
1052
|
const stats = await fs.stat(fullPath);
|
|
1030
1053
|
if (stats.isDirectory()) {
|
|
@@ -1038,7 +1061,7 @@ export const zipResults = async (zipName: string, resultsPath: string): Promise<
|
|
|
1038
1061
|
}
|
|
1039
1062
|
|
|
1040
1063
|
const zip = new JSZip();
|
|
1041
|
-
await addFolderToZip(resultsPath, zip);
|
|
1064
|
+
await addFolderToZip(resultsPath, zip, true);
|
|
1042
1065
|
|
|
1043
1066
|
const zipStream = zip.generateNodeStream({
|
|
1044
1067
|
type: 'nodebuffer',
|
/package/{4b15c550-be4f-428d-b0d0-933dafd76de3.txt → f0ce5c33-4dd0-4e14-8e67-1394f57ff517.txt}
RENAMED
|
File without changes
|