@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.
- package/dist/backends/file_index.js +0 -3
- package/dist/backends/overlay.js +0 -8
- package/dist/backends/store/fs.js +4 -17
- package/dist/config.d.ts +14 -0
- package/dist/config.js +4 -0
- package/dist/devices.js +0 -12
- package/dist/emulation/cache.d.ts +21 -0
- package/dist/emulation/cache.js +36 -0
- package/dist/emulation/promises.d.ts +9 -14
- package/dist/emulation/promises.js +71 -48
- package/dist/emulation/shared.d.ts +22 -0
- package/dist/emulation/shared.js +6 -0
- package/dist/emulation/sync.d.ts +11 -20
- package/dist/emulation/sync.js +44 -23
- package/package.json +4 -2
- package/scripts/test.js +14 -1
- package/src/backends/backend.ts +160 -0
- package/src/backends/fetch.ts +180 -0
- package/src/backends/file_index.ts +206 -0
- package/src/backends/memory.ts +50 -0
- package/src/backends/overlay.ts +560 -0
- package/src/backends/port/fs.ts +335 -0
- package/src/backends/port/readme.md +54 -0
- package/src/backends/port/rpc.ts +167 -0
- package/src/backends/readme.md +3 -0
- package/src/backends/store/fs.ts +700 -0
- package/src/backends/store/readme.md +9 -0
- package/src/backends/store/simple.ts +146 -0
- package/src/backends/store/store.ts +173 -0
- package/src/config.ts +173 -0
- package/src/credentials.ts +31 -0
- package/src/devices.ts +459 -0
- package/src/emulation/async.ts +834 -0
- package/src/emulation/cache.ts +44 -0
- package/src/emulation/constants.ts +182 -0
- package/src/emulation/dir.ts +138 -0
- package/src/emulation/index.ts +8 -0
- package/src/emulation/path.ts +440 -0
- package/src/emulation/promises.ts +1133 -0
- package/src/emulation/shared.ts +160 -0
- package/src/emulation/streams.ts +34 -0
- package/src/emulation/sync.ts +867 -0
- package/src/emulation/watchers.ts +193 -0
- package/src/error.ts +307 -0
- package/src/file.ts +661 -0
- package/src/filesystem.ts +174 -0
- package/src/index.ts +25 -0
- package/src/inode.ts +132 -0
- package/src/mixins/async.ts +208 -0
- package/src/mixins/index.ts +5 -0
- package/src/mixins/mutexed.ts +257 -0
- package/src/mixins/readonly.ts +96 -0
- package/src/mixins/shared.ts +25 -0
- package/src/mixins/sync.ts +58 -0
- package/src/polyfills.ts +21 -0
- package/src/stats.ts +363 -0
- package/src/utils.ts +288 -0
- package/tests/fs/readdir.test.ts +3 -3
package/dist/emulation/sync.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
445
|
-
|
|
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
|
|
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
|
-
|
|
658
|
+
break;
|
|
642
659
|
case constants.S_IFREG:
|
|
643
660
|
case constants.S_IFLNK:
|
|
644
661
|
unlinkSync(path);
|
|
645
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|