@zenfs/core 1.1.6 → 1.2.0

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.
Files changed (58) hide show
  1. package/dist/backends/file_index.js +0 -3
  2. package/dist/backends/overlay.js +0 -8
  3. package/dist/backends/store/fs.js +4 -17
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +4 -0
  6. package/dist/devices.js +0 -12
  7. package/dist/emulation/cache.d.ts +21 -0
  8. package/dist/emulation/cache.js +36 -0
  9. package/dist/emulation/promises.d.ts +9 -14
  10. package/dist/emulation/promises.js +71 -48
  11. package/dist/emulation/shared.d.ts +22 -0
  12. package/dist/emulation/shared.js +6 -0
  13. package/dist/emulation/sync.d.ts +11 -20
  14. package/dist/emulation/sync.js +44 -23
  15. package/package.json +4 -2
  16. package/scripts/test.js +14 -1
  17. package/src/backends/backend.ts +160 -0
  18. package/src/backends/fetch.ts +180 -0
  19. package/src/backends/file_index.ts +206 -0
  20. package/src/backends/memory.ts +50 -0
  21. package/src/backends/overlay.ts +560 -0
  22. package/src/backends/port/fs.ts +335 -0
  23. package/src/backends/port/readme.md +54 -0
  24. package/src/backends/port/rpc.ts +167 -0
  25. package/src/backends/readme.md +3 -0
  26. package/src/backends/store/fs.ts +700 -0
  27. package/src/backends/store/readme.md +9 -0
  28. package/src/backends/store/simple.ts +146 -0
  29. package/src/backends/store/store.ts +173 -0
  30. package/src/config.ts +173 -0
  31. package/src/credentials.ts +31 -0
  32. package/src/devices.ts +459 -0
  33. package/src/emulation/async.ts +834 -0
  34. package/src/emulation/cache.ts +44 -0
  35. package/src/emulation/constants.ts +182 -0
  36. package/src/emulation/dir.ts +138 -0
  37. package/src/emulation/index.ts +8 -0
  38. package/src/emulation/path.ts +440 -0
  39. package/src/emulation/promises.ts +1133 -0
  40. package/src/emulation/shared.ts +160 -0
  41. package/src/emulation/streams.ts +34 -0
  42. package/src/emulation/sync.ts +867 -0
  43. package/src/emulation/watchers.ts +193 -0
  44. package/src/error.ts +307 -0
  45. package/src/file.ts +661 -0
  46. package/src/filesystem.ts +174 -0
  47. package/src/index.ts +25 -0
  48. package/src/inode.ts +132 -0
  49. package/src/mixins/async.ts +208 -0
  50. package/src/mixins/index.ts +5 -0
  51. package/src/mixins/mutexed.ts +257 -0
  52. package/src/mixins/readonly.ts +96 -0
  53. package/src/mixins/shared.ts +25 -0
  54. package/src/mixins/sync.ts +58 -0
  55. package/src/polyfills.ts +21 -0
  56. package/src/stats.ts +363 -0
  57. package/src/utils.ts +288 -0
  58. package/tests/fs/readdir.test.ts +3 -3
@@ -53,14 +53,15 @@ import { decodeUTF8, normalizeMode, normalizeOptions, normalizePath, normalizeTi
53
53
  import * as constants from './constants.js';
54
54
  import { Dir, Dirent } from './dir.js';
55
55
  import { dirname, join, parse } from './path.js';
56
- import { _statfs, fd2file, fdMap, file2fd, fixError, mounts, resolveMount } from './shared.js';
56
+ import { _statfs, config, fd2file, fdMap, file2fd, fixError, mounts, resolveMount } from './shared.js';
57
57
  import { emitChange } from './watchers.js';
58
+ import * as cache from './cache.js';
58
59
  export function renameSync(oldPath, newPath) {
59
60
  oldPath = normalizePath(oldPath);
60
61
  newPath = normalizePath(newPath);
61
62
  const oldMount = resolveMount(oldPath);
62
63
  const newMount = resolveMount(newPath);
63
- if (!statSync(dirname(oldPath)).hasAccess(constants.W_OK)) {
64
+ if (config.checkAccess && !statSync(dirname(oldPath)).hasAccess(constants.W_OK)) {
64
65
  throw ErrnoError.With('EACCES', oldPath, 'rename');
65
66
  }
66
67
  try {
@@ -100,7 +101,7 @@ export function statSync(path, options) {
100
101
  const { fs, path: resolved } = resolveMount(realpathSync(path));
101
102
  try {
102
103
  const stats = fs.statSync(resolved);
103
- if (!stats.hasAccess(constants.R_OK)) {
104
+ if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
104
105
  throw ErrnoError.With('EACCES', resolved, 'stat');
105
106
  }
106
107
  return options?.bigint ? new BigIntStats(stats) : stats;
@@ -145,7 +146,7 @@ export function unlinkSync(path) {
145
146
  path = normalizePath(path);
146
147
  const { fs, path: resolved } = resolveMount(path);
147
148
  try {
148
- if (!fs.statSync(resolved).hasAccess(constants.W_OK)) {
149
+ if (config.checkAccess && !(cache.getStats(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) {
149
150
  throw ErrnoError.With('EACCES', resolved, 'unlink');
150
151
  }
151
152
  fs.unlinkSync(resolved);
@@ -174,7 +175,7 @@ function _openSync(path, _flag, _mode, resolveSymlinks = true) {
174
175
  }
175
176
  // Create the file
176
177
  const parentStats = fs.statSync(dirname(resolved));
177
- if (!parentStats.hasAccess(constants.W_OK)) {
178
+ if (config.checkAccess && !parentStats.hasAccess(constants.W_OK)) {
178
179
  throw ErrnoError.With('EACCES', dirname(path), '_open');
179
180
  }
180
181
  if (!parentStats.isDirectory()) {
@@ -182,7 +183,7 @@ function _openSync(path, _flag, _mode, resolveSymlinks = true) {
182
183
  }
183
184
  return fs.createFileSync(resolved, flag, mode);
184
185
  }
185
- if (!stats.hasAccess(mode) || !stats.hasAccess(flagToMode(flag))) {
186
+ if (config.checkAccess && (!stats.hasAccess(mode) || !stats.hasAccess(flagToMode(flag)))) {
186
187
  throw ErrnoError.With('EACCES', path, '_open');
187
188
  }
188
189
  if (isExclusive(flag)) {
@@ -392,7 +393,11 @@ export function rmdirSync(path) {
392
393
  path = normalizePath(path);
393
394
  const { fs, path: resolved } = resolveMount(realpathSync(path));
394
395
  try {
395
- if (!fs.statSync(resolved).hasAccess(constants.W_OK)) {
396
+ const stats = cache.getStats(path) || fs.statSync(resolved);
397
+ if (!stats.isDirectory()) {
398
+ throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
399
+ }
400
+ if (config.checkAccess && !stats.hasAccess(constants.W_OK)) {
396
401
  throw ErrnoError.With('EACCES', resolved, 'rmdir');
397
402
  }
398
403
  fs.rmdirSync(resolved);
@@ -406,12 +411,12 @@ rmdirSync;
406
411
  export function mkdirSync(path, options) {
407
412
  options = typeof options === 'object' ? options : { mode: options };
408
413
  const mode = normalizeMode(options?.mode, 0o777);
409
- path = realpathSync(normalizePath(path));
414
+ path = realpathSync(path);
410
415
  const { fs, path: resolved } = resolveMount(path);
411
416
  const errorPaths = { [resolved]: path };
412
417
  try {
413
418
  if (!options?.recursive) {
414
- if (!fs.statSync(dirname(resolved)).hasAccess(constants.W_OK)) {
419
+ if (config.checkAccess && !fs.statSync(dirname(resolved)).hasAccess(constants.W_OK)) {
415
420
  throw ErrnoError.With('EACCES', dirname(resolved), 'mkdir');
416
421
  }
417
422
  return fs.mkdirSync(resolved, mode);
@@ -422,7 +427,7 @@ export function mkdirSync(path, options) {
422
427
  errorPaths[dir] = original;
423
428
  }
424
429
  for (const dir of dirs) {
425
- if (!fs.statSync(dirname(dir)).hasAccess(constants.W_OK)) {
430
+ if (config.checkAccess && !fs.statSync(dirname(dir)).hasAccess(constants.W_OK)) {
426
431
  throw ErrnoError.With('EACCES', dirname(dir), 'mkdir');
427
432
  }
428
433
  fs.mkdirSync(dir, mode);
@@ -441,8 +446,13 @@ export function readdirSync(path, options) {
441
446
  const { fs, path: resolved } = resolveMount(realpathSync(path));
442
447
  let entries;
443
448
  try {
444
- if (!fs.statSync(resolved).hasAccess(constants.R_OK)) {
445
- throw ErrnoError.With('EACCES', path, 'readdir');
449
+ const stats = cache.getStats(path) || fs.statSync(resolved);
450
+ cache.setStats(path, stats);
451
+ if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
452
+ throw ErrnoError.With('EACCES', resolved, 'readdir');
453
+ }
454
+ if (!stats.isDirectory()) {
455
+ throw ErrnoError.With('ENOTDIR', resolved, 'readdir');
446
456
  }
447
457
  entries = fs.readdirSync(resolved);
448
458
  }
@@ -463,11 +473,12 @@ export function readdirSync(path, options) {
463
473
  // Iterate over entries and handle recursive case if needed
464
474
  const values = [];
465
475
  for (const entry of entries) {
466
- const entryStat = fs.statSync(join(resolved, entry));
476
+ const entryStat = cache.getStats(join(path, entry)) || fs.statSync(join(resolved, entry));
477
+ cache.setStats(join(path, entry), entryStat);
467
478
  if (options?.withFileTypes) {
468
479
  values.push(new Dirent(entry, entryStat));
469
480
  }
470
- else if (options?.encoding === 'buffer') {
481
+ else if (options?.encoding == 'buffer') {
471
482
  values.push(Buffer.from(entry));
472
483
  }
473
484
  else {
@@ -475,7 +486,7 @@ export function readdirSync(path, options) {
475
486
  }
476
487
  if (!entryStat.isDirectory() || !options?.recursive)
477
488
  continue;
478
- for (const subEntry of readdirSync(join(path, entry), options)) {
489
+ for (const subEntry of readdirSync(join(path, entry), { ...options, _isIndirect: true })) {
479
490
  if (subEntry instanceof Dirent) {
480
491
  subEntry.path = join(entry, subEntry.path);
481
492
  values.push(subEntry);
@@ -488,17 +499,20 @@ export function readdirSync(path, options) {
488
499
  }
489
500
  }
490
501
  }
502
+ if (!options?._isIndirect) {
503
+ cache.clearStats();
504
+ }
491
505
  return values;
492
506
  }
493
507
  readdirSync;
494
508
  // SYMLINK METHODS
495
509
  export function linkSync(targetPath, linkPath) {
496
510
  targetPath = normalizePath(targetPath);
497
- if (!statSync(dirname(targetPath)).hasAccess(constants.R_OK)) {
511
+ if (config.checkAccess && !statSync(dirname(targetPath)).hasAccess(constants.R_OK)) {
498
512
  throw ErrnoError.With('EACCES', dirname(targetPath), 'link');
499
513
  }
500
514
  linkPath = normalizePath(linkPath);
501
- if (!statSync(dirname(linkPath)).hasAccess(constants.W_OK)) {
515
+ if (config.checkAccess && !statSync(dirname(linkPath)).hasAccess(constants.W_OK)) {
502
516
  throw ErrnoError.With('EACCES', dirname(linkPath), 'link');
503
517
  }
504
518
  const { fs, path } = resolveMount(targetPath);
@@ -507,7 +521,7 @@ export function linkSync(targetPath, linkPath) {
507
521
  throw ErrnoError.With('EXDEV', linkPath, 'link');
508
522
  }
509
523
  try {
510
- if (!fs.statSync(path).hasAccess(constants.W_OK)) {
524
+ if (config.checkAccess && !fs.statSync(path).hasAccess(constants.W_OK)) {
511
525
  throw ErrnoError.With('EACCES', path, 'link');
512
526
  }
513
527
  return fs.linkSync(path, linkPath);
@@ -608,6 +622,8 @@ export function realpathSync(path, options) {
608
622
  }
609
623
  realpathSync;
610
624
  export function accessSync(path, mode = 0o600) {
625
+ if (!config.checkAccess)
626
+ return;
611
627
  if (!statSync(path).hasAccess(mode)) {
612
628
  throw new ErrnoError(Errno.EACCES);
613
629
  }
@@ -621,7 +637,7 @@ export function rmSync(path, options) {
621
637
  path = normalizePath(path);
622
638
  let stats;
623
639
  try {
624
- stats = statSync(path);
640
+ stats = cache.getStats(path) || statSync(path);
625
641
  }
626
642
  catch (error) {
627
643
  if (error.code != 'ENOENT' || !options?.force)
@@ -630,26 +646,31 @@ export function rmSync(path, options) {
630
646
  if (!stats) {
631
647
  return;
632
648
  }
649
+ cache.setStats(path, stats);
633
650
  switch (stats.mode & constants.S_IFMT) {
634
651
  case constants.S_IFDIR:
635
652
  if (options?.recursive) {
636
- for (const entry of readdirSync(path)) {
637
- rmSync(join(path, entry), options);
653
+ for (const entry of readdirSync(path, { _isIndirect: true })) {
654
+ rmSync(join(path, entry), { ...options, _isIndirect: true });
638
655
  }
639
656
  }
640
657
  rmdirSync(path);
641
- return;
658
+ break;
642
659
  case constants.S_IFREG:
643
660
  case constants.S_IFLNK:
644
661
  unlinkSync(path);
645
- return;
662
+ break;
646
663
  case constants.S_IFBLK:
647
664
  case constants.S_IFCHR:
648
665
  case constants.S_IFIFO:
649
666
  case constants.S_IFSOCK:
650
667
  default:
668
+ cache.clearStats();
651
669
  throw new ErrnoError(Errno.EPERM, 'File type not supported', path, 'rm');
652
670
  }
671
+ if (!options?._isIndirect) {
672
+ cache.clearStats();
673
+ }
653
674
  }
654
675
  rmSync;
655
676
  export function mkdtempSync(prefix, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -18,6 +18,7 @@
18
18
  "zenfs-test": "scripts/test.js"
19
19
  },
20
20
  "files": [
21
+ "src",
21
22
  "dist",
22
23
  "tests",
23
24
  "license.md",
@@ -48,7 +49,8 @@
48
49
  "./mixins": "./dist/mixins/index.js",
49
50
  "./path": "./dist/emulation/path.js",
50
51
  "./eslint": "./eslint.shared.js",
51
- "./tests/*": "./tests/*"
52
+ "./tests/*": "./tests/*",
53
+ "./src/*": "./src/*"
52
54
  },
53
55
  "scripts": {
54
56
  "format": "prettier --write .",
package/scripts/test.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
4
5
  import { join } from 'node:path';
5
6
  import { parseArgs } from 'node:util';
6
7
 
@@ -8,6 +9,8 @@ const { values: options, positionals } = parseArgs({
8
9
  options: {
9
10
  help: { short: 'h', type: 'boolean', default: false },
10
11
  verbose: { type: 'boolean', default: false },
12
+ test: { type: 'string' },
13
+ forceExit: { short: 'f', type: 'boolean', default: false },
11
14
  },
12
15
  allowPositionals: true,
13
16
  });
@@ -20,12 +23,22 @@ paths: The setup files to run tests on
20
23
  options:
21
24
  --help, -h Outputs this help message
22
25
  --verbose Output verbose messages
26
+ --test Which test to run
27
+ --forceExit Whether to use --test-force-exit
23
28
  `);
24
29
  process.exit();
25
30
  }
26
31
 
32
+ if (options.verbose) console.debug('Forcing tests to exit (--test-force-exit)');
33
+
34
+ const testsGlob = join(import.meta.dirname, `../tests/fs/${options.test || '*'}.test.ts`);
35
+
27
36
  for (const setupFile of positionals) {
28
37
  if (options.verbose) console.debug('Running tests for:', setupFile);
29
38
  process.env.SETUP = setupFile;
30
- execSync('tsx --test --experimental-test-coverage ' + join(import.meta.dirname, '../tests/fs/*.test.ts'), { stdio: 'inherit' });
39
+ if (!existsSync(setupFile)) {
40
+ console.log('ERROR: Skipping non-existent file:', setupFile);
41
+ continue;
42
+ }
43
+ execSync(['tsx --test --experimental-test-coverage', options.forceExit ? '--test-force-exit' : '', testsGlob, process.env.CMD].join(' '), { stdio: 'inherit' });
31
44
  }
@@ -0,0 +1,160 @@
1
+ import type { Entries, RequiredKeys } from 'utilium';
2
+ import { ErrnoError, Errno } from '../error.js';
3
+ import type { FileSystem } from '../filesystem.js';
4
+ import { levenshtein } from '../utils.js';
5
+
6
+ type OptionType = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';
7
+
8
+ /**
9
+ * Resolves the type of Backend.options from the options interface
10
+ */
11
+ export type OptionsConfig<T> = {
12
+ [K in keyof T]: {
13
+ /**
14
+ * The basic JavaScript type(s) for this option.
15
+ */
16
+ type: OptionType | readonly OptionType[];
17
+
18
+ /**
19
+ * Description of the option. Used in error messages and documentation.
20
+ */
21
+ description: string;
22
+
23
+ /**
24
+ * Whether or not the option is required (optional can be set to null or undefined). Defaults to false.
25
+ */
26
+ required: K extends RequiredKeys<T> ? true : false;
27
+
28
+ /**
29
+ * A custom validation function to check if the option is valid.
30
+ * When async, resolves if valid and rejects if not.
31
+ * When sync, it will throw an error if not valid.
32
+ */
33
+ validator?(opt: T[K]): void | Promise<void>;
34
+ };
35
+ };
36
+
37
+ /**
38
+ * Configuration options shared by backends and `Configuration`
39
+ */
40
+ export interface SharedConfig {
41
+ /**
42
+ * If set, disables the sync cache and sync operations on async file systems.
43
+ */
44
+ disableAsyncCache?: boolean;
45
+ }
46
+
47
+ /**
48
+ * A backend
49
+ */
50
+ export interface Backend<FS extends FileSystem = FileSystem, TOptions extends object = object> {
51
+ /**
52
+ * Create a new instance of the backend
53
+ */
54
+ create(options: TOptions & Partial<SharedConfig>): FS | Promise<FS>;
55
+
56
+ /**
57
+ * A name to identify the backend.
58
+ */
59
+ name: string;
60
+
61
+ /**
62
+ * Describes all of the options available for this backend.
63
+ */
64
+ options: OptionsConfig<TOptions>;
65
+
66
+ /**
67
+ * Whether the backend is available in the current environment.
68
+ * It supports checking synchronously and asynchronously
69
+ *
70
+ * Returns 'true' if this backend is available in the current
71
+ * environment. For example, a backend using a browser API will return
72
+ * 'false' if the API is unavailable
73
+ *
74
+ */
75
+ isAvailable(): boolean | Promise<boolean>;
76
+ }
77
+
78
+ /**
79
+ * Gets the options type of a backend
80
+ * @internal
81
+ */
82
+ export type OptionsOf<T extends Backend> = T extends Backend<FileSystem, infer TOptions> ? TOptions : never;
83
+
84
+ /**
85
+ * Gets the FileSystem type for a backend
86
+ * @internal
87
+ */
88
+ export type FilesystemOf<T extends Backend> = T extends Backend<infer FS> ? FS : never;
89
+
90
+ /** @internal */
91
+ export function isBackend(arg: unknown): arg is Backend {
92
+ return arg != null && typeof arg == 'object' && 'isAvailable' in arg && typeof arg.isAvailable == 'function' && 'create' in arg && typeof arg.create == 'function';
93
+ }
94
+
95
+ /**
96
+ * Checks that `options` object is valid for the file system options.
97
+ * @internal
98
+ */
99
+ export async function checkOptions<T extends Backend>(backend: T, options: Record<string, unknown>): Promise<void> {
100
+ if (typeof options != 'object' || options === null) {
101
+ throw new ErrnoError(Errno.EINVAL, 'Invalid options');
102
+ }
103
+
104
+ // Check for required options.
105
+ for (const [optName, opt] of Object.entries(backend.options) as Entries<OptionsConfig<Record<string, any>>>) {
106
+ const providedValue = options?.[optName];
107
+
108
+ if (providedValue === undefined || providedValue === null) {
109
+ if (!opt.required) {
110
+ continue;
111
+ }
112
+ /* Required option not provided.
113
+ if any incorrect options provided, which ones are close to the provided one?
114
+ (edit distance 5 === close)*/
115
+ const incorrectOptions = Object.keys(options)
116
+ .filter(o => !(o in backend.options))
117
+ .map((a: string) => {
118
+ return { str: a, distance: levenshtein(optName, a) };
119
+ })
120
+ .filter(o => o.distance < 5)
121
+ .sort((a, b) => a.distance - b.distance);
122
+
123
+ throw new ErrnoError(
124
+ Errno.EINVAL,
125
+ `${backend.name}: Required option '${optName}' not provided.${
126
+ incorrectOptions.length > 0 ? ` You provided '${incorrectOptions[0].str}', did you mean '${optName}'.` : ''
127
+ }`
128
+ );
129
+ }
130
+ // Option provided, check type.
131
+ const typeMatches = Array.isArray(opt.type) ? opt.type.indexOf(typeof providedValue) != -1 : typeof providedValue == opt.type;
132
+ if (!typeMatches) {
133
+ throw new ErrnoError(
134
+ Errno.EINVAL,
135
+ `${backend.name}: Value provided for option ${optName} is not the proper type. Expected ${
136
+ Array.isArray(opt.type) ? `one of {${opt.type.join(', ')}}` : (opt.type as string)
137
+ }, but received ${typeof providedValue}`
138
+ );
139
+ }
140
+
141
+ if (opt.validator) {
142
+ await opt.validator(providedValue);
143
+ }
144
+ // Otherwise: All good!
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Specifies a file system backend type and its options.
150
+ *
151
+ * Individual options can recursively contain BackendConfiguration objects for values that require file systems.
152
+ *
153
+ * The configuration for each file system corresponds to that file system's option object passed to its `create()` method.
154
+ */
155
+ export type BackendConfiguration<T extends Backend> = OptionsOf<T> & Partial<SharedConfig> & { backend: T };
156
+
157
+ /** @internal */
158
+ export function isBackendConfig<T extends Backend>(arg: unknown): arg is BackendConfiguration<T> {
159
+ return arg != null && typeof arg == 'object' && 'backend' in arg && isBackend(arg.backend);
160
+ }
@@ -0,0 +1,180 @@
1
+ import { Errno, ErrnoError } from '../error.js';
2
+ import type { FileSystemMetadata } from '../filesystem.js';
3
+ import type { Stats } from '../stats.js';
4
+ import type { Backend } from './backend.js';
5
+ import { IndexFS } from './file_index.js';
6
+ import type { IndexData } from './file_index.js';
7
+
8
+ /**
9
+ * Asynchronously download a file as a buffer or a JSON object.
10
+ * Note that the third function signature with a non-specialized type is
11
+ * invalid, but TypeScript requires it when you specialize string arguments to
12
+ * constants.
13
+ * @hidden
14
+ */
15
+ async function fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>;
16
+ async function fetchFile<T extends object>(path: string, type: 'json'): Promise<T>;
17
+ async function fetchFile<T extends object>(path: string, type: 'buffer' | 'json'): Promise<T | Uint8Array>;
18
+ async function fetchFile<T extends object>(path: string, type: string): Promise<T | Uint8Array> {
19
+ const response = await fetch(path).catch((e: Error) => {
20
+ throw new ErrnoError(Errno.EIO, e.message, path);
21
+ });
22
+ if (!response.ok) {
23
+ throw new ErrnoError(Errno.EIO, 'fetch failed: response returned code ' + response.status, path);
24
+ }
25
+ switch (type) {
26
+ case 'buffer': {
27
+ const arrayBuffer = await response.arrayBuffer().catch((e: Error) => {
28
+ throw new ErrnoError(Errno.EIO, e.message, path);
29
+ });
30
+ return new Uint8Array(arrayBuffer);
31
+ }
32
+ case 'json':
33
+ return response.json().catch((e: Error) => {
34
+ throw new ErrnoError(Errno.EIO, e.message, path);
35
+ }) as Promise<T>;
36
+ default:
37
+ throw new ErrnoError(Errno.EINVAL, 'Invalid download type: ' + type);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Configuration options for FetchFS.
43
+ */
44
+ export interface FetchOptions {
45
+ /**
46
+ * URL to a file index as a JSON file or the file index object itself.
47
+ * Defaults to `index.json`.
48
+ */
49
+ index?: string | IndexData;
50
+
51
+ /** Used as the URL prefix for fetched files.
52
+ * Default: Fetch files relative to the index.
53
+ */
54
+ baseUrl?: string;
55
+ }
56
+
57
+ /**
58
+ * A simple filesystem backed by HTTP using the `fetch` API.
59
+ *
60
+ *
61
+ * Index objects look like the following:
62
+ *
63
+ * ```json
64
+ * {
65
+ * "version": 1,
66
+ * "entries": {
67
+ * "/home": { ... },
68
+ * "/home/jvilk": { ... },
69
+ * "/home/james": { ... }
70
+ * }
71
+ * }
72
+ * ```
73
+ *
74
+ * Each entry contains the stats associated with the file.
75
+ */
76
+ export class FetchFS extends IndexFS {
77
+ public readonly baseUrl: string;
78
+
79
+ public async ready(): Promise<void> {
80
+ if (this._isInitialized) {
81
+ return;
82
+ }
83
+ await super.ready();
84
+
85
+ if (this._disableSync) {
86
+ return;
87
+ }
88
+
89
+ /**
90
+ * Iterate over all of the files and cache their contents
91
+ */
92
+ for (const [path, stats] of this.index.files()) {
93
+ await this.getData(path, stats);
94
+ }
95
+ }
96
+
97
+ public constructor({ index = 'index.json', baseUrl = '' }: FetchOptions) {
98
+ // prefix url must end in a directory separator.
99
+ if (baseUrl.at(-1) != '/') {
100
+ baseUrl += '/';
101
+ }
102
+
103
+ super(typeof index != 'string' ? index : fetchFile<IndexData>(baseUrl + index, 'json'));
104
+
105
+ this.baseUrl = baseUrl;
106
+ }
107
+
108
+ public metadata(): FileSystemMetadata {
109
+ return {
110
+ ...super.metadata(),
111
+ name: FetchFS.name,
112
+ readonly: true,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Preload the `path` into the index.
118
+ */
119
+ public preload(path: string, buffer: Uint8Array): void {
120
+ const stats = this.index.get(path);
121
+ if (!stats) {
122
+ throw ErrnoError.With('ENOENT', path, 'preload');
123
+ }
124
+ if (!stats.isFile()) {
125
+ throw ErrnoError.With('EISDIR', path, 'preload');
126
+ }
127
+ stats.size = buffer.length;
128
+ stats.fileData = buffer;
129
+ }
130
+
131
+ /**
132
+ * @todo Be lazier about actually requesting the data?
133
+ */
134
+ protected async getData(path: string, stats: Stats): Promise<Uint8Array> {
135
+ if (stats.fileData) {
136
+ return stats.fileData;
137
+ }
138
+
139
+ const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
140
+ stats.fileData = data;
141
+ return data;
142
+ }
143
+
144
+ protected getDataSync(path: string, stats: Stats): Uint8Array {
145
+ if (stats.fileData) {
146
+ return stats.fileData;
147
+ }
148
+
149
+ throw new ErrnoError(Errno.ENODATA, '', path, 'getData');
150
+ }
151
+ }
152
+
153
+ const _Fetch = {
154
+ name: 'Fetch',
155
+
156
+ options: {
157
+ index: {
158
+ type: ['string', 'object'],
159
+ required: false,
160
+ description: 'URL to a file index as a JSON file or the file index object itself, generated with the make-index script. Defaults to `index.json`.',
161
+ },
162
+ baseUrl: {
163
+ type: 'string',
164
+ required: false,
165
+ description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
166
+ },
167
+ },
168
+
169
+ isAvailable(): boolean {
170
+ return typeof globalThis.fetch == 'function';
171
+ },
172
+
173
+ create(options: FetchOptions) {
174
+ return new FetchFS(options);
175
+ },
176
+ } as const satisfies Backend<FetchFS, FetchOptions>;
177
+ type _Fetch = typeof _Fetch;
178
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
179
+ export interface Fetch extends _Fetch {}
180
+ export const Fetch: Fetch = _Fetch;