apify 4.0.0-beta.39 → 4.0.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/actor.d.ts +46 -29
- package/dist/actor.js +121 -43
- package/dist/apify_file_system_storage_backend.d.ts +34 -0
- package/dist/apify_file_system_storage_backend.js +41 -0
- package/dist/configuration.d.ts +1 -1
- package/dist/configuration.js +7 -1
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +23 -0
- package/package.json +7 -6
package/README.md
CHANGED
package/dist/actor.d.ts
CHANGED
|
@@ -673,17 +673,17 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
673
673
|
/**
|
|
674
674
|
* Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
|
|
675
675
|
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
* const input = await Actor.getInput();
|
|
680
|
-
* ```
|
|
676
|
+
* The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`).
|
|
677
|
+
* A record stored as `application/octet-stream` — which is what a local input file without an
|
|
678
|
+
* extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
|
|
681
679
|
*
|
|
682
|
-
* is
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
680
|
+
* When running locally and the store holds no such record, the input is read from a `<inputKey>` or
|
|
681
|
+
* `<inputKey>.json` file in the current working directory instead.
|
|
682
|
+
*
|
|
683
|
+
* Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
|
|
684
|
+
* files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
|
|
685
|
+
* (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
|
|
686
|
+
* Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest.
|
|
687
687
|
*
|
|
688
688
|
* Note that the `getInput()` function does not cache the value read from the key-value store.
|
|
689
689
|
* If you need to use the input multiple times in your Actor,
|
|
@@ -695,16 +695,20 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
695
695
|
* @returns
|
|
696
696
|
* Returns a promise that resolves to an object, string
|
|
697
697
|
* or [`Buffer`](https://nodejs.org/api/buffer.html), depending
|
|
698
|
-
* on the MIME content type of the record
|
|
699
|
-
*
|
|
698
|
+
* on the MIME content type of the record.
|
|
699
|
+
* @throws {ActorInputError} When the input cannot be produced; see `code`.
|
|
700
700
|
* @ignore
|
|
701
701
|
*/
|
|
702
|
-
getInput<T = Dictionary | string | Buffer>(): Promise<T
|
|
702
|
+
getInput<T = Dictionary | string | Buffer>(): Promise<T>;
|
|
703
703
|
/**
|
|
704
|
-
*
|
|
705
|
-
*
|
|
704
|
+
* Reads the input from a `<inputKey>` or `<inputKey>.json` file in the working directory, for local runs
|
|
705
|
+
* whose default key-value store holds no input record. Mirrors how `ApifyFileSystemStorageBackend` adopts
|
|
706
|
+
* such files inside the store directory: the `.json` file is JSON, the bare one is bytes that
|
|
707
|
+
* {@link parseInputValue} tries as JSON5. Both files present is an error rather than a guess.
|
|
708
|
+
*
|
|
709
|
+
* @returns `undefined` when running on the platform or when neither file exists.
|
|
706
710
|
*/
|
|
707
|
-
|
|
711
|
+
private readInputFromWorkingDirectory;
|
|
708
712
|
/**
|
|
709
713
|
* Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
|
|
710
714
|
*
|
|
@@ -1218,16 +1222,28 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
1218
1222
|
/**
|
|
1219
1223
|
* Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
|
|
1220
1224
|
*
|
|
1221
|
-
*
|
|
1222
|
-
* For example, calling the following code:
|
|
1225
|
+
* The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`):
|
|
1223
1226
|
* ```js
|
|
1224
1227
|
* const input = await Actor.getInput();
|
|
1225
1228
|
* ```
|
|
1226
1229
|
*
|
|
1227
|
-
* is
|
|
1230
|
+
* A record stored as `application/octet-stream` — which is what a local input file without an
|
|
1231
|
+
* extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
|
|
1232
|
+
*
|
|
1233
|
+
* When running locally and the store holds no such record, the input is read from a `<inputKey>` or
|
|
1234
|
+
* `<inputKey>.json` file in the current working directory instead.
|
|
1235
|
+
*
|
|
1236
|
+
* Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
|
|
1237
|
+
* files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
|
|
1238
|
+
* (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
|
|
1239
|
+
* Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest:
|
|
1228
1240
|
* ```js
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1241
|
+
* let input = {};
|
|
1242
|
+
* try {
|
|
1243
|
+
* input = await Actor.getInput();
|
|
1244
|
+
* } catch (error) {
|
|
1245
|
+
* if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
|
|
1246
|
+
* }
|
|
1231
1247
|
* ```
|
|
1232
1248
|
*
|
|
1233
1249
|
* Note that the `getInput()` function does not cache the value read from the key-value store.
|
|
@@ -1239,15 +1255,10 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
1239
1255
|
* @returns
|
|
1240
1256
|
* Returns a promise that resolves to an object, string
|
|
1241
1257
|
* or [`Buffer`](https://nodejs.org/api/buffer.html), depending
|
|
1242
|
-
* on the MIME content type of the record
|
|
1243
|
-
*
|
|
1244
|
-
*/
|
|
1245
|
-
static getInput<T = Dictionary | string | Buffer>(): Promise<T | null>;
|
|
1246
|
-
/**
|
|
1247
|
-
* Gets the Actor input value just like the {@link Actor.getInput} method,
|
|
1248
|
-
* but throws if it is not found.
|
|
1258
|
+
* on the MIME content type of the record.
|
|
1259
|
+
* @throws {ActorInputError} When the input cannot be produced; see `code`.
|
|
1249
1260
|
*/
|
|
1250
|
-
static
|
|
1261
|
+
static getInput<T = Dictionary | string | Buffer>(): Promise<T>;
|
|
1251
1262
|
/**
|
|
1252
1263
|
* Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
|
|
1253
1264
|
*
|
|
@@ -1375,6 +1386,12 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
1375
1386
|
* @internal
|
|
1376
1387
|
*/
|
|
1377
1388
|
static setDefaultInstance(instance?: Actor): void;
|
|
1389
|
+
/**
|
|
1390
|
+
* The same choice crawlee's `ServiceLocator` makes for its implicit default backend, with the
|
|
1391
|
+
* input-aware {@link ApifyFileSystemStorageBackend} in place of crawlee's — crawlee has no
|
|
1392
|
+
* notion of a run input.
|
|
1393
|
+
*/
|
|
1394
|
+
private createLocalStorageBackend;
|
|
1378
1395
|
/**
|
|
1379
1396
|
* crawlee's service locator is set-once, so a backend registered before `init()` wins and the
|
|
1380
1397
|
* Actor cannot install its own over it. Replaces the resulting `ServiceConflictError` with the
|
package/dist/actor.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createPrivateKey } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Dataset, EventType, KeyValueStore, MemoryStorageBackend, purgeDefaultStorages, rejectOperationInTransaction, RequestQueue, serviceLocator, } from '@crawlee/core';
|
|
3
5
|
import { sleep } from '@crawlee/utils';
|
|
4
6
|
import { ApifyClient } from 'apify-client';
|
|
5
7
|
import { z } from 'zod';
|
|
@@ -8,15 +10,29 @@ import { decryptInputSecrets } from '@apify/input_secrets';
|
|
|
8
10
|
import log from '@apify/log';
|
|
9
11
|
import { addTimeoutToPromise } from '@apify/timeout';
|
|
10
12
|
import { parseArgument } from '@apify/validations';
|
|
13
|
+
import { ApifyFileSystemStorageBackend } from './apify_file_system_storage_backend.js';
|
|
11
14
|
import { ApifyStorageBackend } from './apify_storage_backend.js';
|
|
12
15
|
import { ChargingManager, DEFAULT_DATASET_ITEM_EVENT } from './charging.js';
|
|
13
16
|
import { ChargingDatasetBackend, ChargingStorageBackend } from './charging_storage_backend.js';
|
|
14
17
|
import { Configuration } from './configuration.js';
|
|
18
|
+
import { ActorInputError } from './errors.js';
|
|
15
19
|
import { getDefaultsFromInputSchema, noActorInputSchemaDefinedMarker, readInputSchema } from './input-schemas.js';
|
|
16
20
|
import { PlatformEventManager } from './platform_event_manager.js';
|
|
17
21
|
import { ProxyConfiguration } from './proxy_configuration.js';
|
|
18
22
|
import { SmartApifyStorageBackend } from './smart_apify_storage_backend.js';
|
|
19
|
-
import { checkCrawleeVersion, getSystemInfo, isNonEmptyObject, printOutdatedSdkWarning, snakeCaseToCamelCase, } from './utils.js';
|
|
23
|
+
import { BINARY_CONTENT_TYPE, checkCrawleeVersion, getSystemInfo, isNonEmptyObject, JSON_CONTENT_TYPE, parseInputValue, printOutdatedSdkWarning, snakeCaseToCamelCase, } from './utils.js';
|
|
24
|
+
/**
|
|
25
|
+
* {@link parseInputValue} with the parser error wrapped as an {@link ActorInputError}, so a malformed
|
|
26
|
+
* JSON input reports where it came from.
|
|
27
|
+
*/
|
|
28
|
+
function parseInput(value, contentType, source) {
|
|
29
|
+
try {
|
|
30
|
+
return parseInputValue(value, contentType);
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
throw new ActorInputError('PARSE_FAILED', `The input in ${source} is not valid JSON.`, { cause });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
/**
|
|
21
37
|
* Exit codes for the Actor process.
|
|
22
38
|
* The error codes must be in the range 1-128, to avoid collision with signal exits
|
|
@@ -797,17 +813,17 @@ export class Actor {
|
|
|
797
813
|
/**
|
|
798
814
|
* Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
|
|
799
815
|
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* const input = await Actor.getInput();
|
|
804
|
-
* ```
|
|
816
|
+
* The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`).
|
|
817
|
+
* A record stored as `application/octet-stream` — which is what a local input file without an
|
|
818
|
+
* extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
|
|
805
819
|
*
|
|
806
|
-
* is
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
820
|
+
* When running locally and the store holds no such record, the input is read from a `<inputKey>` or
|
|
821
|
+
* `<inputKey>.json` file in the current working directory instead.
|
|
822
|
+
*
|
|
823
|
+
* Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
|
|
824
|
+
* files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
|
|
825
|
+
* (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
|
|
826
|
+
* Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest.
|
|
811
827
|
*
|
|
812
828
|
* Note that the `getInput()` function does not cache the value read from the key-value store.
|
|
813
829
|
* If you need to use the input multiple times in your Actor,
|
|
@@ -819,21 +835,39 @@ export class Actor {
|
|
|
819
835
|
* @returns
|
|
820
836
|
* Returns a promise that resolves to an object, string
|
|
821
837
|
* or [`Buffer`](https://nodejs.org/api/buffer.html), depending
|
|
822
|
-
* on the MIME content type of the record
|
|
823
|
-
*
|
|
838
|
+
* on the MIME content type of the record.
|
|
839
|
+
* @throws {ActorInputError} When the input cannot be produced; see `code`.
|
|
824
840
|
* @ignore
|
|
825
841
|
*/
|
|
826
842
|
async getInput() {
|
|
827
843
|
this.ensureActorInit('getInput');
|
|
828
|
-
const { inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.configuration;
|
|
829
|
-
const
|
|
844
|
+
const { inputKey, inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.configuration;
|
|
845
|
+
const store = await this.openKeyValueStore();
|
|
846
|
+
const record = await store.getRecord(inputKey);
|
|
847
|
+
const rawInput = record
|
|
848
|
+
? parseInput(record.value, record.contentType, `the "${inputKey}" record of the default key-value store`)
|
|
849
|
+
: await this.readInputFromWorkingDirectory(inputKey);
|
|
850
|
+
if (rawInput == null) {
|
|
851
|
+
const locations = [`the "${inputKey}" record of the default key-value store`];
|
|
852
|
+
if (!this.configuration.isAtHome) {
|
|
853
|
+
locations.push(`a "${inputKey}" or "${inputKey}.json" file in the working directory`);
|
|
854
|
+
}
|
|
855
|
+
throw new ActorInputError('NOT_FOUND', `Input does not exist. Expected ${locations.join(' or ')}.`);
|
|
856
|
+
}
|
|
830
857
|
let input = rawInput;
|
|
831
858
|
if (isNonEmptyObject(rawInput) && inputSecretsPrivateKeyFile && inputSecretsPrivateKeyPassphrase) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
859
|
+
try {
|
|
860
|
+
const privateKey = createPrivateKey({
|
|
861
|
+
key: Buffer.from(inputSecretsPrivateKeyFile, 'base64'),
|
|
862
|
+
passphrase: inputSecretsPrivateKeyPassphrase,
|
|
863
|
+
});
|
|
864
|
+
input = decryptInputSecrets({ input: rawInput, privateKey });
|
|
865
|
+
}
|
|
866
|
+
catch (cause) {
|
|
867
|
+
throw new ActorInputError('DECRYPTION_FAILED', 'Failed to decrypt the secret fields of the input.', {
|
|
868
|
+
cause,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
837
871
|
}
|
|
838
872
|
if (isNonEmptyObject(input) && !Buffer.isBuffer(input)) {
|
|
839
873
|
input = await this.inferDefaultsFromInputSchema(input);
|
|
@@ -841,15 +875,37 @@ export class Actor {
|
|
|
841
875
|
return input;
|
|
842
876
|
}
|
|
843
877
|
/**
|
|
844
|
-
*
|
|
845
|
-
*
|
|
878
|
+
* Reads the input from a `<inputKey>` or `<inputKey>.json` file in the working directory, for local runs
|
|
879
|
+
* whose default key-value store holds no input record. Mirrors how `ApifyFileSystemStorageBackend` adopts
|
|
880
|
+
* such files inside the store directory: the `.json` file is JSON, the bare one is bytes that
|
|
881
|
+
* {@link parseInputValue} tries as JSON5. Both files present is an error rather than a guess.
|
|
882
|
+
*
|
|
883
|
+
* @returns `undefined` when running on the platform or when neither file exists.
|
|
846
884
|
*/
|
|
847
|
-
async
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
throw new Error('Input does not exist');
|
|
885
|
+
async readInputFromWorkingDirectory(inputKey) {
|
|
886
|
+
if (this.configuration.isAtHome) {
|
|
887
|
+
return undefined;
|
|
851
888
|
}
|
|
852
|
-
|
|
889
|
+
const candidates = [
|
|
890
|
+
{ filename: inputKey, contentType: BINARY_CONTENT_TYPE },
|
|
891
|
+
{ filename: `${inputKey}.json`, contentType: JSON_CONTENT_TYPE },
|
|
892
|
+
];
|
|
893
|
+
const found = [];
|
|
894
|
+
for (const candidate of candidates) {
|
|
895
|
+
const path = join(process.cwd(), candidate.filename);
|
|
896
|
+
const stats = await stat(path).catch(() => null);
|
|
897
|
+
if (stats?.isFile()) {
|
|
898
|
+
found.push({ ...candidate, path });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (found.length > 1) {
|
|
902
|
+
throw new ActorInputError('MULTIPLE_FILES', `Found multiple input files in the working directory: ${found.map((file) => `"${file.filename}"`).join(', ')}. Keep only one of them.`);
|
|
903
|
+
}
|
|
904
|
+
if (found.length === 0) {
|
|
905
|
+
return undefined;
|
|
906
|
+
}
|
|
907
|
+
const [file] = found;
|
|
908
|
+
return parseInput(await readFile(file.path), file.contentType, `the "${file.filename}" file in the working directory`);
|
|
853
909
|
}
|
|
854
910
|
/**
|
|
855
911
|
* Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
|
|
@@ -1463,16 +1519,28 @@ export class Actor {
|
|
|
1463
1519
|
/**
|
|
1464
1520
|
* Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
|
|
1465
1521
|
*
|
|
1466
|
-
*
|
|
1467
|
-
* For example, calling the following code:
|
|
1522
|
+
* The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`):
|
|
1468
1523
|
* ```js
|
|
1469
1524
|
* const input = await Actor.getInput();
|
|
1470
1525
|
* ```
|
|
1471
1526
|
*
|
|
1472
|
-
* is
|
|
1527
|
+
* A record stored as `application/octet-stream` — which is what a local input file without an
|
|
1528
|
+
* extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
|
|
1529
|
+
*
|
|
1530
|
+
* When running locally and the store holds no such record, the input is read from a `<inputKey>` or
|
|
1531
|
+
* `<inputKey>.json` file in the current working directory instead.
|
|
1532
|
+
*
|
|
1533
|
+
* Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
|
|
1534
|
+
* files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
|
|
1535
|
+
* (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
|
|
1536
|
+
* Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest:
|
|
1473
1537
|
* ```js
|
|
1474
|
-
*
|
|
1475
|
-
*
|
|
1538
|
+
* let input = {};
|
|
1539
|
+
* try {
|
|
1540
|
+
* input = await Actor.getInput();
|
|
1541
|
+
* } catch (error) {
|
|
1542
|
+
* if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
|
|
1543
|
+
* }
|
|
1476
1544
|
* ```
|
|
1477
1545
|
*
|
|
1478
1546
|
* Note that the `getInput()` function does not cache the value read from the key-value store.
|
|
@@ -1484,19 +1552,12 @@ export class Actor {
|
|
|
1484
1552
|
* @returns
|
|
1485
1553
|
* Returns a promise that resolves to an object, string
|
|
1486
1554
|
* or [`Buffer`](https://nodejs.org/api/buffer.html), depending
|
|
1487
|
-
* on the MIME content type of the record
|
|
1488
|
-
*
|
|
1555
|
+
* on the MIME content type of the record.
|
|
1556
|
+
* @throws {ActorInputError} When the input cannot be produced; see `code`.
|
|
1489
1557
|
*/
|
|
1490
1558
|
static async getInput() {
|
|
1491
1559
|
return Actor.getDefaultInstance().getInput();
|
|
1492
1560
|
}
|
|
1493
|
-
/**
|
|
1494
|
-
* Gets the Actor input value just like the {@link Actor.getInput} method,
|
|
1495
|
-
* but throws if it is not found.
|
|
1496
|
-
*/
|
|
1497
|
-
static async getInputOrThrow() {
|
|
1498
|
-
return Actor.getDefaultInstance().getInputOrThrow();
|
|
1499
|
-
}
|
|
1500
1561
|
/**
|
|
1501
1562
|
* Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
|
|
1502
1563
|
*
|
|
@@ -1668,10 +1729,27 @@ export class Actor {
|
|
|
1668
1729
|
configuration: this.configuration,
|
|
1669
1730
|
requestQueueAccess: this.#requestQueueAccess,
|
|
1670
1731
|
}), charging),
|
|
1671
|
-
localStorageBackend: new ChargingStorageBackend(
|
|
1732
|
+
localStorageBackend: new ChargingStorageBackend(this.createLocalStorageBackend(), charging),
|
|
1672
1733
|
configuration: this.configuration,
|
|
1673
1734
|
}));
|
|
1674
1735
|
}
|
|
1736
|
+
/**
|
|
1737
|
+
* The same choice crawlee's `ServiceLocator` makes for its implicit default backend, with the
|
|
1738
|
+
* input-aware {@link ApifyFileSystemStorageBackend} in place of crawlee's — crawlee has no
|
|
1739
|
+
* notion of a run input.
|
|
1740
|
+
*/
|
|
1741
|
+
createLocalStorageBackend() {
|
|
1742
|
+
const { persistStorage, storageDir, inputKey } = this.configuration;
|
|
1743
|
+
const logger = serviceLocator.getLogger();
|
|
1744
|
+
if (!persistStorage) {
|
|
1745
|
+
return new MemoryStorageBackend({ logger: logger.child({ prefix: 'MemoryStorageBackend' }) });
|
|
1746
|
+
}
|
|
1747
|
+
return new ApifyFileSystemStorageBackend({
|
|
1748
|
+
localDataDirectory: storageDir,
|
|
1749
|
+
inputKey,
|
|
1750
|
+
logger: logger.child({ prefix: 'ApifyFileSystemStorageBackend' }),
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1675
1753
|
/** Whether items reaching the run's default dataset are counted for pay-per-event charging. */
|
|
1676
1754
|
#chargesDefaultDatasetItems() {
|
|
1677
1755
|
const installed = serviceLocator.getStorageBackend();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AdoptionCandidate, FileSystemStorageOptions, KeyValueStoreHookOptions, PurgeableKeyValueStoreBackend } from '@crawlee/fs-storage';
|
|
2
|
+
import { FileSystemStorageBackend } from '@crawlee/fs-storage';
|
|
3
|
+
export interface ApifyFileSystemStorageOptions extends FileSystemStorageOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The key the Actor input is read from — `Configuration.inputKey` (`ACTOR_INPUT_KEY`).
|
|
6
|
+
*
|
|
7
|
+
* `INPUT` is treated as an input key regardless of this setting, so a project's `INPUT.json` stays
|
|
8
|
+
* intact when the Apify CLI redirects the run to a temporary key.
|
|
9
|
+
*
|
|
10
|
+
* @default 'INPUT'
|
|
11
|
+
*/
|
|
12
|
+
inputKey?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Crawlee's file-system storage backend, aware of the Actor input.
|
|
16
|
+
*
|
|
17
|
+
* Locally the input arrives in the default key-value store as a bare `INPUT` or `INPUT.json` file with
|
|
18
|
+
* no metadata sidecar — written by the Apify CLI, a project template, or by hand. This backend adopts
|
|
19
|
+
* such a file as the record under the input key when the store is opened (both files present fails the
|
|
20
|
+
* open), and keeps the input keys when the default store is purged on start. Everything else is
|
|
21
|
+
* inherited from {@link FileSystemStorageBackend}.
|
|
22
|
+
*
|
|
23
|
+
* `Actor` installs it as the local backend; pass an instance to `Actor.init({ storage })` only if you
|
|
24
|
+
* need to construct it yourself.
|
|
25
|
+
*/
|
|
26
|
+
export declare class ApifyFileSystemStorageBackend extends FileSystemStorageBackend {
|
|
27
|
+
/** `INPUT` plus the configured input key, deduplicated. */
|
|
28
|
+
private readonly inputKeys;
|
|
29
|
+
constructor(options: ApifyFileSystemStorageOptions);
|
|
30
|
+
/** Claims a bare `<key>` or `<key>.json` in the default store for each input key, ahead of the generic sweep. */
|
|
31
|
+
protected keyValueStoreAdoptionCandidates(options: KeyValueStoreHookOptions): AdoptionCandidate[];
|
|
32
|
+
/** Spares the input keys in the default store; every other run-scoped store is emptied. */
|
|
33
|
+
protected purgeKeyValueStore(store: PurgeableKeyValueStoreBackend, options: KeyValueStoreHookOptions): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FileSystemStorageBackend } from '@crawlee/fs-storage';
|
|
2
|
+
import { KEY_VALUE_STORE_KEYS } from '@apify/consts';
|
|
3
|
+
import { BINARY_CONTENT_TYPE, JSON_CONTENT_TYPE } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Crawlee's file-system storage backend, aware of the Actor input.
|
|
6
|
+
*
|
|
7
|
+
* Locally the input arrives in the default key-value store as a bare `INPUT` or `INPUT.json` file with
|
|
8
|
+
* no metadata sidecar — written by the Apify CLI, a project template, or by hand. This backend adopts
|
|
9
|
+
* such a file as the record under the input key when the store is opened (both files present fails the
|
|
10
|
+
* open), and keeps the input keys when the default store is purged on start. Everything else is
|
|
11
|
+
* inherited from {@link FileSystemStorageBackend}.
|
|
12
|
+
*
|
|
13
|
+
* `Actor` installs it as the local backend; pass an instance to `Actor.init({ storage })` only if you
|
|
14
|
+
* need to construct it yourself.
|
|
15
|
+
*/
|
|
16
|
+
export class ApifyFileSystemStorageBackend extends FileSystemStorageBackend {
|
|
17
|
+
/** `INPUT` plus the configured input key, deduplicated. */
|
|
18
|
+
inputKeys;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
const { inputKey = KEY_VALUE_STORE_KEYS.INPUT, ...fileSystemOptions } = options;
|
|
21
|
+
super(fileSystemOptions);
|
|
22
|
+
this.inputKeys = [...new Set([KEY_VALUE_STORE_KEYS.INPUT, inputKey])];
|
|
23
|
+
}
|
|
24
|
+
/** Claims a bare `<key>` or `<key>.json` in the default store for each input key, ahead of the generic sweep. */
|
|
25
|
+
keyValueStoreAdoptionCandidates(options) {
|
|
26
|
+
const inputCandidates = options.isDefaultStore
|
|
27
|
+
? this.inputKeys.map((key) => ({
|
|
28
|
+
key,
|
|
29
|
+
files: [
|
|
30
|
+
{ filename: key, contentType: BINARY_CONTENT_TYPE },
|
|
31
|
+
{ filename: `${key}.json`, contentType: JSON_CONTENT_TYPE },
|
|
32
|
+
],
|
|
33
|
+
}))
|
|
34
|
+
: [];
|
|
35
|
+
return [...inputCandidates, ...super.keyValueStoreAdoptionCandidates(options)];
|
|
36
|
+
}
|
|
37
|
+
/** Spares the input keys in the default store; every other run-scoped store is emptied. */
|
|
38
|
+
async purgeKeyValueStore(store, options) {
|
|
39
|
+
await (options.isDefaultStore ? store.purgeExcept([...this.inputKeys]) : store.purge());
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/configuration.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ export declare const apifyConfigFields: {
|
|
|
9
9
|
persistStorage: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean, unknown>>>;
|
|
10
10
|
storageDir: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
11
11
|
containerized: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodBoolean, unknown>>>;
|
|
12
|
-
inputKey: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
13
12
|
memoryMbytes: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber, unknown>>>;
|
|
14
13
|
availableMemoryRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber, unknown>>>;
|
|
15
14
|
disableBrowserSandbox: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean, unknown>>>;
|
|
@@ -22,6 +21,7 @@ export declare const apifyConfigFields: {
|
|
|
22
21
|
defaultDatasetId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
23
22
|
defaultKeyValueStoreId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
24
23
|
defaultRequestQueueId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
24
|
+
inputKey: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
25
25
|
metamorphAfterSleepMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber, unknown>>>;
|
|
26
26
|
actorEventsWsUrl: ConfigField<z.ZodOptional<z.ZodString>>;
|
|
27
27
|
token: ConfigField<z.ZodOptional<z.ZodString>>;
|
package/dist/configuration.js
CHANGED
|
@@ -20,7 +20,6 @@ export const apifyConfigFields = {
|
|
|
20
20
|
// take precedence; crawlee's own CRAWLEE_* var is reused as the fallback,
|
|
21
21
|
// never re-typed). A schema is passed only where the SDK needs a different
|
|
22
22
|
// default than crawlee's.
|
|
23
|
-
inputKey: withApifyEnv(crawleeConfigFields.inputKey, [ACTOR_ENV_VARS.INPUT_KEY, APIFY_ENV_VARS.INPUT_KEY]),
|
|
24
23
|
memoryMbytes: withApifyEnv(crawleeConfigFields.memoryMbytes, [
|
|
25
24
|
ACTOR_ENV_VARS.MEMORY_MBYTES,
|
|
26
25
|
APIFY_ENV_VARS.MEMORY_MBYTES,
|
|
@@ -51,6 +50,13 @@ export const apifyConfigFields = {
|
|
|
51
50
|
APIFY_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID,
|
|
52
51
|
]),
|
|
53
52
|
// Apify-specific fields
|
|
53
|
+
// Crawlee has no notion of a run input; `CRAWLEE_INPUT_KEY` stays as the
|
|
54
|
+
// lowest-priority alias because the Apify CLI sets all three variables.
|
|
55
|
+
inputKey: field(z.string().default('INPUT'), [
|
|
56
|
+
ACTOR_ENV_VARS.INPUT_KEY,
|
|
57
|
+
APIFY_ENV_VARS.INPUT_KEY,
|
|
58
|
+
'CRAWLEE_INPUT_KEY',
|
|
59
|
+
]),
|
|
54
60
|
metamorphAfterSleepMillis: field(coerceNumber.default(300_000), APIFY_ENV_VARS.METAMORPH_AFTER_SLEEP_MILLIS),
|
|
55
61
|
actorEventsWsUrl: field(z.string().optional(), [
|
|
56
62
|
ACTOR_ENV_VARS.EVENTS_WEBSOCKET_URL,
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Why {@link Actor.getInput} could not produce the input. */
|
|
2
|
+
export type ActorInputErrorCode =
|
|
3
|
+
/** No input record in the default key-value store, and locally no input file in the working directory either. */
|
|
4
|
+
'NOT_FOUND'
|
|
5
|
+
/** Both `<inputKey>` and `<inputKey>.json` exist in the working directory, so neither is picked. */
|
|
6
|
+
| 'MULTIPLE_FILES'
|
|
7
|
+
/** The secret fields of the input could not be decrypted; `cause` holds the underlying error. */
|
|
8
|
+
| 'DECRYPTION_FAILED'
|
|
9
|
+
/** A JSON input record or file does not parse; `cause` holds the parser error. */
|
|
10
|
+
| 'PARSE_FAILED';
|
|
11
|
+
/**
|
|
12
|
+
* Thrown by {@link Actor.getInput} when the Actor input cannot be produced. `code` says why, and
|
|
13
|
+
* `cause` carries the underlying error where there is one. Anything else thrown by `getInput()`, such as
|
|
14
|
+
* an Apify API error, is not an input problem and is passed through as is.
|
|
15
|
+
*
|
|
16
|
+
* ```js
|
|
17
|
+
* try {
|
|
18
|
+
* input = await Actor.getInput();
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
|
|
21
|
+
* input = {};
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class ActorInputError extends Error {
|
|
26
|
+
readonly name = "ActorInputError";
|
|
27
|
+
readonly code: ActorInputErrorCode;
|
|
28
|
+
constructor(code: ActorInputErrorCode, message: string, options?: {
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
});
|
|
31
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link Actor.getInput} when the Actor input cannot be produced. `code` says why, and
|
|
3
|
+
* `cause` carries the underlying error where there is one. Anything else thrown by `getInput()`, such as
|
|
4
|
+
* an Apify API error, is not an input problem and is passed through as is.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* try {
|
|
8
|
+
* input = await Actor.getInput();
|
|
9
|
+
* } catch (error) {
|
|
10
|
+
* if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
|
|
11
|
+
* input = {};
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export class ActorInputError extends Error {
|
|
16
|
+
name = 'ActorInputError';
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message, options) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from './actor.js';
|
|
2
2
|
export { ApifyStorageBackend, type ApifyStorageBackendOptions } from './apify_storage_backend.js';
|
|
3
|
+
export { ApifyFileSystemStorageBackend, type ApifyFileSystemStorageOptions, } from './apify_file_system_storage_backend.js';
|
|
3
4
|
export type { RequestQueueAccessMode } from './apify_request_queue_backend.js';
|
|
4
5
|
export { ArgumentValidationError } from './utils.js';
|
|
6
|
+
export { ActorInputError, type ActorInputErrorCode } from './errors.js';
|
|
5
7
|
export { createTransformRequestFunction, type GlobInput, type PseudoUrlInput, type UrlPatternFilters, type UrlPatternRequestOptions, } from './enqueue_links_filters.js';
|
|
6
8
|
export type { OpenStorageOptions, StorageAlias, StorageId, StorageName, StorageIdentifier } from './storage.js';
|
|
7
9
|
export { ChargeOptions, ChargeResult, ActorPricingInfo, ChargingManager } from './charging.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './actor.js';
|
|
2
2
|
export { ApifyStorageBackend } from './apify_storage_backend.js';
|
|
3
|
+
export { ApifyFileSystemStorageBackend, } from './apify_file_system_storage_backend.js';
|
|
3
4
|
export { ArgumentValidationError } from './utils.js';
|
|
5
|
+
export { ActorInputError } from './errors.js';
|
|
4
6
|
export { createTransformRequestFunction, } from './enqueue_links_filters.js';
|
|
5
7
|
export { ChargingManager } from './charging.js';
|
|
6
8
|
export * from './configuration.js';
|
package/dist/utils.d.ts
CHANGED
|
@@ -5,6 +5,17 @@ export { ArgumentValidationError } from '@apify/validations';
|
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
7
|
export declare function isNonEmptyObject(value: unknown): value is Record<string, unknown>;
|
|
8
|
+
/** Content type of a JSON record, as `KeyValueStore` and the file-system adoption write it. */
|
|
9
|
+
export declare const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
10
|
+
/** Content type of raw bytes, which is what a local input file without an extension is adopted as. */
|
|
11
|
+
export declare const BINARY_CONTENT_TYPE = "application/octet-stream";
|
|
12
|
+
/**
|
|
13
|
+
* Parses a raw input record the way `KeyValueStore.getValue` would, with one exception:
|
|
14
|
+
* `application/octet-stream` — the type a local input file without an extension is adopted as — is
|
|
15
|
+
* tried as JSON5 first, so such a file still yields an object. Bytes that do not parse come back as they are.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseInputValue(value: Buffer | ArrayBuffer, contentType: string | null): unknown;
|
|
8
19
|
/**
|
|
9
20
|
* Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
|
|
10
21
|
* @internal
|
package/dist/utils.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { type } from 'node:os';
|
|
4
4
|
import { normalize } from 'node:path';
|
|
5
|
+
import { parseValue } from '@crawlee/core';
|
|
5
6
|
// @ts-ignore if we enable resolveJsonModule, we end up with `src` folder in `dist`
|
|
6
7
|
import crawleePkgJson from '@crawlee/core/package.json' with { type: 'json' };
|
|
7
8
|
// @ts-ignore if we enable resolveJsonModule, we end up with `src` folder in `dist`
|
|
@@ -21,6 +22,28 @@ const require = createRequire(import.meta.url);
|
|
|
21
22
|
export function isNonEmptyObject(value) {
|
|
22
23
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
23
24
|
}
|
|
25
|
+
/** Content type of a JSON record, as `KeyValueStore` and the file-system adoption write it. */
|
|
26
|
+
export const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
27
|
+
/** Content type of raw bytes, which is what a local input file without an extension is adopted as. */
|
|
28
|
+
export const BINARY_CONTENT_TYPE = 'application/octet-stream';
|
|
29
|
+
/**
|
|
30
|
+
* Parses a raw input record the way `KeyValueStore.getValue` would, with one exception:
|
|
31
|
+
* `application/octet-stream` — the type a local input file without an extension is adopted as — is
|
|
32
|
+
* tried as JSON5 first, so such a file still yields an object. Bytes that do not parse come back as they are.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export function parseInputValue(value, contentType) {
|
|
36
|
+
const mediaType = contentType?.split(';')[0].trim().toLowerCase();
|
|
37
|
+
if (mediaType === BINARY_CONTENT_TYPE) {
|
|
38
|
+
try {
|
|
39
|
+
return parseValue(value, JSON_CONTENT_TYPE);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return parseValue(value, contentType);
|
|
46
|
+
}
|
|
24
47
|
/**
|
|
25
48
|
* Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
|
|
26
49
|
* @internal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apify",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.41",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -61,9 +61,10 @@
|
|
|
61
61
|
"@apify/timeout": "^1.0.1",
|
|
62
62
|
"@apify/utilities": "^3.0.1",
|
|
63
63
|
"@apify/validations": "^1.1.0",
|
|
64
|
-
"@crawlee/core": ">=4.0.0-beta.
|
|
65
|
-
"@crawlee/
|
|
66
|
-
"@crawlee/
|
|
64
|
+
"@crawlee/core": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
|
|
65
|
+
"@crawlee/fs-storage": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
|
|
66
|
+
"@crawlee/types": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
|
|
67
|
+
"@crawlee/utils": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
|
|
67
68
|
"apify-client": "^3.0.0-beta.26",
|
|
68
69
|
"minimatch": "^10.2.6",
|
|
69
70
|
"semver": "^7.8.5",
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
"@types/tough-cookie": "^4.0.5",
|
|
82
83
|
"@types/ws": "^8.18.1",
|
|
83
84
|
"commitlint": "^21.2.2",
|
|
84
|
-
"crawlee": ">=4.0.0-beta.
|
|
85
|
+
"crawlee": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
|
|
85
86
|
"husky": "^9.1.7",
|
|
86
87
|
"lint-staged": "^17.3.0",
|
|
87
88
|
"oxfmt": "0.64.0",
|
|
@@ -97,7 +98,7 @@
|
|
|
97
98
|
"devEngines": {
|
|
98
99
|
"packageManager": {
|
|
99
100
|
"name": "pnpm",
|
|
100
|
-
"version": "
|
|
101
|
+
"version": "12.6.0",
|
|
101
102
|
"onFail": "warn"
|
|
102
103
|
}
|
|
103
104
|
},
|