@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.
@@ -1,10 +1,24 @@
1
1
  import { opendir, readFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
- import { s } from '@sapphire/shapeshift';
4
- import { FileSystemDatasetClient as NativeDatasetBackend, FileSystemKeyValueStoreClient as NativeKeyValueStoreBackend, FileSystemRequestQueueClient as NativeRequestQueueBackend, } from '@crawlee/fs-storage-native';
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
- s.object({
32
- localDataDirectory: s.string(),
33
- requestQueueAccess: s.enum(['single', 'shared']).optional(),
34
- }).parse(options);
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 NativeDatasetBackend.open(id, name, alias, this.localDataDirectory);
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 NativeKeyValueStoreBackend.open(id, name, alias, this.localDataDirectory);
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 NativeRequestQueueBackend.open(id, name, alias, this.localDataDirectory,
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.120",
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.120",
47
- "@sapphire/shapeshift": "^4.0.0"
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": "da5d427c4c1c9dcaea95b151e9c4c7310100885d"
57
+ "gitHead": "2c3e87fefdb9e1fca4c144f03167d8524cfdc2e5"
57
58
  }
@@ -1,4 +1,4 @@
1
- import { s } from '@sapphire/shapeshift';
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 } = s
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 { s } from '@sapphire/shapeshift';
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 } = s
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
- s.string().parse(key);
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
- s.string().parse(key);
151
+ parseArgument(key, keySchema);
143
152
  return (await this.resolveExistingKey(key)) !== undefined;
144
153
  }
145
154
  async getValue(key) {
146
- s.string().parse(key);
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
- s.object({
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
- s.string().parse(key);
197
+ parseArgument(key, keySchema);
201
198
  await this.#nativeBackend.deleteValue(key);
202
199
  }
203
200
  /**
@@ -1,5 +1,7 @@
1
- import { s } from '@sapphire/shapeshift';
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
- batchRequestShapeWithoutId.parse(requests);
76
- requestOptionsShape.parse(options);
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
- s.string().parse(uniqueKey);
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
- requestShape.parse(request);
84
+ parseArgument(request, schemas.storageRequest);
98
85
  return (await this.#nativeBackend.markRequestAsHandled(plainifyRequest(request))) ?? undefined;
99
86
  }
100
87
  async reclaimRequest(request, options = {}) {
101
- requestShape.parse(request);
102
- requestOptionsShape.parse(options);
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
  }