@crawlee/fs-storage 4.0.0-beta.178 → 4.0.0-beta.180
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/file-system-storage.d.ts
CHANGED
|
@@ -28,11 +28,11 @@ export interface FileSystemStorageOptions {
|
|
|
28
28
|
/**
|
|
29
29
|
* The key the run input is read from — Crawlee's `inputKey` (`CRAWLEE_INPUT_KEY`).
|
|
30
30
|
*
|
|
31
|
-
* Like the conventional `INPUT`, this key may
|
|
31
|
+
* Like the conventional `INPUT`, this key may arrive in the default key-value store as a bare value
|
|
32
32
|
* file with no metadata sidecar (e.g. the Apify CLI writes the effective input to `__CLI_INPUT.json`
|
|
33
|
-
* and points the run at that key).
|
|
34
|
-
* preserved when the default store is purged
|
|
35
|
-
* which is always
|
|
33
|
+
* and points the run at that key). Such a file is adopted into a record under this key when the
|
|
34
|
+
* store is opened, and the key is preserved when the default store is purged — exactly like
|
|
35
|
+
* `INPUT`, which is always treated as an input key regardless of this setting.
|
|
36
36
|
*
|
|
37
37
|
* @default 'INPUT'
|
|
38
38
|
*/
|
package/file-system-storage.js
CHANGED
|
@@ -24,6 +24,15 @@ async function importNativeModule() {
|
|
|
24
24
|
const DEFAULT_STORAGE_ALIAS = '__default__';
|
|
25
25
|
/** The directory the default storage lives in, one level below `datasets` / `key_value_stores` / etc. */
|
|
26
26
|
const DEFAULT_STORAGE_DIRECTORY = 'default';
|
|
27
|
+
/** The conventional run-input key, always treated as one alongside the configured `inputKey`. */
|
|
28
|
+
const DEFAULT_INPUT_KEY = 'INPUT';
|
|
29
|
+
/**
|
|
30
|
+
* Content types declared for adopted files. The native client infers nothing from extensions, and
|
|
31
|
+
* neither do we beyond this: a `.json` file is JSON, anything else is bytes for the
|
|
32
|
+
* {@link KeyValueStore} frontend to make sense of.
|
|
33
|
+
*/
|
|
34
|
+
const ADOPTED_JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
35
|
+
const ADOPTED_BINARY_CONTENT_TYPE = 'application/octet-stream';
|
|
27
36
|
/**
|
|
28
37
|
* A file-system storage backend backed by the native `@crawlee/fs-storage-native` Rust extension.
|
|
29
38
|
*
|
|
@@ -39,7 +48,8 @@ export class FileSystemStorageBackend {
|
|
|
39
48
|
requestQueuesDirectory;
|
|
40
49
|
logger;
|
|
41
50
|
requestQueueAccess;
|
|
42
|
-
|
|
51
|
+
/** `INPUT` plus the configured `inputKey`, deduplicated. */
|
|
52
|
+
#inputKeys;
|
|
43
53
|
#keyValueStoreBackendCache = [];
|
|
44
54
|
#datasetBackendCache = [];
|
|
45
55
|
#requestQueueBackendCache = [];
|
|
@@ -47,7 +57,7 @@ export class FileSystemStorageBackend {
|
|
|
47
57
|
const { logger, requestQueueAccess, inputKey, localDataDirectory } = parseArgument(options, fileSystemStorageOptionsSchema);
|
|
48
58
|
this.logger = logger;
|
|
49
59
|
this.requestQueueAccess = requestQueueAccess;
|
|
50
|
-
this.#
|
|
60
|
+
this.#inputKeys = [...new Set([DEFAULT_INPUT_KEY, inputKey])];
|
|
51
61
|
this.localDataDirectory = localDataDirectory;
|
|
52
62
|
this.datasetsDirectory = resolve(this.localDataDirectory, 'datasets');
|
|
53
63
|
this.keyValueStoresDirectory = resolve(this.localDataDirectory, 'key_value_stores');
|
|
@@ -100,13 +110,15 @@ export class FileSystemStorageBackend {
|
|
|
100
110
|
if (found) {
|
|
101
111
|
return found;
|
|
102
112
|
}
|
|
103
|
-
const nativeBackend = await (await importNativeModule()).FileSystemKeyValueStoreClient.open(id, name, alias, this.localDataDirectory
|
|
113
|
+
const nativeBackend = await (await importNativeModule()).FileSystemKeyValueStoreClient.open(id, name, alias, this.localDataDirectory,
|
|
114
|
+
// useTestClock — always real wall-clock outside of native tests.
|
|
115
|
+
undefined, this.#adoptionCandidates(cacheKey === DEFAULT_STORAGE_DIRECTORY));
|
|
104
116
|
const newStore = await KeyValueStoreBackend.create({
|
|
105
117
|
name: alias ? undefined : (name ?? cacheKey),
|
|
106
118
|
cacheKey,
|
|
107
119
|
nativeBackend,
|
|
108
120
|
logger: this.logger,
|
|
109
|
-
|
|
121
|
+
inputKeys: this.#inputKeys,
|
|
110
122
|
});
|
|
111
123
|
this.#keyValueStoreBackendCache.push(newStore);
|
|
112
124
|
return newStore;
|
|
@@ -131,6 +143,38 @@ export class FileSystemStorageBackend {
|
|
|
131
143
|
this.#requestQueueBackendCache.push(newStore);
|
|
132
144
|
return newStore;
|
|
133
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* What the native `open` may turn into records: value files sitting in the store directory with no
|
|
148
|
+
* metadata sidecar, written out-of-band by a CLI, a project template, a v3 Crawlee or a text editor.
|
|
149
|
+
*
|
|
150
|
+
* Each run-input key claims a bare `<key>` or `<key>.json` first — that is the layout the Apify CLI
|
|
151
|
+
* and the templates produce, and the key must end up being `INPUT` rather than `INPUT.json`. Only
|
|
152
|
+
* the default store holds the run input, so elsewhere an `INPUT.json` is just a file named
|
|
153
|
+
* `INPUT.json`, adopted by the trailing sweep like every other one.
|
|
154
|
+
*
|
|
155
|
+
* Once adopted, the file is an ordinary record: readable, listed, deletable, and — in a run-scoped
|
|
156
|
+
* store — purged on start unless its key is a run-input key.
|
|
157
|
+
*/
|
|
158
|
+
#adoptionCandidates(isDefaultStore) {
|
|
159
|
+
const inputCandidates = isDefaultStore
|
|
160
|
+
? this.#inputKeys.map((key) => ({
|
|
161
|
+
key,
|
|
162
|
+
files: [
|
|
163
|
+
{ filename: key, contentType: ADOPTED_BINARY_CONTENT_TYPE },
|
|
164
|
+
{ filename: `${key}.json`, contentType: ADOPTED_JSON_CONTENT_TYPE },
|
|
165
|
+
],
|
|
166
|
+
}))
|
|
167
|
+
: [];
|
|
168
|
+
return [
|
|
169
|
+
...inputCandidates,
|
|
170
|
+
{
|
|
171
|
+
files: [
|
|
172
|
+
{ filename: '*.json', contentType: ADOPTED_JSON_CONTENT_TYPE },
|
|
173
|
+
{ filename: '*', contentType: ADOPTED_BINARY_CONTENT_TYPE },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
}
|
|
134
178
|
async storageExists(id, type) {
|
|
135
179
|
let backends;
|
|
136
180
|
let baseDir;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/fs-storage",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.180",
|
|
4
4
|
"description": "A file-system storage implementation of the Apify API",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@crawlee/fs-storage-native": ">=0.2.1
|
|
46
|
-
"@crawlee/types": "4.0.0-beta.
|
|
47
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
45
|
+
"@crawlee/fs-storage-native": ">=0.2.1 <0.3",
|
|
46
|
+
"@crawlee/types": "4.0.0-beta.180",
|
|
47
|
+
"@crawlee/utils": "4.0.0-beta.180",
|
|
48
48
|
"zod": "^4.5.4"
|
|
49
49
|
},
|
|
50
50
|
"lerna": {
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "4adb7b0a96ee16824ee53d18f437961d9287f977"
|
|
58
58
|
}
|
|
@@ -12,8 +12,11 @@ export interface KeyValueStoreBackendOptions {
|
|
|
12
12
|
cacheKey: string;
|
|
13
13
|
nativeBackend: NativeFileSystemKeyValueStoreBackend;
|
|
14
14
|
logger?: CrawleeLogger;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The run-input keys of this store, deduplicated — `INPUT` plus `FileSystemStorageOptions.inputKey`.
|
|
17
|
+
* All that is left of the run-input special case: the keys {@link purgeExceptInput} spares.
|
|
18
|
+
*/
|
|
19
|
+
inputKeys: string[];
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* A file-system key-value store backend backed by the native `@crawlee/fs-storage-native` Rust
|
|
@@ -36,18 +39,15 @@ export declare class KeyValueStoreBackend extends CachedIdClient implements stor
|
|
|
36
39
|
/**
|
|
37
40
|
* Remove every record from the store except the run input. Used by
|
|
38
41
|
* {@link FileSystemStorageBackend.purge} to clean the default key-value store at the start of a run
|
|
39
|
-
* while preserving the run's input.
|
|
40
|
-
* variant of every input key is listed.
|
|
42
|
+
* while preserving the run's input.
|
|
41
43
|
*/
|
|
42
44
|
purgeExceptInput(): Promise<void>;
|
|
43
45
|
listKeys(options?: storage.KeyValueStoreListKeysOptions): Promise<storage.KeyValueStoreListKeysResult>;
|
|
44
46
|
/**
|
|
45
47
|
* Generates a public `file://` URL for accessing a specific record in the key-value store.
|
|
46
48
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* means nothing to resolve — the requested key is used as-is, and the URL is the one the record will
|
|
50
|
-
* have once written.
|
|
49
|
+
* Existence-agnostic per the {@link KeyValueStoreBackend} contract: the URL points at the file
|
|
50
|
+
* the key is bound to if there is a record, and at the file it would be written to otherwise.
|
|
51
51
|
* @param key The key of the record to generate the public URL for.
|
|
52
52
|
*/
|
|
53
53
|
getPublicUrl(key: string): Promise<string | undefined>;
|
|
@@ -61,10 +61,4 @@ export declare class KeyValueStoreBackend extends CachedIdClient implements stor
|
|
|
61
61
|
getValue(key: string): Promise<storage.KeyValueStoreRecord | undefined>;
|
|
62
62
|
setValue(record: storage.KeyValueStoreInputRecord): Promise<void>;
|
|
63
63
|
deleteValue(key: string): Promise<void>;
|
|
64
|
-
/**
|
|
65
|
-
* Resolve `key` to the on-disk key that actually exists, or `undefined` if nothing does. Run-input
|
|
66
|
-
* keys fall back to bare files, in which case the matched on-disk key is returned so callers like
|
|
67
|
-
* `getPublicUrl` point at the file that exists.
|
|
68
|
-
*/
|
|
69
|
-
private resolveExistingKey;
|
|
70
64
|
}
|
|
@@ -3,21 +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 a run-input lookup misses the tracked record, so a
|
|
8
|
-
* lookup for `INPUT` also matches a hand-placed `INPUT.json`. Passed to the native
|
|
9
|
-
* `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.
|
|
14
|
-
*/
|
|
15
|
-
const BARE_FILE_FALLBACKS = [
|
|
16
|
-
{ extension: '', contentType: '' },
|
|
17
|
-
{ extension: '.json', contentType: 'application/json; charset=utf-8' },
|
|
18
|
-
];
|
|
19
|
-
/** The conventional run-input key, always treated as one alongside the configured `inputKey`. */
|
|
20
|
-
const DEFAULT_INPUT_KEY = 'INPUT';
|
|
21
6
|
const keySchema = z.string();
|
|
22
7
|
const inputRecordShape = z.object({
|
|
23
8
|
key: z.string().min(1),
|
|
@@ -44,20 +29,14 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
44
29
|
name;
|
|
45
30
|
cacheKey;
|
|
46
31
|
#nativeBackend;
|
|
47
|
-
/**
|
|
32
|
+
/** See {@link KeyValueStoreBackendOptions.inputKeys}. */
|
|
48
33
|
#inputKeys;
|
|
49
|
-
/** Bare files the native `listKeys` should surface, under their on-disk name (e.g. `INPUT.json`). */
|
|
50
|
-
#listBareFallbacks;
|
|
51
|
-
/** Bare-file on-disk name (`INPUT.json`) to logical key (`INPUT`). */
|
|
52
|
-
#bareFileLogicalKeys;
|
|
53
34
|
constructor(options) {
|
|
54
35
|
super();
|
|
55
36
|
this.name = options.name;
|
|
56
37
|
this.cacheKey = options.cacheKey;
|
|
57
38
|
this.#nativeBackend = options.nativeBackend;
|
|
58
|
-
this.#inputKeys =
|
|
59
|
-
this.#listBareFallbacks = this.#inputKeys.flatMap((key) => BARE_FILE_FALLBACKS.map(({ extension, contentType }) => ({ name: `${key}${extension}`, contentType })));
|
|
60
|
-
this.#bareFileLogicalKeys = new Map(this.#inputKeys.flatMap((key) => BARE_FILE_FALLBACKS.map(({ extension }) => [`${key}${extension}`, key])));
|
|
39
|
+
this.#inputKeys = options.inputKeys;
|
|
61
40
|
}
|
|
62
41
|
get keyValueStoreDirectory() {
|
|
63
42
|
return this.#nativeBackend.pathToKvs;
|
|
@@ -79,54 +58,27 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
79
58
|
/**
|
|
80
59
|
* Remove every record from the store except the run input. Used by
|
|
81
60
|
* {@link FileSystemStorageBackend.purge} to clean the default key-value store at the start of a run
|
|
82
|
-
* while preserving the run's input.
|
|
83
|
-
* variant of every input key is listed.
|
|
61
|
+
* while preserving the run's input.
|
|
84
62
|
*/
|
|
85
63
|
async purgeExceptInput() {
|
|
86
|
-
|
|
87
|
-
await this.#nativeBackend.purge(keep);
|
|
64
|
+
await this.#nativeBackend.purge(this.#inputKeys);
|
|
88
65
|
}
|
|
89
66
|
async listKeys(options = {}) {
|
|
90
67
|
const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
|
|
91
|
-
// Pass the bare-file fallbacks so out-of-band value files (e.g. a hand-placed `INPUT.json`)
|
|
92
|
-
// are enumerated alongside tracked records, under their actual on-disk name. The native reads
|
|
93
|
-
// everything it needs off the filesystem index — no per-file reads — so this stays cheap.
|
|
94
68
|
// The native `listKeys` already returns a self-describing page (items + pagination cursors)
|
|
95
|
-
// matching the `KeyValueStoreListKeysResult` contract
|
|
96
|
-
|
|
97
|
-
const presentKeys = new Set(page.items.map((record) => record.key));
|
|
98
|
-
// A bare value file is listed under its actual name (`INPUT.json`), which already round-trips
|
|
99
|
-
// through `getValue`/`recordExists`. The only collision is a tracked record occupying the
|
|
100
|
-
// logical key itself (`INPUT`): it shadows the extension-bearing bare variants (`INPUT.json`
|
|
101
|
-
// etc.) for the same logical key, so drop those. The extensionless bare file *is* the logical
|
|
102
|
-
// key, so it is never a separate duplicate.
|
|
103
|
-
const items = page.items.filter((record) => {
|
|
104
|
-
const logicalKey = this.#bareFileLogicalKeys.get(record.key);
|
|
105
|
-
const isExtensionBearingBareFile = logicalKey !== undefined && logicalKey !== record.key;
|
|
106
|
-
return !(isExtensionBearingBareFile && presentKeys.has(logicalKey));
|
|
107
|
-
});
|
|
108
|
-
return {
|
|
109
|
-
items,
|
|
110
|
-
count: items.length,
|
|
111
|
-
limit: page.limit,
|
|
112
|
-
exclusiveStartKey: page.exclusiveStartKey,
|
|
113
|
-
isTruncated: page.isTruncated,
|
|
114
|
-
nextExclusiveStartKey: page.nextExclusiveStartKey,
|
|
115
|
-
};
|
|
69
|
+
// matching the `KeyValueStoreListKeysResult` contract.
|
|
70
|
+
return this.#nativeBackend.listKeys(exclusiveStartKey, limit, prefix);
|
|
116
71
|
}
|
|
117
72
|
/**
|
|
118
73
|
* Generates a public `file://` URL for accessing a specific record in the key-value store.
|
|
119
74
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* means nothing to resolve — the requested key is used as-is, and the URL is the one the record will
|
|
123
|
-
* have once written.
|
|
75
|
+
* Existence-agnostic per the {@link KeyValueStoreBackend} contract: the URL points at the file
|
|
76
|
+
* the key is bound to if there is a record, and at the file it would be written to otherwise.
|
|
124
77
|
* @param key The key of the record to generate the public URL for.
|
|
125
78
|
*/
|
|
126
79
|
async getPublicUrl(key) {
|
|
127
80
|
parseArgument(key, keySchema);
|
|
128
|
-
|
|
129
|
-
return this.#nativeBackend.getPublicUrl(resolvedKey);
|
|
81
|
+
return this.#nativeBackend.getPublicUrl(key);
|
|
130
82
|
}
|
|
131
83
|
/**
|
|
132
84
|
* Tests whether a record with the given key exists without retrieving its value.
|
|
@@ -136,14 +88,11 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
136
88
|
*/
|
|
137
89
|
async recordExists(key) {
|
|
138
90
|
parseArgument(key, keySchema);
|
|
139
|
-
return
|
|
91
|
+
return this.#nativeBackend.recordExists(key);
|
|
140
92
|
}
|
|
141
93
|
async getValue(key) {
|
|
142
94
|
parseArgument(key, keySchema);
|
|
143
|
-
const
|
|
144
|
-
const record = fallbacks
|
|
145
|
-
? await this.#nativeBackend.resolveValue(key, fallbacks)
|
|
146
|
-
: await this.#nativeBackend.getValue(key);
|
|
95
|
+
const record = await this.#nativeBackend.getValue(key);
|
|
147
96
|
if (record) {
|
|
148
97
|
return {
|
|
149
98
|
key: record.key,
|
|
@@ -184,32 +133,4 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
184
133
|
parseArgument(key, keySchema);
|
|
185
134
|
await this.#nativeBackend.deleteValue(key);
|
|
186
135
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Resolve `key` to the on-disk key that actually exists, or `undefined` if nothing does. Run-input
|
|
189
|
-
* keys fall back to bare files, in which case the matched on-disk key is returned so callers like
|
|
190
|
-
* `getPublicUrl` point at the file that exists.
|
|
191
|
-
*/
|
|
192
|
-
async resolveExistingKey(key) {
|
|
193
|
-
const fallbacks = this.#bareFallbacksFor(key);
|
|
194
|
-
if (fallbacks) {
|
|
195
|
-
return ((await this.#nativeBackend.resolveExistingKey(key, fallbacks.map(({ extension }) => extension))) ?? undefined);
|
|
196
|
-
}
|
|
197
|
-
return (await this.#nativeBackend.recordExists(key)) ? key : undefined;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Bare-file fallbacks for `key`, or `undefined` for a plain tracked-record lookup. A logical input
|
|
201
|
-
* key (`INPUT`) probes the whole extension ladder; a literal bare filename (`INPUT.json`, as listed
|
|
202
|
-
* by `listKeys`) probes only itself, so a listed key reads back under its own name.
|
|
203
|
-
*/
|
|
204
|
-
#bareFallbacksFor(key) {
|
|
205
|
-
if (this.#inputKeys.includes(key)) {
|
|
206
|
-
return BARE_FILE_FALLBACKS;
|
|
207
|
-
}
|
|
208
|
-
const logicalKey = this.#bareFileLogicalKeys.get(key);
|
|
209
|
-
if (logicalKey === undefined) {
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
212
|
-
const { contentType } = BARE_FILE_FALLBACKS.find(({ extension }) => `${logicalKey}${extension}` === key);
|
|
213
|
-
return [{ extension: '', contentType }];
|
|
214
|
-
}
|
|
215
136
|
}
|