@crawlee/fs-storage 4.0.0-beta.170 → 4.0.0-beta.172

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.
@@ -25,6 +25,18 @@ export interface FileSystemStorageOptions {
25
25
  * @default 'single'
26
26
  */
27
27
  requestQueueAccess?: 'single' | 'shared';
28
+ /**
29
+ * The key the run input is read from — Crawlee's `inputKey` (`CRAWLEE_INPUT_KEY`).
30
+ *
31
+ * Like the conventional `INPUT`, this key may live in the default key-value store as a bare value
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). It is therefore readable out-of-band (`<key>`, `<key>.json`,
34
+ * `<key>.txt`, `<key>.bin`) and preserved when the default store is purged, exactly like `INPUT`,
35
+ * which is always kept regardless of this setting.
36
+ *
37
+ * @default 'INPUT'
38
+ */
39
+ inputKey?: string;
28
40
  }
29
41
  /**
30
42
  * A file-system storage backend backed by the native `@crawlee/fs-storage-native` Rust extension.
@@ -8,6 +8,7 @@ import { RequestQueueBackend } from './resource-clients/request-queue.js';
8
8
  const fileSystemStorageOptionsSchema = z.object({
9
9
  localDataDirectory: z.string(),
10
10
  requestQueueAccess: z.enum(['single', 'shared']).default('single'),
11
+ inputKey: z.string().min(1).default('INPUT'),
11
12
  logger: schemas.logger.optional(),
12
13
  });
13
14
  // The native package throws at load time on platforms without a published binary (e.g.
@@ -38,13 +39,15 @@ export class FileSystemStorageBackend {
38
39
  requestQueuesDirectory;
39
40
  logger;
40
41
  requestQueueAccess;
42
+ #inputKey;
41
43
  #keyValueStoreBackendCache = [];
42
44
  #datasetBackendCache = [];
43
45
  #requestQueueBackendCache = [];
44
46
  constructor(options) {
45
- const { logger, requestQueueAccess, localDataDirectory } = parseArgument(options, fileSystemStorageOptionsSchema);
47
+ const { logger, requestQueueAccess, inputKey, localDataDirectory } = parseArgument(options, fileSystemStorageOptionsSchema);
46
48
  this.logger = logger;
47
49
  this.requestQueueAccess = requestQueueAccess;
50
+ this.#inputKey = inputKey;
48
51
  this.localDataDirectory = localDataDirectory;
49
52
  this.datasetsDirectory = resolve(this.localDataDirectory, 'datasets');
50
53
  this.keyValueStoresDirectory = resolve(this.localDataDirectory, 'key_value_stores');
@@ -103,6 +106,7 @@ export class FileSystemStorageBackend {
103
106
  cacheKey,
104
107
  nativeBackend,
105
108
  logger: this.logger,
109
+ inputKey: this.#inputKey,
106
110
  });
107
111
  this.#keyValueStoreBackendCache.push(newStore);
108
112
  return newStore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/fs-storage",
3
- "version": "4.0.0-beta.170",
3
+ "version": "4.0.0-beta.172",
4
4
  "description": "A file-system storage implementation of the Apify API",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@crawlee/fs-storage-native": ">=0.2.1-beta.0 <0.3",
46
- "@crawlee/types": "4.0.0-beta.170",
47
- "@crawlee/utils": "4.0.0-beta.170",
46
+ "@crawlee/types": "4.0.0-beta.172",
47
+ "@crawlee/utils": "4.0.0-beta.172",
48
48
  "zod": "^4.5.4"
49
49
  },
50
50
  "lerna": {
@@ -54,5 +54,5 @@
54
54
  }
55
55
  }
56
56
  },
57
- "gitHead": "86e1c891da983125e9e18f1a2327fbcf5e0f5fc2"
57
+ "gitHead": "619809c59ea633121f3aa9c8f333efeb9c6167af"
58
58
  }
@@ -12,6 +12,8 @@ export interface KeyValueStoreBackendOptions {
12
12
  cacheKey: string;
13
13
  nativeBackend: NativeFileSystemKeyValueStoreBackend;
14
14
  logger?: CrawleeLogger;
15
+ /** The configured run-input key, see `FileSystemStorageOptions.inputKey`. Treated like `INPUT`. */
16
+ inputKey?: string;
15
17
  }
16
18
  /**
17
19
  * A file-system key-value store backend backed by the native `@crawlee/fs-storage-native` Rust
@@ -34,10 +36,8 @@ export declare class KeyValueStoreBackend extends CachedIdClient implements stor
34
36
  /**
35
37
  * Remove every record from the store except the run input. Used by
36
38
  * {@link FileSystemStorageBackend.purge} to clean the default key-value store at the start of a run
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`).
39
+ * while preserving the run's input. The native keep-list matches exact filenames, so every extension
40
+ * variant of every input key is listed.
41
41
  */
42
42
  purgeExceptInput(): Promise<void>;
43
43
  listKeys(options?: storage.KeyValueStoreListKeysOptions): Promise<storage.KeyValueStoreListKeysResult>;
@@ -62,23 +62,9 @@ export declare class KeyValueStoreBackend extends CachedIdClient implements stor
62
62
  setValue(record: storage.KeyValueStoreInputRecord): Promise<void>;
63
63
  deleteValue(key: string): Promise<void>;
64
64
  /**
65
- * Resolve `key` to the on-disk key that actually exists, or `undefined` if nothing does. Every
66
- * key is checked against its tracked record; the run-input keys additionally fall back to
67
- * out-of-band bare files, in which case the matched on-disk key is returned so callers like
68
- * `getPublicUrl` point at the file that exists. Two run-input shapes are handled (see
69
- * {@link bareFallbacksFor}): the logical `INPUT`, which probes the conventional extensions, and a
70
- * literal bare filename such as `INPUT.json` as listed by `listKeys`, which resolves itself.
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.
71
68
  */
72
69
  private resolveExistingKey;
73
- /**
74
- * The native `resolveValue`/`resolveExistingKey` bare-file fallbacks to use for `key`, or
75
- * `undefined` if `key` is a plain tracked-record lookup with no bare-file probing.
76
- *
77
- * - The logical run-input key (`INPUT`) probes the full extension ladder (`INPUT`, `INPUT.json`,
78
- * `INPUT.txt`, `INPUT.bin`), matching how Crawlee reads run input.
79
- * - A literal bare filename as surfaced by `listKeys` (`INPUT.json`/`.txt`/`.bin`) resolves itself:
80
- * the tracked record first, then the bare file at that exact name (a single empty-extension
81
- * fallback), so a listed key round-trips through `getValue`/`recordExists`.
82
- */
83
- private bareFallbacksFor;
84
70
  }
@@ -4,9 +4,9 @@ import { z } from 'zod';
4
4
  import { isStream } from '../utils.js';
5
5
  import { CachedIdClient } from './cached-id-client.js';
6
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.
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`/`.txt`/`.bin`. Passed to the native
9
+ * `resolveValue`/`resolveExistingKey`, which do the probing and re-keying.
10
10
  *
11
11
  * Each entry declares the content type to report on a match — the native client does no MIME
12
12
  * inference. An empty `contentType` is its sentinel for "keep the synthesized
@@ -18,7 +18,8 @@ const BARE_FILE_FALLBACKS = [
18
18
  { extension: '.txt', contentType: 'text/plain; charset=utf-8' },
19
19
  { extension: '.bin', contentType: '' },
20
20
  ];
21
- const ALLOWED_BARE_FILES = ['INPUT'];
21
+ /** The conventional run-input key, always treated as one alongside the configured `inputKey`. */
22
+ const DEFAULT_INPUT_KEY = 'INPUT';
22
23
  const keySchema = z.string();
23
24
  const inputRecordShape = z.object({
24
25
  key: z.string().min(1),
@@ -33,23 +34,6 @@ const inputRecordShape = z.object({
33
34
  ]),
34
35
  contentType: z.string().min(1).optional(),
35
36
  });
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
37
  /**
54
38
  * A file-system key-value store backend backed by the native `@crawlee/fs-storage-native` Rust
55
39
  * extension.
@@ -62,11 +46,20 @@ export class KeyValueStoreBackend extends CachedIdClient {
62
46
  name;
63
47
  cacheKey;
64
48
  #nativeBackend;
49
+ /** `INPUT` plus the configured `inputKey`, deduplicated. */
50
+ #inputKeys;
51
+ /** Bare files the native `listKeys` should surface, under their on-disk name (e.g. `INPUT.json`). */
52
+ #listBareFallbacks;
53
+ /** Bare-file on-disk name (`INPUT.json`) to logical key (`INPUT`). */
54
+ #bareFileLogicalKeys;
65
55
  constructor(options) {
66
56
  super();
67
57
  this.name = options.name;
68
58
  this.cacheKey = options.cacheKey;
69
59
  this.#nativeBackend = options.nativeBackend;
60
+ this.#inputKeys = [...new Set([DEFAULT_INPUT_KEY, options.inputKey ?? DEFAULT_INPUT_KEY])];
61
+ this.#listBareFallbacks = this.#inputKeys.flatMap((key) => BARE_FILE_FALLBACKS.map(({ extension, contentType }) => ({ name: `${key}${extension}`, contentType })));
62
+ this.#bareFileLogicalKeys = new Map(this.#inputKeys.flatMap((key) => BARE_FILE_FALLBACKS.map(({ extension }) => [`${key}${extension}`, key])));
70
63
  }
71
64
  get keyValueStoreDirectory() {
72
65
  return this.#nativeBackend.pathToKvs;
@@ -88,13 +81,12 @@ export class KeyValueStoreBackend extends CachedIdClient {
88
81
  /**
89
82
  * Remove every record from the store except the run input. Used by
90
83
  * {@link FileSystemStorageBackend.purge} to clean the default key-value store at the start of a run
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`).
84
+ * while preserving the run's input. The native keep-list matches exact filenames, so every extension
85
+ * variant of every input key is listed.
95
86
  */
96
87
  async purgeExceptInput() {
97
- await this.#nativeBackend.purge(BARE_FILE_FALLBACKS.flatMap(({ extension }) => `INPUT${extension}`));
88
+ const keep = this.#inputKeys.flatMap((key) => BARE_FILE_FALLBACKS.map(({ extension }) => `${key}${extension}`));
89
+ await this.#nativeBackend.purge(keep);
98
90
  }
99
91
  async listKeys(options = {}) {
100
92
  const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
@@ -103,7 +95,7 @@ export class KeyValueStoreBackend extends CachedIdClient {
103
95
  // everything it needs off the filesystem index — no per-file reads — so this stays cheap.
104
96
  // The native `listKeys` already returns a self-describing page (items + pagination cursors)
105
97
  // matching the `KeyValueStoreListKeysResult` contract, so we only post-process the items.
106
- const page = await this.#nativeBackend.listKeys(exclusiveStartKey, limit, prefix, LIST_BARE_FALLBACKS);
98
+ const page = await this.#nativeBackend.listKeys(exclusiveStartKey, limit, prefix, this.#listBareFallbacks);
107
99
  const presentKeys = new Set(page.items.map((record) => record.key));
108
100
  // A bare value file is listed under its actual name (`INPUT.json`), which already round-trips
109
101
  // through `getValue`/`recordExists`. The only collision is a tracked record occupying the
@@ -111,7 +103,7 @@ export class KeyValueStoreBackend extends CachedIdClient {
111
103
  // etc.) for the same logical key, so drop those. The extensionless bare file *is* the logical
112
104
  // key, so it is never a separate duplicate.
113
105
  const items = page.items.filter((record) => {
114
- const logicalKey = BARE_FILE_LOGICAL_KEYS.get(record.key);
106
+ const logicalKey = this.#bareFileLogicalKeys.get(record.key);
115
107
  const isExtensionBearingBareFile = logicalKey !== undefined && logicalKey !== record.key;
116
108
  return !(isExtensionBearingBareFile && presentKeys.has(logicalKey));
117
109
  });
@@ -150,7 +142,7 @@ export class KeyValueStoreBackend extends CachedIdClient {
150
142
  }
151
143
  async getValue(key) {
152
144
  parseArgument(key, keySchema);
153
- const fallbacks = this.bareFallbacksFor(key);
145
+ const fallbacks = this.#bareFallbacksFor(key);
154
146
  const record = fallbacks
155
147
  ? await this.#nativeBackend.resolveValue(key, fallbacks)
156
148
  : await this.#nativeBackend.getValue(key);
@@ -195,39 +187,31 @@ export class KeyValueStoreBackend extends CachedIdClient {
195
187
  await this.#nativeBackend.deleteValue(key);
196
188
  }
197
189
  /**
198
- * Resolve `key` to the on-disk key that actually exists, or `undefined` if nothing does. Every
199
- * key is checked against its tracked record; the run-input keys additionally fall back to
200
- * out-of-band bare files, in which case the matched on-disk key is returned so callers like
201
- * `getPublicUrl` point at the file that exists. Two run-input shapes are handled (see
202
- * {@link bareFallbacksFor}): the logical `INPUT`, which probes the conventional extensions, and a
203
- * literal bare filename such as `INPUT.json` as listed by `listKeys`, which resolves itself.
190
+ * Resolve `key` to the on-disk key that actually exists, or `undefined` if nothing does. Run-input
191
+ * keys fall back to bare files, in which case the matched on-disk key is returned so callers like
192
+ * `getPublicUrl` point at the file that exists.
204
193
  */
205
194
  async resolveExistingKey(key) {
206
- const fallbacks = this.bareFallbacksFor(key);
195
+ const fallbacks = this.#bareFallbacksFor(key);
207
196
  if (fallbacks) {
208
197
  return ((await this.#nativeBackend.resolveExistingKey(key, fallbacks.map(({ extension }) => extension))) ?? undefined);
209
198
  }
210
199
  return (await this.#nativeBackend.recordExists(key)) ? key : undefined;
211
200
  }
212
201
  /**
213
- * The native `resolveValue`/`resolveExistingKey` bare-file fallbacks to use for `key`, or
214
- * `undefined` if `key` is a plain tracked-record lookup with no bare-file probing.
215
- *
216
- * - The logical run-input key (`INPUT`) probes the full extension ladder (`INPUT`, `INPUT.json`,
217
- * `INPUT.txt`, `INPUT.bin`), matching how Crawlee reads run input.
218
- * - A literal bare filename as surfaced by `listKeys` (`INPUT.json`/`.txt`/`.bin`) resolves itself:
219
- * the tracked record first, then the bare file at that exact name (a single empty-extension
220
- * fallback), so a listed key round-trips through `getValue`/`recordExists`.
202
+ * Bare-file fallbacks for `key`, or `undefined` for a plain tracked-record lookup. A logical input
203
+ * key (`INPUT`) probes the whole extension ladder; a literal bare filename (`INPUT.json`, as listed
204
+ * by `listKeys`) probes only itself, so a listed key reads back under its own name.
221
205
  */
222
- // eslint-disable-next-line class-methods-use-this
223
- bareFallbacksFor(key) {
224
- if (ALLOWED_BARE_FILES.includes(key)) {
206
+ #bareFallbacksFor(key) {
207
+ if (this.#inputKeys.includes(key)) {
225
208
  return BARE_FILE_FALLBACKS;
226
209
  }
227
- const contentType = BARE_FILE_CONTENT_TYPES.get(key);
228
- if (contentType !== undefined) {
229
- return [{ extension: '', contentType }];
210
+ const logicalKey = this.#bareFileLogicalKeys.get(key);
211
+ if (logicalKey === undefined) {
212
+ return undefined;
230
213
  }
231
- return undefined;
214
+ const { contentType } = BARE_FILE_FALLBACKS.find(({ extension }) => `${logicalKey}${extension}` === key);
215
+ return [{ extension: '', contentType }];
232
216
  }
233
217
  }