@crawlee/fs-storage 4.0.0-beta.120 → 4.0.0-beta.122
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.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { opendir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
4
|
+
import { z } from 'zod';
|
|
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
|
+
const fileSystemStorageOptionsSchema = z.object({
|
|
9
|
+
localDataDirectory: z.string(),
|
|
10
|
+
requestQueueAccess: z.enum(['single', 'shared']).default('single'),
|
|
11
|
+
logger: schemas.logger.optional(),
|
|
12
|
+
});
|
|
13
|
+
// The native package throws at load time on platforms without a published binary (e.g.
|
|
14
|
+
// linux musl), and `@crawlee/core` imports this module eagerly via its service locator.
|
|
15
|
+
// Load it lazily so merely importing `@crawlee/fs-storage` stays safe everywhere and the
|
|
16
|
+
// native binding is only loaded when a file-system storage is actually used.
|
|
17
|
+
let nativeModule;
|
|
18
|
+
async function importNativeModule() {
|
|
19
|
+
nativeModule ??= import('@crawlee/fs-storage-native');
|
|
20
|
+
return nativeModule;
|
|
21
|
+
}
|
|
8
22
|
/** The alias `@crawlee/core` opens the default (unnamed) storage under. */
|
|
9
23
|
const DEFAULT_STORAGE_ALIAS = '__default__';
|
|
10
24
|
/** The directory the default storage lives in, one level below `datasets` / `key_value_stores` / etc. */
|
|
@@ -28,13 +42,10 @@ export class FileSystemStorageBackend {
|
|
|
28
42
|
datasetBackendCache = [];
|
|
29
43
|
requestQueueBackendCache = [];
|
|
30
44
|
constructor(options) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.logger = options.logger;
|
|
36
|
-
this.requestQueueAccess = options.requestQueueAccess ?? 'single';
|
|
37
|
-
this.localDataDirectory = options.localDataDirectory;
|
|
45
|
+
const { logger, requestQueueAccess, localDataDirectory } = parseArgument(options, fileSystemStorageOptionsSchema);
|
|
46
|
+
this.logger = logger;
|
|
47
|
+
this.requestQueueAccess = requestQueueAccess;
|
|
48
|
+
this.localDataDirectory = localDataDirectory;
|
|
38
49
|
this.datasetsDirectory = resolve(this.localDataDirectory, 'datasets');
|
|
39
50
|
this.keyValueStoresDirectory = resolve(this.localDataDirectory, 'key_value_stores');
|
|
40
51
|
this.requestQueuesDirectory = resolve(this.localDataDirectory, 'request_queues');
|
|
@@ -68,7 +79,7 @@ export class FileSystemStorageBackend {
|
|
|
68
79
|
if (found) {
|
|
69
80
|
return found;
|
|
70
81
|
}
|
|
71
|
-
const nativeBackend = await
|
|
82
|
+
const nativeBackend = await (await importNativeModule()).FileSystemDatasetClient.open(id, name, alias, this.localDataDirectory);
|
|
72
83
|
const newStore = await DatasetBackend.create({
|
|
73
84
|
name: alias ? undefined : (name ?? cacheKey),
|
|
74
85
|
cacheKey,
|
|
@@ -86,7 +97,7 @@ export class FileSystemStorageBackend {
|
|
|
86
97
|
if (found) {
|
|
87
98
|
return found;
|
|
88
99
|
}
|
|
89
|
-
const nativeBackend = await
|
|
100
|
+
const nativeBackend = await (await importNativeModule()).FileSystemKeyValueStoreClient.open(id, name, alias, this.localDataDirectory);
|
|
90
101
|
const newStore = await KeyValueStoreBackend.create({
|
|
91
102
|
name: alias ? undefined : (name ?? cacheKey),
|
|
92
103
|
cacheKey,
|
|
@@ -104,7 +115,7 @@ export class FileSystemStorageBackend {
|
|
|
104
115
|
if (found) {
|
|
105
116
|
return found;
|
|
106
117
|
}
|
|
107
|
-
const nativeBackend = await
|
|
118
|
+
const nativeBackend = await (await importNativeModule()).FileSystemRequestQueueClient.open(id, name, alias, this.localDataDirectory,
|
|
108
119
|
// useTestClock — always real wall-clock outside of native tests.
|
|
109
120
|
undefined, this.requestQueueAccess);
|
|
110
121
|
const newStore = await RequestQueueBackend.create({
|
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.122",
|
|
4
4
|
"description": "A file-system storage implementation of the Apify API",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -43,8 +43,9 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@crawlee/fs-storage-native": "0.1.5-beta.18",
|
|
46
|
-
"@crawlee/types": "4.0.0-beta.
|
|
47
|
-
"@
|
|
46
|
+
"@crawlee/types": "4.0.0-beta.122",
|
|
47
|
+
"@crawlee/utils": "4.0.0-beta.122",
|
|
48
|
+
"zod": "^4.4.3"
|
|
48
49
|
},
|
|
49
50
|
"lerna": {
|
|
50
51
|
"command": {
|
|
@@ -53,5 +54,5 @@
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
},
|
|
56
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "2c3e87fefdb9e1fca4c144f03167d8524cfdc2e5"
|
|
57
58
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
2
2
|
import { CachedIdClient } from './cached-id-client.js';
|
|
3
3
|
/**
|
|
4
4
|
* `getData` options accepted by the high-level `Dataset` frontend but not supported by the native
|
|
@@ -55,13 +55,7 @@ export class DatasetBackend extends CachedIdClient {
|
|
|
55
55
|
this.#logger?.warning?.(`getData() options [${ignored.join(', ')}] are not supported by the file-system dataset ` +
|
|
56
56
|
`and were ignored. Only "offset", "limit" and "desc" are honored.`);
|
|
57
57
|
}
|
|
58
|
-
const { desc, limit, offset } =
|
|
59
|
-
.object({
|
|
60
|
-
desc: s.boolean().optional(),
|
|
61
|
-
limit: s.number().int().optional(),
|
|
62
|
-
offset: s.number().int().optional(),
|
|
63
|
-
})
|
|
64
|
-
.parse(options);
|
|
58
|
+
const { desc, limit, offset } = parseArgument(options, schemas.datasetListItemsOptions);
|
|
65
59
|
const page = await this.#nativeBackend.getData(offset ?? 0, limit, desc ?? false, false);
|
|
66
60
|
return {
|
|
67
61
|
count: page.count,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
-
import {
|
|
2
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { isStream } from '../utils.js';
|
|
4
5
|
import { CachedIdClient } from './cached-id-client.js';
|
|
5
6
|
/**
|
|
@@ -18,6 +19,20 @@ const BARE_FILE_FALLBACKS = [
|
|
|
18
19
|
{ extension: '.bin', contentType: '' },
|
|
19
20
|
];
|
|
20
21
|
const ALLOWED_BARE_FILES = ['INPUT'];
|
|
22
|
+
const keySchema = z.string();
|
|
23
|
+
const inputRecordShape = z.object({
|
|
24
|
+
key: z.string().min(1),
|
|
25
|
+
value: z.union([
|
|
26
|
+
z.string(),
|
|
27
|
+
z.instanceof(Buffer),
|
|
28
|
+
z.instanceof(ArrayBuffer),
|
|
29
|
+
schemas.typedArray,
|
|
30
|
+
// A stream is an object; this only checks it is a non-null, non-array object
|
|
31
|
+
// (the stream guard in `setValue` does the real check).
|
|
32
|
+
schemas.plainObject,
|
|
33
|
+
]),
|
|
34
|
+
contentType: z.string().min(1).optional(),
|
|
35
|
+
});
|
|
21
36
|
/**
|
|
22
37
|
* The out-of-band ("bare") files to surface from the native `listKeys`, derived from
|
|
23
38
|
* {@link ALLOWED_BARE_FILES} × {@link BARE_FILE_FALLBACKS}. Each native {@link ListBareFallback}
|
|
@@ -82,13 +97,7 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
82
97
|
await this.#nativeBackend.purge(BARE_FILE_FALLBACKS.flatMap(({ extension }) => `INPUT${extension}`));
|
|
83
98
|
}
|
|
84
99
|
async listKeys(options = {}) {
|
|
85
|
-
const { prefix, exclusiveStartKey, limit } =
|
|
86
|
-
.object({
|
|
87
|
-
prefix: s.string().optional(),
|
|
88
|
-
exclusiveStartKey: s.string().optional(),
|
|
89
|
-
limit: s.number().int().greaterThan(0).optional(),
|
|
90
|
-
})
|
|
91
|
-
.parse(options);
|
|
100
|
+
const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
|
|
92
101
|
// Pass the bare-file fallbacks so out-of-band value files (e.g. a hand-placed `INPUT.json`)
|
|
93
102
|
// are enumerated alongside tracked records, under their actual on-disk name. The native reads
|
|
94
103
|
// everything it needs off the filesystem index — no per-file reads — so this stays cheap.
|
|
@@ -122,7 +131,7 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
122
131
|
* @param key The key of the record to generate the public URL for.
|
|
123
132
|
*/
|
|
124
133
|
async getPublicUrl(key) {
|
|
125
|
-
|
|
134
|
+
parseArgument(key, keySchema);
|
|
126
135
|
// The native `getPublicUrl` stats the encoded path but does not probe bare-file extensions,
|
|
127
136
|
// so we resolve the on-disk key first (handling e.g. `INPUT` -> `INPUT.json`) and normalize
|
|
128
137
|
// the native `null`-for-missing result to the historical `undefined` contract.
|
|
@@ -139,11 +148,11 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
139
148
|
* @returns `true` if the record exists, `false` otherwise.
|
|
140
149
|
*/
|
|
141
150
|
async recordExists(key) {
|
|
142
|
-
|
|
151
|
+
parseArgument(key, keySchema);
|
|
143
152
|
return (await this.resolveExistingKey(key)) !== undefined;
|
|
144
153
|
}
|
|
145
154
|
async getValue(key) {
|
|
146
|
-
|
|
155
|
+
parseArgument(key, keySchema);
|
|
147
156
|
const fallbacks = this.bareFallbacksFor(key);
|
|
148
157
|
const record = fallbacks
|
|
149
158
|
? await this.#nativeBackend.resolveValue(key, fallbacks)
|
|
@@ -162,19 +171,7 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
162
171
|
// serialized it: non-bytes become a `string`, everything else is a `Buffer`/typed array or a
|
|
163
172
|
// stream. So we only accept those shapes here — there is no JSON inference or `String(value)`
|
|
164
173
|
// coercion left to do.
|
|
165
|
-
|
|
166
|
-
key: s.string().lengthGreaterThan(0),
|
|
167
|
-
value: s.union([
|
|
168
|
-
s.string(),
|
|
169
|
-
s.instance(Buffer),
|
|
170
|
-
s.instance(ArrayBuffer),
|
|
171
|
-
s.typedArray(),
|
|
172
|
-
// A stream is an object; disabling validation makes shapeshift only check it is a
|
|
173
|
-
// non-null, non-array object (the stream guard below does the real check).
|
|
174
|
-
s.object({}).setValidationEnabled(false),
|
|
175
|
-
]),
|
|
176
|
-
contentType: s.string().lengthGreaterThan(0).optional(),
|
|
177
|
-
}).parse(record);
|
|
174
|
+
parseArgument(record, inputRecordShape);
|
|
178
175
|
const { key, value } = record;
|
|
179
176
|
// The frontend resolves the content type before it reaches the backend; this backend is a plain
|
|
180
177
|
// byte transport and does not infer content types.
|
|
@@ -197,7 +194,7 @@ export class KeyValueStoreBackend extends CachedIdClient {
|
|
|
197
194
|
await this.#nativeBackend.setValue(key, buffer, contentType);
|
|
198
195
|
}
|
|
199
196
|
async deleteValue(key) {
|
|
200
|
-
|
|
197
|
+
parseArgument(key, keySchema);
|
|
201
198
|
await this.#nativeBackend.deleteValue(key);
|
|
202
199
|
}
|
|
203
200
|
/**
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { CachedIdClient } from './cached-id-client.js';
|
|
4
|
+
const uniqueKeySchema = z.string();
|
|
3
5
|
/**
|
|
4
6
|
* Convert a request (either a Crawlee `Request` instance or a plain schema object) into a plain object
|
|
5
7
|
* whose properties are all enumerable.
|
|
@@ -15,21 +17,6 @@ import { CachedIdClient } from './cached-id-client.js';
|
|
|
15
17
|
function plainifyRequest(request) {
|
|
16
18
|
return JSON.parse(JSON.stringify(request));
|
|
17
19
|
}
|
|
18
|
-
const requestShape = s
|
|
19
|
-
.object({
|
|
20
|
-
id: s.string(),
|
|
21
|
-
url: s.string().url({ allowedProtocols: ['http:', 'https:'] }),
|
|
22
|
-
uniqueKey: s.string(),
|
|
23
|
-
method: s.string().optional(),
|
|
24
|
-
retryCount: s.number().int().optional(),
|
|
25
|
-
handledAt: s.union([s.string(), s.date().valid()]).optional(),
|
|
26
|
-
})
|
|
27
|
-
.passthrough();
|
|
28
|
-
const requestShapeWithoutId = requestShape.omit(['id']);
|
|
29
|
-
const batchRequestShapeWithoutId = requestShapeWithoutId.array();
|
|
30
|
-
const requestOptionsShape = s.object({
|
|
31
|
-
forefront: s.boolean().optional(),
|
|
32
|
-
});
|
|
33
20
|
/**
|
|
34
21
|
* A file-system request queue backend backed by the native `@crawlee/fs-storage-native` Rust
|
|
35
22
|
* extension.
|
|
@@ -72,8 +59,8 @@ export class RequestQueueBackend extends CachedIdClient {
|
|
|
72
59
|
await this.#nativeBackend.purge();
|
|
73
60
|
}
|
|
74
61
|
async addBatchOfRequests(requests, options = {}) {
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
parseArgument(requests, schemas.storageRequestBatch);
|
|
63
|
+
parseArgument(options, schemas.requestQueueOperationOptions);
|
|
77
64
|
const response = await this.#nativeBackend.addBatchOfRequests(requests.map((request) => plainifyRequest(request)), options.forefront ?? false);
|
|
78
65
|
// `processedRequests` is structurally identical between the native and `storage` types, so it
|
|
79
66
|
// passes through unchanged. `unprocessedRequests` only differs in that the native `method` is
|
|
@@ -84,7 +71,7 @@ export class RequestQueueBackend extends CachedIdClient {
|
|
|
84
71
|
};
|
|
85
72
|
}
|
|
86
73
|
async getRequest(uniqueKey) {
|
|
87
|
-
|
|
74
|
+
parseArgument(uniqueKey, uniqueKeySchema);
|
|
88
75
|
// The native client tags requests with an internal `orderNo`; it's harmless to leak, so we
|
|
89
76
|
// hand the request back as-is rather than copying it just to drop one undeclared property.
|
|
90
77
|
// The native client already returns `undefined` for a missing request, matching this contract.
|
|
@@ -94,12 +81,12 @@ export class RequestQueueBackend extends CachedIdClient {
|
|
|
94
81
|
return (await this.#nativeBackend.fetchNextRequest());
|
|
95
82
|
}
|
|
96
83
|
async markRequestAsHandled(request) {
|
|
97
|
-
|
|
84
|
+
parseArgument(request, schemas.storageRequest);
|
|
98
85
|
return (await this.#nativeBackend.markRequestAsHandled(plainifyRequest(request))) ?? undefined;
|
|
99
86
|
}
|
|
100
87
|
async reclaimRequest(request, options = {}) {
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
parseArgument(request, schemas.storageRequest);
|
|
89
|
+
parseArgument(options, schemas.requestQueueOperationOptions);
|
|
103
90
|
return ((await this.#nativeBackend.reclaimRequest(plainifyRequest(request), options.forefront ?? false)) ??
|
|
104
91
|
undefined);
|
|
105
92
|
}
|