@crawlee/fs-storage 4.0.0-rc.0 → 4.0.0-rc.1
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/file-system-storage.d.ts +33 -12
- package/file-system-storage.js +79 -41
- package/index.d.ts +1 -0
- package/package.json +7 -7
- package/resource-clients/key-value-store.d.ts +5 -28
- package/resource-clients/key-value-store.js +11 -115
- package/resource-clients/request-queue.d.ts +5 -0
- package/resource-clients/request-queue.js +7 -0
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Crawlee is available as the [`crawlee`](https://www.npmjs.com/package/crawlee) N
|
|
|
34
34
|
|
|
35
35
|
We recommend visiting the [Introduction tutorial](https://crawlee.dev/js/docs/introduction) in Crawlee documentation for more information.
|
|
36
36
|
|
|
37
|
-
> Crawlee requires **Node.js
|
|
37
|
+
> Crawlee requires **Node.js 22.13 or higher**.
|
|
38
38
|
|
|
39
39
|
### With Crawlee CLI
|
|
40
40
|
|
package/file-system-storage.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type * as storage from '@crawlee/types';
|
|
2
2
|
import type { CrawleeLogger } from '@crawlee/types';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import type { AdoptionCandidate } from '@crawlee/fs-storage-native';
|
|
4
|
+
/** What the key-value store hooks of {@link FileSystemStorageBackend} learn about the store at hand. */
|
|
5
|
+
export interface KeyValueStoreHookOptions {
|
|
6
|
+
/** Whether the store is the run's default key-value store. */
|
|
7
|
+
isDefaultStore: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A file-system key-value store backend as seen by {@link FileSystemStorageBackend.purgeKeyValueStore}. */
|
|
10
|
+
export interface PurgeableKeyValueStoreBackend extends storage.KeyValueStoreBackend {
|
|
11
|
+
/** Remove every record from the store except the given keys. */
|
|
12
|
+
purgeExcept(keys: string[]): Promise<void>;
|
|
13
|
+
}
|
|
6
14
|
export interface FileSystemStorageOptions {
|
|
7
15
|
/**
|
|
8
16
|
* Path to directory where the data will be saved.
|
|
@@ -10,6 +18,8 @@ export interface FileSystemStorageOptions {
|
|
|
10
18
|
localDataDirectory: string;
|
|
11
19
|
/**
|
|
12
20
|
* Optional logger for FileSystemStorageBackend warnings.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
13
23
|
*/
|
|
14
24
|
logger?: CrawleeLogger;
|
|
15
25
|
/**
|
|
@@ -39,15 +49,6 @@ export interface FileSystemStorageOptions {
|
|
|
39
49
|
*/
|
|
40
50
|
export declare class FileSystemStorageBackend implements storage.StorageBackend {
|
|
41
51
|
#private;
|
|
42
|
-
readonly localDataDirectory: string;
|
|
43
|
-
readonly datasetsDirectory: string;
|
|
44
|
-
readonly keyValueStoresDirectory: string;
|
|
45
|
-
readonly requestQueuesDirectory: string;
|
|
46
|
-
readonly logger?: CrawleeLogger;
|
|
47
|
-
readonly requestQueueAccess: 'single' | 'shared';
|
|
48
|
-
readonly keyValueStoreBackendCache: KeyValueStoreBackend[];
|
|
49
|
-
readonly datasetBackendCache: DatasetBackend[];
|
|
50
|
-
readonly requestQueueBackendCache: RequestQueueBackend[];
|
|
51
52
|
constructor(options: FileSystemStorageOptions);
|
|
52
53
|
/**
|
|
53
54
|
* Return a cache key that includes the resolved storage directory, so that two
|
|
@@ -58,6 +59,26 @@ export declare class FileSystemStorageBackend implements storage.StorageBackend
|
|
|
58
59
|
createDatasetBackend(options?: storage.StorageIdentifier): Promise<storage.DatasetBackend>;
|
|
59
60
|
createKeyValueStoreBackend(options?: storage.StorageIdentifier): Promise<storage.KeyValueStoreBackend>;
|
|
60
61
|
createRequestQueueBackend(options?: storage.StorageIdentifier): Promise<storage.RequestQueueBackend>;
|
|
62
|
+
/**
|
|
63
|
+
* What the native `open` may turn into records: value files sitting in the store directory with no
|
|
64
|
+
* metadata sidecar, written out-of-band by a CLI, a project template, a v3 Crawlee or a text editor.
|
|
65
|
+
* Each becomes a record keyed by its filename, with the content type derived from the extension alone.
|
|
66
|
+
*
|
|
67
|
+
* Subclasses may claim specific files under a key of their own by prepending candidates — the Apify
|
|
68
|
+
* SDK adopts a bare `INPUT` or `INPUT.json` in the default store as the record `INPUT` this way.
|
|
69
|
+
*
|
|
70
|
+
* Once adopted, the file is an ordinary record: readable, listed, deletable, and — in a run-scoped
|
|
71
|
+
* store — purged on start unless {@link purgeKeyValueStore} spares it. Files adoption skips (dotfiles)
|
|
72
|
+
* are purged all the same: the purge sweeps the directory and keeps only the store metadata and the
|
|
73
|
+
* spared keys.
|
|
74
|
+
*
|
|
75
|
+
*/
|
|
76
|
+
protected keyValueStoreAdoptionCandidates(_options: KeyValueStoreHookOptions): AdoptionCandidate[];
|
|
77
|
+
/**
|
|
78
|
+
* Empties one run-scoped key-value store during {@link purge}. Subclasses may spare some keys of the
|
|
79
|
+
* default store with {@link PurgeableKeyValueStoreBackend.purgeExcept} — the Apify SDK keeps its run input.
|
|
80
|
+
*/
|
|
81
|
+
protected purgeKeyValueStore(store: PurgeableKeyValueStoreBackend, _options: KeyValueStoreHookOptions): Promise<void>;
|
|
61
82
|
storageExists(id: string, type: 'Dataset' | 'KeyValueStore' | 'RequestQueue'): Promise<boolean>;
|
|
62
83
|
/**
|
|
63
84
|
* Cleans up the run-scoped storages before the run starts, sweeping the storage directories so that
|
package/file-system-storage.js
CHANGED
|
@@ -23,6 +23,13 @@ async function importNativeModule() {
|
|
|
23
23
|
const DEFAULT_STORAGE_ALIAS = '__default__';
|
|
24
24
|
/** The directory the default storage lives in, one level below `datasets` / `key_value_stores` / etc. */
|
|
25
25
|
const DEFAULT_STORAGE_DIRECTORY = 'default';
|
|
26
|
+
/**
|
|
27
|
+
* Content types declared for adopted files. The native client infers nothing from extensions, and
|
|
28
|
+
* neither do we beyond this: a `.json` file is JSON, anything else is bytes for the
|
|
29
|
+
* {@link KeyValueStore} frontend to make sense of.
|
|
30
|
+
*/
|
|
31
|
+
const ADOPTED_JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
32
|
+
const ADOPTED_BINARY_CONTENT_TYPE = 'application/octet-stream';
|
|
26
33
|
/**
|
|
27
34
|
* A file-system storage backend backed by the native `@crawlee/fs-storage-native` Rust extension.
|
|
28
35
|
*
|
|
@@ -32,23 +39,23 @@ const DEFAULT_STORAGE_DIRECTORY = 'default';
|
|
|
32
39
|
* `teardown` can operate over them), and exposing them through the `@crawlee/types` interfaces.
|
|
33
40
|
*/
|
|
34
41
|
export class FileSystemStorageBackend {
|
|
35
|
-
localDataDirectory;
|
|
36
|
-
datasetsDirectory;
|
|
37
|
-
keyValueStoresDirectory;
|
|
38
|
-
requestQueuesDirectory;
|
|
39
|
-
logger;
|
|
40
|
-
requestQueueAccess;
|
|
41
|
-
keyValueStoreBackendCache = [];
|
|
42
|
-
datasetBackendCache = [];
|
|
43
|
-
requestQueueBackendCache = [];
|
|
42
|
+
#localDataDirectory;
|
|
43
|
+
#datasetsDirectory;
|
|
44
|
+
#keyValueStoresDirectory;
|
|
45
|
+
#requestQueuesDirectory;
|
|
46
|
+
#logger;
|
|
47
|
+
#requestQueueAccess;
|
|
48
|
+
#keyValueStoreBackendCache = [];
|
|
49
|
+
#datasetBackendCache = [];
|
|
50
|
+
#requestQueueBackendCache = [];
|
|
44
51
|
constructor(options) {
|
|
45
52
|
const { logger, requestQueueAccess, localDataDirectory } = parseArgument(options, fileSystemStorageOptionsSchema);
|
|
46
|
-
this
|
|
47
|
-
this
|
|
48
|
-
this
|
|
49
|
-
this
|
|
50
|
-
this
|
|
51
|
-
this
|
|
53
|
+
this.#logger = logger;
|
|
54
|
+
this.#requestQueueAccess = requestQueueAccess;
|
|
55
|
+
this.#localDataDirectory = localDataDirectory;
|
|
56
|
+
this.#datasetsDirectory = resolve(this.#localDataDirectory, 'datasets');
|
|
57
|
+
this.#keyValueStoresDirectory = resolve(this.#localDataDirectory, 'key_value_stores');
|
|
58
|
+
this.#requestQueuesDirectory = resolve(this.#localDataDirectory, 'request_queues');
|
|
52
59
|
}
|
|
53
60
|
/**
|
|
54
61
|
* Return a cache key that includes the resolved storage directory, so that two
|
|
@@ -56,7 +63,7 @@ export class FileSystemStorageBackend {
|
|
|
56
63
|
* partitions, by including the storage directory in the cache key.
|
|
57
64
|
*/
|
|
58
65
|
getStorageBackendCacheKey() {
|
|
59
|
-
return `FileSystemStorageBackend:${resolve(this
|
|
66
|
+
return `FileSystemStorageBackend:${resolve(this.#localDataDirectory)}`;
|
|
60
67
|
}
|
|
61
68
|
static #resolveStorageKey(options) {
|
|
62
69
|
// No identifier at all means the default storage, which is opened under the reserved alias —
|
|
@@ -73,75 +80,108 @@ export class FileSystemStorageBackend {
|
|
|
73
80
|
}
|
|
74
81
|
async createDatasetBackend(options = {}) {
|
|
75
82
|
const { id, name, alias, cacheKey } = FileSystemStorageBackend.#resolveStorageKey(options);
|
|
76
|
-
const found = this
|
|
83
|
+
const found = this.#datasetBackendCache.find((store) => store.id === cacheKey ||
|
|
77
84
|
store.name?.toLowerCase() === cacheKey.toLowerCase() ||
|
|
78
85
|
store.cacheKey.toLowerCase() === cacheKey.toLowerCase());
|
|
79
86
|
if (found) {
|
|
80
87
|
return found;
|
|
81
88
|
}
|
|
82
|
-
const nativeBackend = await (await importNativeModule()).FileSystemDatasetClient.open(id, name, alias, this
|
|
89
|
+
const nativeBackend = await (await importNativeModule()).FileSystemDatasetClient.open(id, name, alias, this.#localDataDirectory);
|
|
83
90
|
const newStore = await DatasetBackend.create({
|
|
84
91
|
name: alias ? undefined : (name ?? cacheKey),
|
|
85
92
|
cacheKey,
|
|
86
93
|
nativeBackend,
|
|
87
|
-
logger: this
|
|
94
|
+
logger: this.#logger,
|
|
88
95
|
});
|
|
89
|
-
this
|
|
96
|
+
this.#datasetBackendCache.push(newStore);
|
|
90
97
|
return newStore;
|
|
91
98
|
}
|
|
92
99
|
async createKeyValueStoreBackend(options = {}) {
|
|
93
100
|
const { id, name, alias, cacheKey } = FileSystemStorageBackend.#resolveStorageKey(options);
|
|
94
|
-
const found = this
|
|
101
|
+
const found = this.#keyValueStoreBackendCache.find((store) => store.id === cacheKey ||
|
|
95
102
|
store.name?.toLowerCase() === cacheKey.toLowerCase() ||
|
|
96
103
|
store.cacheKey.toLowerCase() === cacheKey.toLowerCase());
|
|
97
104
|
if (found) {
|
|
98
105
|
return found;
|
|
99
106
|
}
|
|
100
|
-
const nativeBackend = await (await importNativeModule()).FileSystemKeyValueStoreClient.open(id, name, alias, this
|
|
107
|
+
const nativeBackend = await (await importNativeModule()).FileSystemKeyValueStoreClient.open(id, name, alias, this.#localDataDirectory,
|
|
108
|
+
// useTestClock — always real wall-clock outside of native tests.
|
|
109
|
+
undefined, this.keyValueStoreAdoptionCandidates({ isDefaultStore: cacheKey === DEFAULT_STORAGE_DIRECTORY }));
|
|
101
110
|
const newStore = await KeyValueStoreBackend.create({
|
|
102
111
|
name: alias ? undefined : (name ?? cacheKey),
|
|
103
112
|
cacheKey,
|
|
104
113
|
nativeBackend,
|
|
105
|
-
logger: this
|
|
114
|
+
logger: this.#logger,
|
|
106
115
|
});
|
|
107
|
-
this
|
|
116
|
+
this.#keyValueStoreBackendCache.push(newStore);
|
|
108
117
|
return newStore;
|
|
109
118
|
}
|
|
110
119
|
async createRequestQueueBackend(options = {}) {
|
|
111
120
|
const { id, name, alias, cacheKey } = FileSystemStorageBackend.#resolveStorageKey(options);
|
|
112
|
-
const found = this
|
|
121
|
+
const found = this.#requestQueueBackendCache.find((queue) => queue.id === cacheKey ||
|
|
113
122
|
queue.name?.toLowerCase() === cacheKey.toLowerCase() ||
|
|
114
123
|
queue.cacheKey.toLowerCase() === cacheKey.toLowerCase());
|
|
115
124
|
if (found) {
|
|
116
125
|
return found;
|
|
117
126
|
}
|
|
118
|
-
const nativeBackend = await (await importNativeModule()).FileSystemRequestQueueClient.open(id, name, alias, this
|
|
127
|
+
const nativeBackend = await (await importNativeModule()).FileSystemRequestQueueClient.open(id, name, alias, this.#localDataDirectory,
|
|
119
128
|
// useTestClock — always real wall-clock outside of native tests.
|
|
120
|
-
undefined, this
|
|
129
|
+
undefined, this.#requestQueueAccess);
|
|
121
130
|
const newStore = await RequestQueueBackend.create({
|
|
122
131
|
name: alias ? undefined : (name ?? cacheKey),
|
|
123
132
|
cacheKey,
|
|
124
133
|
nativeBackend,
|
|
125
|
-
logger: this
|
|
134
|
+
logger: this.#logger,
|
|
126
135
|
});
|
|
127
|
-
this
|
|
136
|
+
this.#requestQueueBackendCache.push(newStore);
|
|
128
137
|
return newStore;
|
|
129
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* What the native `open` may turn into records: value files sitting in the store directory with no
|
|
141
|
+
* metadata sidecar, written out-of-band by a CLI, a project template, a v3 Crawlee or a text editor.
|
|
142
|
+
* Each becomes a record keyed by its filename, with the content type derived from the extension alone.
|
|
143
|
+
*
|
|
144
|
+
* Subclasses may claim specific files under a key of their own by prepending candidates — the Apify
|
|
145
|
+
* SDK adopts a bare `INPUT` or `INPUT.json` in the default store as the record `INPUT` this way.
|
|
146
|
+
*
|
|
147
|
+
* Once adopted, the file is an ordinary record: readable, listed, deletable, and — in a run-scoped
|
|
148
|
+
* store — purged on start unless {@link purgeKeyValueStore} spares it. Files adoption skips (dotfiles)
|
|
149
|
+
* are purged all the same: the purge sweeps the directory and keeps only the store metadata and the
|
|
150
|
+
* spared keys.
|
|
151
|
+
*
|
|
152
|
+
*/
|
|
153
|
+
keyValueStoreAdoptionCandidates(_options) {
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
files: [
|
|
157
|
+
{ filename: '*.json', contentType: ADOPTED_JSON_CONTENT_TYPE },
|
|
158
|
+
{ filename: '*', contentType: ADOPTED_BINARY_CONTENT_TYPE },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Empties one run-scoped key-value store during {@link purge}. Subclasses may spare some keys of the
|
|
165
|
+
* default store with {@link PurgeableKeyValueStoreBackend.purgeExcept} — the Apify SDK keeps its run input.
|
|
166
|
+
*/
|
|
167
|
+
async purgeKeyValueStore(store, _options) {
|
|
168
|
+
await store.purge();
|
|
169
|
+
}
|
|
130
170
|
async storageExists(id, type) {
|
|
131
171
|
let backends;
|
|
132
172
|
let baseDir;
|
|
133
173
|
switch (type) {
|
|
134
174
|
case 'Dataset':
|
|
135
|
-
backends = this
|
|
136
|
-
baseDir = this
|
|
175
|
+
backends = this.#datasetBackendCache;
|
|
176
|
+
baseDir = this.#datasetsDirectory;
|
|
137
177
|
break;
|
|
138
178
|
case 'KeyValueStore':
|
|
139
|
-
backends = this
|
|
140
|
-
baseDir = this
|
|
179
|
+
backends = this.#keyValueStoreBackendCache;
|
|
180
|
+
baseDir = this.#keyValueStoresDirectory;
|
|
141
181
|
break;
|
|
142
182
|
case 'RequestQueue':
|
|
143
|
-
backends = this
|
|
144
|
-
baseDir = this
|
|
183
|
+
backends = this.#requestQueueBackendCache;
|
|
184
|
+
baseDir = this.#requestQueuesDirectory;
|
|
145
185
|
break;
|
|
146
186
|
default:
|
|
147
187
|
return false;
|
|
@@ -214,11 +254,9 @@ export class FileSystemStorageBackend {
|
|
|
214
254
|
*/
|
|
215
255
|
async purge() {
|
|
216
256
|
await Promise.all([
|
|
217
|
-
this.#purgeRunScopedStorages(this
|
|
218
|
-
|
|
219
|
-
async (
|
|
220
|
-
this.#purgeRunScopedStorages(this.datasetsDirectory, async (alias) => this.createDatasetBackend({ alias }), async (store) => store.purge()),
|
|
221
|
-
this.#purgeRunScopedStorages(this.requestQueuesDirectory, async (alias) => this.createRequestQueueBackend({ alias }), async (store) => store.purge()),
|
|
257
|
+
this.#purgeRunScopedStorages(this.#keyValueStoresDirectory, async (alias) => this.createKeyValueStoreBackend({ alias }), async (store, isDefault) => this.purgeKeyValueStore(store, { isDefaultStore: isDefault })),
|
|
258
|
+
this.#purgeRunScopedStorages(this.#datasetsDirectory, async (alias) => this.createDatasetBackend({ alias }), async (store) => store.purge()),
|
|
259
|
+
this.#purgeRunScopedStorages(this.#requestQueuesDirectory, async (alias) => this.createRequestQueueBackend({ alias }), async (store) => store.purge()),
|
|
222
260
|
]);
|
|
223
261
|
}
|
|
224
262
|
/**
|
|
@@ -281,6 +319,6 @@ export class FileSystemStorageBackend {
|
|
|
281
319
|
* are not stuck (until their lock expires) for the next consumer of the same on-disk queue.
|
|
282
320
|
*/
|
|
283
321
|
async teardown() {
|
|
284
|
-
await Promise.all(this
|
|
322
|
+
await Promise.all(this.#requestQueueBackendCache.map(async (queue) => queue.persistState()));
|
|
285
323
|
}
|
|
286
324
|
}
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/fs-storage",
|
|
3
|
-
"version": "4.0.0-rc.
|
|
3
|
+
"version": "4.0.0-rc.1",
|
|
4
4
|
"description": "A file-system storage implementation of the Apify API",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=22.
|
|
6
|
+
"node": ">=22.13.0"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"exports": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@crawlee/fs-storage-native": "0.
|
|
46
|
-
"@crawlee/types": "4.0.0-rc.
|
|
47
|
-
"@crawlee/utils": "4.0.0-rc.
|
|
48
|
-
"zod": "^4.4
|
|
45
|
+
"@crawlee/fs-storage-native": ">=0.2.2 <0.3",
|
|
46
|
+
"@crawlee/types": "4.0.0-rc.1",
|
|
47
|
+
"@crawlee/utils": "4.0.0-rc.1",
|
|
48
|
+
"zod": "^4.5.4"
|
|
49
49
|
},
|
|
50
50
|
"lerna": {
|
|
51
51
|
"command": {
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
|
|
58
58
|
}
|
|
@@ -32,19 +32,16 @@ export declare class KeyValueStoreBackend extends CachedIdClient implements stor
|
|
|
32
32
|
drop(): Promise<void>;
|
|
33
33
|
purge(): Promise<void>;
|
|
34
34
|
/**
|
|
35
|
-
* Remove every record from the store except the
|
|
36
|
-
*
|
|
37
|
-
* while preserving the run's input, matching the historical file-system storage behavior.
|
|
38
|
-
*
|
|
39
|
-
* The native `purge` keep-list matches by exact key with no extension globbing, so we pass every
|
|
40
|
-
* filename the input might live under (`INPUT`, `INPUT.json`, `INPUT.txt`, `INPUT.bin`).
|
|
35
|
+
* Remove every record from the store except the given keys. For {@link FileSystemStorageBackend}
|
|
36
|
+
* subclasses that keep some of the default store's records across the purge on start.
|
|
41
37
|
*/
|
|
42
|
-
|
|
38
|
+
purgeExcept(keys: string[]): Promise<void>;
|
|
43
39
|
listKeys(options?: storage.KeyValueStoreListKeysOptions): Promise<storage.KeyValueStoreListKeysResult>;
|
|
44
40
|
/**
|
|
45
41
|
* Generates a public `file://` URL for accessing a specific record in the key-value store.
|
|
46
42
|
*
|
|
47
|
-
*
|
|
43
|
+
* Existence-agnostic per the {@link KeyValueStoreBackend} contract: the URL points at the file
|
|
44
|
+
* the key is bound to if there is a record, and at the file it would be written to otherwise.
|
|
48
45
|
* @param key The key of the record to generate the public URL for.
|
|
49
46
|
*/
|
|
50
47
|
getPublicUrl(key: string): Promise<string | undefined>;
|
|
@@ -58,24 +55,4 @@ export declare class KeyValueStoreBackend extends CachedIdClient implements stor
|
|
|
58
55
|
getValue(key: string): Promise<storage.KeyValueStoreRecord | undefined>;
|
|
59
56
|
setValue(record: storage.KeyValueStoreInputRecord): Promise<void>;
|
|
60
57
|
deleteValue(key: string): Promise<void>;
|
|
61
|
-
/**
|
|
62
|
-
* Resolve `key` to the on-disk key that actually exists, or `undefined` if nothing does. Every
|
|
63
|
-
* key is checked against its tracked record; the run-input keys additionally fall back to
|
|
64
|
-
* out-of-band bare files, in which case the matched on-disk key is returned so callers like
|
|
65
|
-
* `getPublicUrl` point at the file that exists. Two run-input shapes are handled (see
|
|
66
|
-
* {@link bareFallbacksFor}): the logical `INPUT`, which probes the conventional extensions, and a
|
|
67
|
-
* literal bare filename such as `INPUT.json` as listed by `listKeys`, which resolves itself.
|
|
68
|
-
*/
|
|
69
|
-
private resolveExistingKey;
|
|
70
|
-
/**
|
|
71
|
-
* The native `resolveValue`/`resolveExistingKey` bare-file fallbacks to use for `key`, or
|
|
72
|
-
* `undefined` if `key` is a plain tracked-record lookup with no bare-file probing.
|
|
73
|
-
*
|
|
74
|
-
* - The logical run-input key (`INPUT`) probes the full extension ladder (`INPUT`, `INPUT.json`,
|
|
75
|
-
* `INPUT.txt`, `INPUT.bin`), matching how Crawlee reads run input.
|
|
76
|
-
* - A literal bare filename as surfaced by `listKeys` (`INPUT.json`/`.txt`/`.bin`) resolves itself:
|
|
77
|
-
* the tracked record first, then the bare file at that exact name (a single empty-extension
|
|
78
|
-
* fallback), so a listed key round-trips through `getValue`/`recordExists`.
|
|
79
|
-
*/
|
|
80
|
-
private bareFallbacksFor;
|
|
81
58
|
}
|
|
@@ -3,22 +3,6 @@ import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { isStream } from '../utils.js';
|
|
5
5
|
import { CachedIdClient } from './cached-id-client.js';
|
|
6
|
-
/**
|
|
7
|
-
* Out-of-band ("bare") value-file fallbacks tried when the {@link ALLOWED_BARE_FILES} lookup misses the tracked
|
|
8
|
-
* record, so a lookup for `INPUT` also matches a hand-placed `INPUT.json`/`.txt`/`.bin`. Passed to the
|
|
9
|
-
* native `resolveValue`/`resolveExistingKey`, which do the probing and re-keying.
|
|
10
|
-
*
|
|
11
|
-
* Each entry declares the content type to report on a match — the native client does no MIME
|
|
12
|
-
* inference. An empty `contentType` is its sentinel for "keep the synthesized
|
|
13
|
-
* `application/octet-stream`", used for the extensionless key and `.bin`.
|
|
14
|
-
*/
|
|
15
|
-
const BARE_FILE_FALLBACKS = [
|
|
16
|
-
{ extension: '', contentType: '' },
|
|
17
|
-
{ extension: '.json', contentType: 'application/json; charset=utf-8' },
|
|
18
|
-
{ extension: '.txt', contentType: 'text/plain; charset=utf-8' },
|
|
19
|
-
{ extension: '.bin', contentType: '' },
|
|
20
|
-
];
|
|
21
|
-
const ALLOWED_BARE_FILES = ['INPUT'];
|
|
22
6
|
const keySchema = z.string();
|
|
23
7
|
const inputRecordShape = z.object({
|
|
24
8
|
key: z.string().min(1),
|
|
@@ -33,23 +17,6 @@ const inputRecordShape = z.object({
|
|
|
33
17
|
]),
|
|
34
18
|
contentType: z.string().min(1).optional(),
|
|
35
19
|
});
|
|
36
|
-
/**
|
|
37
|
-
* The out-of-band ("bare") files to surface from the native `listKeys`, derived from
|
|
38
|
-
* {@link ALLOWED_BARE_FILES} × {@link BARE_FILE_FALLBACKS}. Each native {@link ListBareFallback}
|
|
39
|
-
* `name` is the literal on-disk filename to probe (e.g. `INPUT.json`), and the native lists a match
|
|
40
|
-
* under that same `name` — which is exactly the key we return, so a listed bare file round-trips
|
|
41
|
-
* through `getValue`/`recordExists` (see {@link BARE_FILE_CONTENT_TYPES}).
|
|
42
|
-
*/
|
|
43
|
-
const LIST_BARE_FALLBACKS = ALLOWED_BARE_FILES.flatMap((key) => BARE_FILE_FALLBACKS.map(({ extension, contentType }) => ({ name: `${key}${extension}`, contentType })));
|
|
44
|
-
/**
|
|
45
|
-
* Lookup from a bare file's literal on-disk name (e.g. `INPUT.json`) to the content type to report
|
|
46
|
-
* for it, used to read a listed bare key back directly (`getValue('INPUT.json')`). The empty-extension
|
|
47
|
-
* entry (`INPUT`) is intentionally excluded: an extensionless `INPUT` lookup goes through the
|
|
48
|
-
* `resolveValue` fallback probing instead, which already covers the extensionless file.
|
|
49
|
-
*/
|
|
50
|
-
const BARE_FILE_CONTENT_TYPES = new Map(ALLOWED_BARE_FILES.flatMap((key) => BARE_FILE_FALLBACKS.filter(({ extension }) => extension !== '').map(({ extension, contentType }) => [`${key}${extension}`, contentType])));
|
|
51
|
-
/** Maps a bare file's on-disk name (e.g. `INPUT.json`) to its logical key (e.g. `INPUT`), for dedup. */
|
|
52
|
-
const BARE_FILE_LOGICAL_KEYS = new Map(ALLOWED_BARE_FILES.flatMap((key) => BARE_FILE_FALLBACKS.map(({ extension }) => [`${key}${extension}`, key])));
|
|
53
20
|
/**
|
|
54
21
|
* A file-system key-value store backend backed by the native `@crawlee/fs-storage-native` Rust
|
|
55
22
|
* extension.
|
|
@@ -86,60 +53,28 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
86
53
|
await this.#nativeBackend.purge();
|
|
87
54
|
}
|
|
88
55
|
/**
|
|
89
|
-
* Remove every record from the store except the
|
|
90
|
-
*
|
|
91
|
-
* while preserving the run's input, matching the historical file-system storage behavior.
|
|
92
|
-
*
|
|
93
|
-
* The native `purge` keep-list matches by exact key with no extension globbing, so we pass every
|
|
94
|
-
* filename the input might live under (`INPUT`, `INPUT.json`, `INPUT.txt`, `INPUT.bin`).
|
|
56
|
+
* Remove every record from the store except the given keys. For {@link FileSystemStorageBackend}
|
|
57
|
+
* subclasses that keep some of the default store's records across the purge on start.
|
|
95
58
|
*/
|
|
96
|
-
async
|
|
97
|
-
await this.#nativeBackend.purge(
|
|
59
|
+
async purgeExcept(keys) {
|
|
60
|
+
await this.#nativeBackend.purge(keys);
|
|
98
61
|
}
|
|
99
62
|
async listKeys(options = {}) {
|
|
100
63
|
const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
|
|
101
|
-
// Pass the bare-file fallbacks so out-of-band value files (e.g. a hand-placed `INPUT.json`)
|
|
102
|
-
// are enumerated alongside tracked records, under their actual on-disk name. The native reads
|
|
103
|
-
// everything it needs off the filesystem index — no per-file reads — so this stays cheap.
|
|
104
64
|
// The native `listKeys` already returns a self-describing page (items + pagination cursors)
|
|
105
|
-
// matching the `KeyValueStoreListKeysResult` contract
|
|
106
|
-
|
|
107
|
-
const presentKeys = new Set(page.items.map((record) => record.key));
|
|
108
|
-
// A bare value file is listed under its actual name (`INPUT.json`), which already round-trips
|
|
109
|
-
// through `getValue`/`recordExists`. The only collision is a tracked record occupying the
|
|
110
|
-
// logical key itself (`INPUT`): it shadows the extension-bearing bare variants (`INPUT.json`
|
|
111
|
-
// etc.) for the same logical key, so drop those. The extensionless bare file *is* the logical
|
|
112
|
-
// key, so it is never a separate duplicate.
|
|
113
|
-
const items = page.items.filter((record) => {
|
|
114
|
-
const logicalKey = BARE_FILE_LOGICAL_KEYS.get(record.key);
|
|
115
|
-
const isExtensionBearingBareFile = logicalKey !== undefined && logicalKey !== record.key;
|
|
116
|
-
return !(isExtensionBearingBareFile && presentKeys.has(logicalKey));
|
|
117
|
-
});
|
|
118
|
-
return {
|
|
119
|
-
items,
|
|
120
|
-
count: items.length,
|
|
121
|
-
limit: page.limit,
|
|
122
|
-
exclusiveStartKey: page.exclusiveStartKey,
|
|
123
|
-
isTruncated: page.isTruncated,
|
|
124
|
-
nextExclusiveStartKey: page.nextExclusiveStartKey,
|
|
125
|
-
};
|
|
65
|
+
// matching the `KeyValueStoreListKeysResult` contract.
|
|
66
|
+
return this.#nativeBackend.listKeys(exclusiveStartKey, limit, prefix);
|
|
126
67
|
}
|
|
127
68
|
/**
|
|
128
69
|
* Generates a public `file://` URL for accessing a specific record in the key-value store.
|
|
129
70
|
*
|
|
130
|
-
*
|
|
71
|
+
* Existence-agnostic per the {@link KeyValueStoreBackend} contract: the URL points at the file
|
|
72
|
+
* the key is bound to if there is a record, and at the file it would be written to otherwise.
|
|
131
73
|
* @param key The key of the record to generate the public URL for.
|
|
132
74
|
*/
|
|
133
75
|
async getPublicUrl(key) {
|
|
134
76
|
parseArgument(key, keySchema);
|
|
135
|
-
|
|
136
|
-
// so we resolve the on-disk key first (handling e.g. `INPUT` -> `INPUT.json`) and normalize
|
|
137
|
-
// the native `null`-for-missing result to the historical `undefined` contract.
|
|
138
|
-
const resolvedKey = await this.resolveExistingKey(key);
|
|
139
|
-
if (resolvedKey === undefined) {
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
return (await this.#nativeBackend.getPublicUrl(resolvedKey)) ?? undefined;
|
|
77
|
+
return this.#nativeBackend.getPublicUrl(key);
|
|
143
78
|
}
|
|
144
79
|
/**
|
|
145
80
|
* Tests whether a record with the given key exists without retrieving its value.
|
|
@@ -149,14 +84,11 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
149
84
|
*/
|
|
150
85
|
async recordExists(key) {
|
|
151
86
|
parseArgument(key, keySchema);
|
|
152
|
-
return
|
|
87
|
+
return this.#nativeBackend.recordExists(key);
|
|
153
88
|
}
|
|
154
89
|
async getValue(key) {
|
|
155
90
|
parseArgument(key, keySchema);
|
|
156
|
-
const
|
|
157
|
-
const record = fallbacks
|
|
158
|
-
? await this.#nativeBackend.resolveValue(key, fallbacks)
|
|
159
|
-
: await this.#nativeBackend.getValue(key);
|
|
91
|
+
const record = await this.#nativeBackend.getValue(key);
|
|
160
92
|
if (record) {
|
|
161
93
|
return {
|
|
162
94
|
key: record.key,
|
|
@@ -197,40 +129,4 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
197
129
|
parseArgument(key, keySchema);
|
|
198
130
|
await this.#nativeBackend.deleteValue(key);
|
|
199
131
|
}
|
|
200
|
-
/**
|
|
201
|
-
* Resolve `key` to the on-disk key that actually exists, or `undefined` if nothing does. Every
|
|
202
|
-
* key is checked against its tracked record; the run-input keys additionally fall back to
|
|
203
|
-
* out-of-band bare files, in which case the matched on-disk key is returned so callers like
|
|
204
|
-
* `getPublicUrl` point at the file that exists. Two run-input shapes are handled (see
|
|
205
|
-
* {@link bareFallbacksFor}): the logical `INPUT`, which probes the conventional extensions, and a
|
|
206
|
-
* literal bare filename such as `INPUT.json` as listed by `listKeys`, which resolves itself.
|
|
207
|
-
*/
|
|
208
|
-
async resolveExistingKey(key) {
|
|
209
|
-
const fallbacks = this.bareFallbacksFor(key);
|
|
210
|
-
if (fallbacks) {
|
|
211
|
-
return ((await this.#nativeBackend.resolveExistingKey(key, fallbacks.map(({ extension }) => extension))) ?? undefined);
|
|
212
|
-
}
|
|
213
|
-
return (await this.#nativeBackend.recordExists(key)) ? key : undefined;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* The native `resolveValue`/`resolveExistingKey` bare-file fallbacks to use for `key`, or
|
|
217
|
-
* `undefined` if `key` is a plain tracked-record lookup with no bare-file probing.
|
|
218
|
-
*
|
|
219
|
-
* - The logical run-input key (`INPUT`) probes the full extension ladder (`INPUT`, `INPUT.json`,
|
|
220
|
-
* `INPUT.txt`, `INPUT.bin`), matching how Crawlee reads run input.
|
|
221
|
-
* - A literal bare filename as surfaced by `listKeys` (`INPUT.json`/`.txt`/`.bin`) resolves itself:
|
|
222
|
-
* the tracked record first, then the bare file at that exact name (a single empty-extension
|
|
223
|
-
* fallback), so a listed key round-trips through `getValue`/`recordExists`.
|
|
224
|
-
*/
|
|
225
|
-
// eslint-disable-next-line class-methods-use-this
|
|
226
|
-
bareFallbacksFor(key) {
|
|
227
|
-
if (ALLOWED_BARE_FILES.includes(key)) {
|
|
228
|
-
return BARE_FILE_FALLBACKS;
|
|
229
|
-
}
|
|
230
|
-
const contentType = BARE_FILE_CONTENT_TYPES.get(key);
|
|
231
|
-
if (contentType !== undefined) {
|
|
232
|
-
return [{ extension: '', contentType }];
|
|
233
|
-
}
|
|
234
|
-
return undefined;
|
|
235
|
-
}
|
|
236
132
|
}
|
|
@@ -32,6 +32,11 @@ export declare class RequestQueueBackend extends CachedIdClient implements stora
|
|
|
32
32
|
* available again.
|
|
33
33
|
*/
|
|
34
34
|
setExpectedRequestProcessingTimeSecs(secs: number): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* @inheritdoc
|
|
37
|
+
* The lock is owned and extended by the native client; `false` means it no longer holds the request.
|
|
38
|
+
*/
|
|
39
|
+
extendRequestProcessingTimeSecs(requestId: string, secs: number): Promise<boolean>;
|
|
35
40
|
getMetadata(): Promise<storage.RequestQueueInfo>;
|
|
36
41
|
drop(): Promise<void>;
|
|
37
42
|
purge(): Promise<void>;
|
|
@@ -49,6 +49,13 @@ export class RequestQueueBackend extends CachedIdClient {
|
|
|
49
49
|
async setExpectedRequestProcessingTimeSecs(secs) {
|
|
50
50
|
await this.#nativeBackend.setExpectedRequestProcessingTime(secs);
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* @inheritdoc
|
|
54
|
+
* The lock is owned and extended by the native client; `false` means it no longer holds the request.
|
|
55
|
+
*/
|
|
56
|
+
async extendRequestProcessingTimeSecs(requestId, secs) {
|
|
57
|
+
return this.#nativeBackend.prolongRequestLock(requestId, secs);
|
|
58
|
+
}
|
|
52
59
|
async getMetadata() {
|
|
53
60
|
return this.#nativeBackend.getMetadata();
|
|
54
61
|
}
|