@crawlee/fs-storage 4.0.0-beta.118 → 4.0.0-beta.119
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 +3 -16
- package/file-system-storage.js +106 -60
- package/package.json +3 -3
package/file-system-storage.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface FileSystemStorageOptions {
|
|
|
38
38
|
* `teardown` can operate over them), and exposing them through the `@crawlee/types` interfaces.
|
|
39
39
|
*/
|
|
40
40
|
export declare class FileSystemStorageBackend implements storage.StorageBackend {
|
|
41
|
+
#private;
|
|
41
42
|
readonly localDataDirectory: string;
|
|
42
43
|
readonly datasetsDirectory: string;
|
|
43
44
|
readonly keyValueStoresDirectory: string;
|
|
@@ -54,27 +55,13 @@ export declare class FileSystemStorageBackend implements storage.StorageBackend
|
|
|
54
55
|
* partitions, by including the storage directory in the cache key.
|
|
55
56
|
*/
|
|
56
57
|
getStorageBackendCacheKey(): string;
|
|
57
|
-
private static resolveStorageKey;
|
|
58
58
|
createDatasetBackend(options?: storage.StorageIdentifier): Promise<storage.DatasetBackend>;
|
|
59
59
|
createKeyValueStoreBackend(options?: storage.StorageIdentifier): Promise<storage.KeyValueStoreBackend>;
|
|
60
60
|
createRequestQueueBackend(options?: storage.StorageIdentifier): Promise<storage.RequestQueueBackend>;
|
|
61
61
|
storageExists(id: string, type: 'Dataset' | 'KeyValueStore' | 'RequestQueue'): Promise<boolean>;
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* `__metadata__.json`; the directory itself is named after the storage's `name ?? id`. So this
|
|
66
|
-
* first tries the directory named exactly `entryNameOrId` (reading its metadata id), then falls
|
|
67
|
-
* back to scanning sibling directories for one whose metadata id equals `entryNameOrId` (the case
|
|
68
|
-
* of a storage opened by name and later looked up by its auto-assigned id).
|
|
69
|
-
*/
|
|
70
|
-
private static resolveStorageIdOnDisk;
|
|
71
|
-
/** Read the `id` field from a storage directory's `__metadata__.json`, or `undefined` if absent. */
|
|
72
|
-
private static readMetadataId;
|
|
73
|
-
/**
|
|
74
|
-
* Cleans up the default storages before the run starts:
|
|
75
|
-
* - the default dataset;
|
|
76
|
-
* - all records from the default key-value store, except for the "INPUT" key;
|
|
77
|
-
* - the default request queue.
|
|
63
|
+
* Cleans up the run-scoped storages before the run starts, sweeping the storage directories so that
|
|
64
|
+
* leftovers from a previous process are caught too.
|
|
78
65
|
*/
|
|
79
66
|
purge(): Promise<void>;
|
|
80
67
|
/**
|
package/file-system-storage.js
CHANGED
|
@@ -5,6 +5,10 @@ import { FileSystemDatasetClient as NativeDatasetBackend, FileSystemKeyValueStor
|
|
|
5
5
|
import { DatasetBackend } from './resource-clients/dataset.js';
|
|
6
6
|
import { KeyValueStoreBackend } from './resource-clients/key-value-store.js';
|
|
7
7
|
import { RequestQueueBackend } from './resource-clients/request-queue.js';
|
|
8
|
+
/** The alias `@crawlee/core` opens the default (unnamed) storage under. */
|
|
9
|
+
const DEFAULT_STORAGE_ALIAS = '__default__';
|
|
10
|
+
/** The directory the default storage lives in, one level below `datasets` / `key_value_stores` / etc. */
|
|
11
|
+
const DEFAULT_STORAGE_DIRECTORY = 'default';
|
|
8
12
|
/**
|
|
9
13
|
* A file-system storage backend backed by the native `@crawlee/fs-storage-native` Rust extension.
|
|
10
14
|
*
|
|
@@ -43,27 +47,31 @@ export class FileSystemStorageBackend {
|
|
|
43
47
|
getStorageBackendCacheKey() {
|
|
44
48
|
return `FileSystemStorageBackend:${resolve(this.localDataDirectory)}`;
|
|
45
49
|
}
|
|
46
|
-
static resolveStorageKey(options) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
50
|
+
static #resolveStorageKey(options) {
|
|
51
|
+
// No identifier at all means the default storage, which is opened under the reserved alias —
|
|
52
|
+
// same rule as `resolveStorageIdentifier` in @crawlee/core, so that a backend used directly
|
|
53
|
+
// lands on the very storage the frontends would have opened.
|
|
54
|
+
const requestedAlias = options.alias || (!options.id && !options.name ? DEFAULT_STORAGE_ALIAS : undefined);
|
|
55
|
+
// `__default__` is an internal sentinel and must not escape onto disk: the default storage lives
|
|
56
|
+
// in `default`, which is what the docs, the project templates and every pre-existing local
|
|
57
|
+
// `storage/` directory expect. Normalizing here keeps the cache key and the directory in step.
|
|
58
|
+
const alias = requestedAlias === DEFAULT_STORAGE_ALIAS ? DEFAULT_STORAGE_DIRECTORY : requestedAlias;
|
|
59
|
+
// `alias` covers the identifier-less case, so one of the three is always set.
|
|
60
|
+
const cacheKey = alias ?? options.name ?? options.id;
|
|
61
|
+
return { id: options.id, name: options.name, alias, cacheKey };
|
|
52
62
|
}
|
|
53
63
|
async createDatasetBackend(options = {}) {
|
|
54
|
-
const { id, name, alias, cacheKey } = FileSystemStorageBackend
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return found;
|
|
61
|
-
}
|
|
64
|
+
const { id, name, alias, cacheKey } = FileSystemStorageBackend.#resolveStorageKey(options);
|
|
65
|
+
const found = this.datasetBackendCache.find((store) => store.id === cacheKey ||
|
|
66
|
+
store.name?.toLowerCase() === cacheKey.toLowerCase() ||
|
|
67
|
+
store.cacheKey.toLowerCase() === cacheKey.toLowerCase());
|
|
68
|
+
if (found) {
|
|
69
|
+
return found;
|
|
62
70
|
}
|
|
63
71
|
const nativeBackend = await NativeDatasetBackend.open(id, name, alias, this.localDataDirectory);
|
|
64
72
|
const newStore = await DatasetBackend.create({
|
|
65
73
|
name: alias ? undefined : (name ?? cacheKey),
|
|
66
|
-
cacheKey
|
|
74
|
+
cacheKey,
|
|
67
75
|
nativeBackend,
|
|
68
76
|
logger: this.logger,
|
|
69
77
|
});
|
|
@@ -71,19 +79,17 @@ export class FileSystemStorageBackend {
|
|
|
71
79
|
return newStore;
|
|
72
80
|
}
|
|
73
81
|
async createKeyValueStoreBackend(options = {}) {
|
|
74
|
-
const { id, name, alias, cacheKey } = FileSystemStorageBackend
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return found;
|
|
81
|
-
}
|
|
82
|
+
const { id, name, alias, cacheKey } = FileSystemStorageBackend.#resolveStorageKey(options);
|
|
83
|
+
const found = this.keyValueStoreBackendCache.find((store) => store.id === cacheKey ||
|
|
84
|
+
store.name?.toLowerCase() === cacheKey.toLowerCase() ||
|
|
85
|
+
store.cacheKey.toLowerCase() === cacheKey.toLowerCase());
|
|
86
|
+
if (found) {
|
|
87
|
+
return found;
|
|
82
88
|
}
|
|
83
89
|
const nativeBackend = await NativeKeyValueStoreBackend.open(id, name, alias, this.localDataDirectory);
|
|
84
90
|
const newStore = await KeyValueStoreBackend.create({
|
|
85
91
|
name: alias ? undefined : (name ?? cacheKey),
|
|
86
|
-
cacheKey
|
|
92
|
+
cacheKey,
|
|
87
93
|
nativeBackend,
|
|
88
94
|
logger: this.logger,
|
|
89
95
|
});
|
|
@@ -91,21 +97,19 @@ export class FileSystemStorageBackend {
|
|
|
91
97
|
return newStore;
|
|
92
98
|
}
|
|
93
99
|
async createRequestQueueBackend(options = {}) {
|
|
94
|
-
const { id, name, alias, cacheKey } = FileSystemStorageBackend
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return found;
|
|
101
|
-
}
|
|
100
|
+
const { id, name, alias, cacheKey } = FileSystemStorageBackend.#resolveStorageKey(options);
|
|
101
|
+
const found = this.requestQueueBackendCache.find((queue) => queue.id === cacheKey ||
|
|
102
|
+
queue.name?.toLowerCase() === cacheKey.toLowerCase() ||
|
|
103
|
+
queue.cacheKey.toLowerCase() === cacheKey.toLowerCase());
|
|
104
|
+
if (found) {
|
|
105
|
+
return found;
|
|
102
106
|
}
|
|
103
107
|
const nativeBackend = await NativeRequestQueueBackend.open(id, name, alias, this.localDataDirectory,
|
|
104
108
|
// useTestClock — always real wall-clock outside of native tests.
|
|
105
109
|
undefined, this.requestQueueAccess);
|
|
106
110
|
const newStore = await RequestQueueBackend.create({
|
|
107
111
|
name: alias ? undefined : (name ?? cacheKey),
|
|
108
|
-
cacheKey
|
|
112
|
+
cacheKey,
|
|
109
113
|
nativeBackend,
|
|
110
114
|
logger: this.logger,
|
|
111
115
|
});
|
|
@@ -144,7 +148,7 @@ export class FileSystemStorageBackend {
|
|
|
144
148
|
// has a matching directory. We therefore read the real id from the metadata and only report
|
|
145
149
|
// existence when it equals the queried string. This matches upstream PR #3800/#3808 and
|
|
146
150
|
// prevents a named storage from being re-resolved as `{ id: name }` on a subsequent run.
|
|
147
|
-
const resolvedId = await FileSystemStorageBackend
|
|
151
|
+
const resolvedId = await FileSystemStorageBackend.#resolveStorageIdOnDisk(baseDir, id);
|
|
148
152
|
return resolvedId === id;
|
|
149
153
|
}
|
|
150
154
|
/**
|
|
@@ -155,10 +159,10 @@ export class FileSystemStorageBackend {
|
|
|
155
159
|
* back to scanning sibling directories for one whose metadata id equals `entryNameOrId` (the case
|
|
156
160
|
* of a storage opened by name and later looked up by its auto-assigned id).
|
|
157
161
|
*/
|
|
158
|
-
static async resolveStorageIdOnDisk(baseDirectory, entryNameOrId) {
|
|
162
|
+
static async #resolveStorageIdOnDisk(baseDirectory, entryNameOrId) {
|
|
159
163
|
// Directory named exactly after the string: return its real (metadata) id, which may differ
|
|
160
164
|
// from the string when the string is a name rather than an id.
|
|
161
|
-
const directId = await FileSystemStorageBackend
|
|
165
|
+
const directId = (await FileSystemStorageBackend.#readMetadata(resolve(baseDirectory, entryNameOrId)))?.id;
|
|
162
166
|
if (directId !== undefined) {
|
|
163
167
|
return directId;
|
|
164
168
|
}
|
|
@@ -174,49 +178,91 @@ export class FileSystemStorageBackend {
|
|
|
174
178
|
if (!directory.isDirectory()) {
|
|
175
179
|
continue;
|
|
176
180
|
}
|
|
177
|
-
const metadataId = await FileSystemStorageBackend
|
|
181
|
+
const metadataId = (await FileSystemStorageBackend.#readMetadata(resolve(baseDirectory, directory.name)))
|
|
182
|
+
?.id;
|
|
178
183
|
if (metadataId === entryNameOrId) {
|
|
179
184
|
return metadataId;
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
return undefined;
|
|
183
188
|
}
|
|
184
|
-
/** Read
|
|
185
|
-
static async
|
|
189
|
+
/** Read a storage directory's `__metadata__.json`, or `undefined` if there is none to read. */
|
|
190
|
+
static async #readMetadata(storageDirectory) {
|
|
186
191
|
try {
|
|
187
192
|
const fileContent = await readFile(resolve(storageDirectory, '__metadata__.json'), 'utf8');
|
|
188
|
-
return JSON.parse(fileContent)
|
|
193
|
+
return JSON.parse(fileContent);
|
|
189
194
|
}
|
|
190
195
|
catch {
|
|
191
|
-
// Directory missing, or no/unreadable metadata file —
|
|
196
|
+
// Directory missing, or no/unreadable metadata file — nothing to report.
|
|
192
197
|
return undefined;
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
/**
|
|
196
|
-
* Cleans up the
|
|
197
|
-
*
|
|
198
|
-
* - all records from the default key-value store, except for the "INPUT" key;
|
|
199
|
-
* - the default request queue.
|
|
201
|
+
* Cleans up the run-scoped storages before the run starts, sweeping the storage directories so that
|
|
202
|
+
* leftovers from a previous process are caught too.
|
|
200
203
|
*/
|
|
201
204
|
async purge() {
|
|
202
|
-
// Resolve the default stores up front so leftover on-disk records are purged even when the
|
|
203
|
-
// store has not been opened in this process yet (e.g. a fresh run over a pre-existing
|
|
204
|
-
// directory). Opening caches the backend, so the subsequent purge operates on a real backend.
|
|
205
|
-
// The default store is opened via the internal `__default__` alias (see resolveStorageIdentifier
|
|
206
|
-
// in @crawlee/core), which resolves to the `default` cache key — match that here so we purge the
|
|
207
|
-
// very backend the default open would return rather than creating a divergent one.
|
|
208
|
-
const [defaultKeyValueStore, defaultDataset, defaultRequestQueue] = await Promise.all([
|
|
209
|
-
this.createKeyValueStoreBackend({ alias: '__default__' }),
|
|
210
|
-
this.createDatasetBackend({ alias: '__default__' }),
|
|
211
|
-
this.createRequestQueueBackend({ alias: '__default__' }),
|
|
212
|
-
]);
|
|
213
205
|
await Promise.all([
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
206
|
+
this.#purgeRunScopedStorages(this.keyValueStoresDirectory, async (alias) => this.createKeyValueStoreBackend({ alias }),
|
|
207
|
+
// Only the default store holds the run input, so it is the only one that keeps `INPUT`.
|
|
208
|
+
async (store, isDefault) => (isDefault ? store.purgeExceptInput() : store.purge())),
|
|
209
|
+
this.#purgeRunScopedStorages(this.datasetsDirectory, async (alias) => this.createDatasetBackend({ alias }), async (store) => store.purge()),
|
|
210
|
+
this.#purgeRunScopedStorages(this.requestQueuesDirectory, async (alias) => this.createRequestQueueBackend({ alias }), async (store) => store.purge()),
|
|
218
211
|
]);
|
|
219
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Purge every run-scoped storage under `storagesDirectory`, whether or not it has been opened in this
|
|
215
|
+
* process yet. Storages are opened rather than emptied on disk directly, so that one already open
|
|
216
|
+
* under the same name or alias is purged through the backend the run is using, not a second one.
|
|
217
|
+
*/
|
|
218
|
+
async #purgeRunScopedStorages(storagesDirectory, open, purgeStorage) {
|
|
219
|
+
// The default storage is listed unconditionally, so that a run over an empty directory still ends
|
|
220
|
+
// up with it opened (and cached) exactly as it was before. Deduplicating by cache key then keeps
|
|
221
|
+
// it to a single open: every run after the first also finds its `default` directory on disk, and
|
|
222
|
+
// opening the same storage twice concurrently would race two backends onto one directory.
|
|
223
|
+
const aliasesByCacheKey = new Map();
|
|
224
|
+
for (const alias of [
|
|
225
|
+
DEFAULT_STORAGE_ALIAS,
|
|
226
|
+
...(await FileSystemStorageBackend.#listUnnamedStorages(storagesDirectory)),
|
|
227
|
+
]) {
|
|
228
|
+
const { cacheKey } = FileSystemStorageBackend.#resolveStorageKey({ alias });
|
|
229
|
+
if (!aliasesByCacheKey.has(cacheKey)) {
|
|
230
|
+
aliasesByCacheKey.set(cacheKey, alias);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
await Promise.all(Array.from(aliasesByCacheKey, async ([cacheKey, alias]) => {
|
|
234
|
+
await purgeStorage(await open(alias), cacheKey === DEFAULT_STORAGE_DIRECTORY);
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The directory names of the on-disk storages under `storagesDirectory` that Crawlee created without
|
|
239
|
+
* a name — the default storage and every alias-keyed one. Since the directory is named after the
|
|
240
|
+
* storage's `name ?? alias`, the name is read from the metadata rather than guessed.
|
|
241
|
+
*
|
|
242
|
+
* Two kinds of directory are left out, as purging them would destroy data this process never wrote:
|
|
243
|
+
* one without a readable `__metadata__.json` (not written by Crawlee — a hand-placed input directory,
|
|
244
|
+
* say), and one named after its own id, which is reachable only by `{ id }` and so is not run-scoped.
|
|
245
|
+
*/
|
|
246
|
+
static async #listUnnamedStorages(storagesDirectory) {
|
|
247
|
+
let directories;
|
|
248
|
+
try {
|
|
249
|
+
directories = await opendir(storagesDirectory);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
const unnamed = [];
|
|
255
|
+
for await (const directory of directories) {
|
|
256
|
+
if (!directory.isDirectory()) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const metadata = await FileSystemStorageBackend.#readMetadata(resolve(storagesDirectory, directory.name));
|
|
260
|
+
if (metadata !== undefined && typeof metadata.name !== 'string' && metadata.id !== directory.name) {
|
|
261
|
+
unnamed.push(directory.name);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return unnamed;
|
|
265
|
+
}
|
|
220
266
|
/**
|
|
221
267
|
* This method should be called at the end of the process, to ensure all data is saved.
|
|
222
268
|
*
|
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.119",
|
|
4
4
|
"description": "A file-system storage implementation of the Apify API",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@crawlee/fs-storage-native": "0.1.5-beta.18",
|
|
46
|
-
"@crawlee/types": "4.0.0-beta.
|
|
46
|
+
"@crawlee/types": "4.0.0-beta.119",
|
|
47
47
|
"@sapphire/shapeshift": "^4.0.0"
|
|
48
48
|
},
|
|
49
49
|
"lerna": {
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "552fe2371a3c6e9e9f3514010536ea6abdc27eb4"
|
|
57
57
|
}
|