@zenfs/core 1.3.4 → 1.3.6

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/config.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { Backend, BackendConfiguration, FilesystemOf, SharedConfig } from './backends/backend.js';
2
- import type { AbsolutePath } from './emulation/path.js';
3
2
  /**
4
3
  * Configuration for a specific mount point
5
4
  */
@@ -9,8 +8,11 @@ export type MountConfiguration<T extends Backend> = FilesystemOf<T> | BackendCon
9
8
  * @see MountConfiguration
10
9
  */
11
10
  export declare function resolveMountConfig<T extends Backend>(configuration: MountConfiguration<T>, _depth?: number): Promise<FilesystemOf<T>>;
11
+ /**
12
+ * An object mapping mount points to backends
13
+ */
12
14
  export interface ConfigMounts {
13
- [K: AbsolutePath]: Backend;
15
+ [K: string]: Backend;
14
16
  }
15
17
  /**
16
18
  * Configuration
@@ -20,7 +22,7 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
20
22
  * An object mapping mount points to mount configuration
21
23
  */
22
24
  mounts: {
23
- [K in keyof T & AbsolutePath]: MountConfiguration<T[K]>;
25
+ [K in keyof T]: MountConfiguration<T[K]>;
24
26
  };
25
27
  /**
26
28
  * The uid to use
package/dist/config.js CHANGED
@@ -96,10 +96,8 @@ export async function configure(configuration) {
96
96
  if (configuration.mounts) {
97
97
  const toMount = [];
98
98
  let unmountRoot = false;
99
- for (const [point, mountConfig] of Object.entries(configuration.mounts)) {
100
- if (!point.startsWith('/')) {
101
- throw new ErrnoError(Errno.EINVAL, 'Mount points must have absolute paths');
102
- }
99
+ for (const [_point, mountConfig] of Object.entries(configuration.mounts)) {
100
+ const point = _point.startsWith('/') ? _point : '/' + _point;
103
101
  if (isBackendConfig(mountConfig)) {
104
102
  mountConfig.disableAsyncCache ?? (mountConfig.disableAsyncCache = configuration.disableAsyncCache || false);
105
103
  }
@@ -7,26 +7,30 @@ export declare class Cache<T> {
7
7
  isEnabled: boolean;
8
8
  protected sync: Map<string, T>;
9
9
  protected async: Map<string, Promise<T>>;
10
+ /**
11
+ * Whether the data exists in the cache
12
+ */
13
+ has(path: string): boolean;
10
14
  /**
11
15
  * Gets data from the cache, if is exists and the cache is enabled.
12
16
  */
13
- getSync(path: string): T | undefined;
17
+ get(path: string): T | undefined;
14
18
  /**
15
19
  * Adds data if the cache is enabled
16
20
  */
17
- setSync(path: string, value: T): void;
21
+ set(path: string, value: T): void;
18
22
  /**
19
- * Clears the cache if it is enabled
23
+ * Whether the data exists in the cache
20
24
  */
21
- clearSync(): void;
25
+ hasAsync(path: string): boolean;
22
26
  /**
23
27
  * Gets data from the cache, if it exists and the cache is enabled.
24
28
  */
25
- get(path: string): Promise<T> | undefined;
29
+ getAsync(path: string): Promise<T> | undefined;
26
30
  /**
27
31
  * Adds data if the cache is enabled
28
32
  */
29
- set(path: string, value: Promise<T>): void;
33
+ setAsync(path: string, value: Promise<T>): void;
30
34
  /**
31
35
  * Clears the cache if it is enabled
32
36
  */
@@ -9,10 +9,16 @@ export class Cache {
9
9
  this.sync = new Map();
10
10
  this.async = new Map();
11
11
  }
12
+ /**
13
+ * Whether the data exists in the cache
14
+ */
15
+ has(path) {
16
+ return this.isEnabled && this.sync.has(path);
17
+ }
12
18
  /**
13
19
  * Gets data from the cache, if is exists and the cache is enabled.
14
20
  */
15
- getSync(path) {
21
+ get(path) {
16
22
  if (!this.isEnabled)
17
23
  return;
18
24
  return this.sync.get(path);
@@ -20,24 +26,22 @@ export class Cache {
20
26
  /**
21
27
  * Adds data if the cache is enabled
22
28
  */
23
- setSync(path, value) {
29
+ set(path, value) {
24
30
  if (!this.isEnabled)
25
31
  return;
26
32
  this.sync.set(path, value);
27
33
  this.async.set(path, Promise.resolve(value));
28
34
  }
29
35
  /**
30
- * Clears the cache if it is enabled
36
+ * Whether the data exists in the cache
31
37
  */
32
- clearSync() {
33
- if (!this.isEnabled)
34
- return;
35
- this.sync.clear();
38
+ hasAsync(path) {
39
+ return this.isEnabled && this.async.has(path);
36
40
  }
37
41
  /**
38
42
  * Gets data from the cache, if it exists and the cache is enabled.
39
43
  */
40
- get(path) {
44
+ getAsync(path) {
41
45
  if (!this.isEnabled)
42
46
  return;
43
47
  return this.async.get(path);
@@ -45,7 +49,7 @@ export class Cache {
45
49
  /**
46
50
  * Adds data if the cache is enabled
47
51
  */
48
- set(path, value) {
52
+ setAsync(path, value) {
49
53
  if (!this.isEnabled)
50
54
  return;
51
55
  this.async.set(path, value);
@@ -57,6 +61,7 @@ export class Cache {
57
61
  clear() {
58
62
  if (!this.isEnabled)
59
63
  return;
64
+ this.sync.clear();
60
65
  this.async.clear();
61
66
  }
62
67
  }
@@ -56,7 +56,7 @@ import * as cache from './cache.js';
56
56
  import { config } from './config.js';
57
57
  import * as constants from './constants.js';
58
58
  import { Dir, Dirent } from './dir.js';
59
- import { dirname, join, parse } from './path.js';
59
+ import { dirname, join, parse, resolve } from './path.js';
60
60
  import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount } from './shared.js';
61
61
  import { ReadStream, WriteStream } from './streams.js';
62
62
  import { FSWatcher, emitChange } from './watchers.js';
@@ -360,6 +360,7 @@ export async function rename(oldPath, newPath) {
360
360
  if (src.mountPoint == dst.mountPoint) {
361
361
  await src.fs.rename(src.path, dst.path);
362
362
  emitChange('rename', oldPath.toString());
363
+ emitChange('change', newPath.toString());
363
364
  return;
364
365
  }
365
366
  await writeFile(newPath, await readFile(oldPath));
@@ -435,7 +436,7 @@ export async function unlink(path) {
435
436
  path = normalizePath(path);
436
437
  const { fs, path: resolved } = resolveMount(path);
437
438
  try {
438
- if (config.checkAccess && !(await (cache.stats.get(path) || fs.stat(resolved))).hasAccess(constants.W_OK)) {
439
+ if (config.checkAccess && !(await (cache.stats.getAsync(path) || fs.stat(resolved))).hasAccess(constants.W_OK)) {
439
440
  throw ErrnoError.With('EACCES', resolved, 'unlink');
440
441
  }
441
442
  await fs.unlink(resolved);
@@ -583,7 +584,7 @@ export async function rmdir(path) {
583
584
  path = await realpath(path);
584
585
  const { fs, path: resolved } = resolveMount(path);
585
586
  try {
586
- const stats = await (cache.stats.get(path) || fs.stat(resolved));
587
+ const stats = await (cache.stats.getAsync(path) || fs.stat(resolved));
587
588
  if (!stats) {
588
589
  throw ErrnoError.With('ENOENT', path, 'rmdir');
589
590
  }
@@ -642,8 +643,8 @@ export async function readdir(path, options) {
642
643
  throw fixError(e, { [resolved]: path });
643
644
  };
644
645
  const { fs, path: resolved } = resolveMount(path);
645
- const _stats = cache.stats.get(path) || fs.stat(resolved).catch(handleError);
646
- cache.stats.set(path, _stats);
646
+ const _stats = cache.stats.getAsync(path) || fs.stat(resolved).catch(handleError);
647
+ cache.stats.setAsync(path, _stats);
647
648
  const stats = await _stats;
648
649
  if (!stats) {
649
650
  throw ErrnoError.With('ENOENT', path, 'readdir');
@@ -659,8 +660,8 @@ export async function readdir(path, options) {
659
660
  const addEntry = async (entry) => {
660
661
  let entryStats;
661
662
  if (options?.recursive || options?.withFileTypes) {
662
- const _entryStats = cache.stats.get(join(path, entry)) || fs.stat(join(resolved, entry)).catch(handleError);
663
- cache.stats.set(join(path, entry), _entryStats);
663
+ const _entryStats = cache.stats.getAsync(join(path, entry)) || fs.stat(join(resolved, entry)).catch(handleError);
664
+ cache.stats.setAsync(join(path, entry), _entryStats);
664
665
  entryStats = await _entryStats;
665
666
  }
666
667
  if (options?.withFileTypes) {
@@ -880,17 +881,23 @@ export async function lutimes(path, atime, mtime) {
880
881
  lutimes;
881
882
  export async function realpath(path, options) {
882
883
  path = normalizePath(path);
884
+ if (cache.paths.hasAsync(path))
885
+ return cache.paths.getAsync(path);
883
886
  const { base, dir } = parse(path);
884
- const lpath = join(dir == '/' ? '/' : await (cache.paths.get(dir) || realpath(dir)), base);
885
- const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
887
+ const realDir = dir == '/' ? '/' : await (cache.paths.getAsync(dir) || realpath(dir));
888
+ const lpath = join(realDir, base);
889
+ const { fs, path: resolvedPath } = resolveMount(lpath);
886
890
  try {
887
- const _stats = cache.stats.get(lpath) || fs.stat(resolvedPath);
888
- cache.stats.set(lpath, _stats);
891
+ const _stats = cache.stats.getAsync(lpath) || fs.stat(resolvedPath);
892
+ cache.stats.setAsync(lpath, _stats);
889
893
  if (!(await _stats).isSymbolicLink()) {
894
+ cache.paths.set(path, lpath);
890
895
  return lpath;
891
896
  }
892
- const target = mountPoint + (await readlink(lpath));
893
- return await (cache.paths.get(target) || realpath(target));
897
+ const target = resolve(realDir, await readlink(lpath));
898
+ const real = cache.paths.getAsync(target) || realpath(target);
899
+ cache.paths.setAsync(path, real);
900
+ return await real;
894
901
  }
895
902
  catch (e) {
896
903
  if (e.code == 'ENOENT') {
@@ -945,7 +952,7 @@ access;
945
952
  */
946
953
  export async function rm(path, options) {
947
954
  path = normalizePath(path);
948
- const stats = await (cache.stats.get(path) ||
955
+ const stats = await (cache.stats.getAsync(path) ||
949
956
  stat(path).catch((error) => {
950
957
  if (error.code == 'ENOENT' && options?.force)
951
958
  return undefined;
@@ -954,7 +961,7 @@ export async function rm(path, options) {
954
961
  if (!stats) {
955
962
  return;
956
963
  }
957
- cache.stats.setSync(path, stats);
964
+ cache.stats.set(path, stats);
958
965
  switch (stats.mode & constants.S_IFMT) {
959
966
  case constants.S_IFDIR:
960
967
  if (options?.recursive) {
@@ -966,10 +973,10 @@ export async function rm(path, options) {
966
973
  break;
967
974
  case constants.S_IFREG:
968
975
  case constants.S_IFLNK:
969
- await unlink(path);
970
- break;
971
976
  case constants.S_IFBLK:
972
977
  case constants.S_IFCHR:
978
+ await unlink(path);
979
+ break;
973
980
  case constants.S_IFIFO:
974
981
  case constants.S_IFSOCK:
975
982
  default:
@@ -4,6 +4,7 @@ import { Errno, ErrnoError } from '../error.js';
4
4
  import { normalizePath } from '../utils.js';
5
5
  import { resolve } from './path.js';
6
6
  import { size_max } from './constants.js';
7
+ import { paths as pathCache } from './cache.js';
7
8
  // descriptors
8
9
  export const fdMap = new Map();
9
10
  let nextFd = 100;
@@ -37,6 +38,7 @@ export function mount(mountPoint, fs) {
37
38
  throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already in use.');
38
39
  }
39
40
  mounts.set(mountPoint, fs);
41
+ pathCache.clear();
40
42
  }
41
43
  /**
42
44
  * Unmounts the file system at `mountPoint`.
@@ -50,6 +52,7 @@ export function umount(mountPoint) {
50
52
  throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already unmounted.');
51
53
  }
52
54
  mounts.delete(mountPoint);
55
+ pathCache.clear();
53
56
  }
54
57
  /**
55
58
  * Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root
@@ -54,7 +54,7 @@ import * as cache from './cache.js';
54
54
  import { config } from './config.js';
55
55
  import * as constants from './constants.js';
56
56
  import { Dir, Dirent } from './dir.js';
57
- import { dirname, join, parse } from './path.js';
57
+ import { dirname, join, parse, resolve } from './path.js';
58
58
  import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount } from './shared.js';
59
59
  import { emitChange } from './watchers.js';
60
60
  export function renameSync(oldPath, newPath) {
@@ -69,6 +69,7 @@ export function renameSync(oldPath, newPath) {
69
69
  if (oldMount === newMount) {
70
70
  oldMount.fs.renameSync(oldMount.path, newMount.path);
71
71
  emitChange('rename', oldPath.toString());
72
+ emitChange('change', newPath.toString());
72
73
  return;
73
74
  }
74
75
  writeFileSync(newPath, readFileSync(oldPath));
@@ -147,7 +148,7 @@ export function unlinkSync(path) {
147
148
  path = normalizePath(path);
148
149
  const { fs, path: resolved } = resolveMount(path);
149
150
  try {
150
- if (config.checkAccess && !(cache.stats.getSync(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) {
151
+ if (config.checkAccess && !(cache.stats.get(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) {
151
152
  throw ErrnoError.With('EACCES', resolved, 'unlink');
152
153
  }
153
154
  fs.unlinkSync(resolved);
@@ -393,7 +394,7 @@ export function rmdirSync(path) {
393
394
  path = normalizePath(path);
394
395
  const { fs, path: resolved } = resolveMount(realpathSync(path));
395
396
  try {
396
- const stats = cache.stats.getSync(path) || fs.statSync(resolved);
397
+ const stats = cache.stats.get(path) || fs.statSync(resolved);
397
398
  if (!stats.isDirectory()) {
398
399
  throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
399
400
  }
@@ -446,8 +447,8 @@ export function readdirSync(path, options) {
446
447
  const { fs, path: resolved } = resolveMount(realpathSync(path));
447
448
  let entries;
448
449
  try {
449
- const stats = cache.stats.getSync(path) || fs.statSync(resolved);
450
- cache.stats.setSync(path, stats);
450
+ const stats = cache.stats.get(path) || fs.statSync(resolved);
451
+ cache.stats.set(path, stats);
451
452
  if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
452
453
  throw ErrnoError.With('EACCES', resolved, 'readdir');
453
454
  }
@@ -462,8 +463,8 @@ export function readdirSync(path, options) {
462
463
  // Iterate over entries and handle recursive case if needed
463
464
  const values = [];
464
465
  for (const entry of entries) {
465
- const entryStat = cache.stats.getSync(join(path, entry)) || fs.statSync(join(resolved, entry));
466
- cache.stats.setSync(join(path, entry), entryStat);
466
+ const entryStat = cache.stats.get(join(path, entry)) || fs.statSync(join(resolved, entry));
467
+ cache.stats.set(join(path, entry), entryStat);
467
468
  if (options?.withFileTypes) {
468
469
  values.push(new Dirent(entry, entryStat));
469
470
  }
@@ -489,7 +490,7 @@ export function readdirSync(path, options) {
489
490
  }
490
491
  }
491
492
  if (!options?._isIndirect) {
492
- cache.stats.clearSync();
493
+ cache.stats.clear();
493
494
  }
494
495
  return values;
495
496
  }
@@ -592,16 +593,23 @@ export function lutimesSync(path, atime, mtime) {
592
593
  lutimesSync;
593
594
  export function realpathSync(path, options) {
594
595
  path = normalizePath(path);
596
+ if (cache.paths.has(path))
597
+ return cache.paths.get(path);
595
598
  const { base, dir } = parse(path);
596
- const lpath = join(dir == '/' ? '/' : cache.paths.getSync(dir) || realpathSync(dir), base);
597
- const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
599
+ const realDir = dir == '/' ? '/' : cache.paths.get(dir) || realpathSync(dir);
600
+ const lpath = join(realDir, base);
601
+ const { fs, path: resolvedPath } = resolveMount(lpath);
598
602
  try {
599
- const stats = fs.statSync(resolvedPath);
603
+ const stats = cache.stats.get(lpath) || fs.statSync(resolvedPath);
604
+ cache.stats.set(lpath, stats);
600
605
  if (!stats.isSymbolicLink()) {
606
+ cache.paths.set(path, lpath);
601
607
  return lpath;
602
608
  }
603
- const target = mountPoint + readlinkSync(lpath, options).toString();
604
- return cache.paths.getSync(target) || realpathSync(target);
609
+ const target = resolve(realDir, readlinkSync(lpath, options).toString());
610
+ const real = cache.paths.get(target) || realpathSync(target);
611
+ cache.paths.set(path, real);
612
+ return real;
605
613
  }
606
614
  catch (e) {
607
615
  if (e.code == 'ENOENT') {
@@ -627,7 +635,7 @@ export function rmSync(path, options) {
627
635
  path = normalizePath(path);
628
636
  let stats;
629
637
  try {
630
- stats = cache.stats.getSync(path) || statSync(path);
638
+ stats = cache.stats.get(path) || statSync(path);
631
639
  }
632
640
  catch (error) {
633
641
  if (error.code != 'ENOENT' || !options?.force)
@@ -636,7 +644,7 @@ export function rmSync(path, options) {
636
644
  if (!stats) {
637
645
  return;
638
646
  }
639
- cache.stats.setSync(path, stats);
647
+ cache.stats.set(path, stats);
640
648
  switch (stats.mode & constants.S_IFMT) {
641
649
  case constants.S_IFDIR:
642
650
  if (options?.recursive) {
@@ -648,18 +656,18 @@ export function rmSync(path, options) {
648
656
  break;
649
657
  case constants.S_IFREG:
650
658
  case constants.S_IFLNK:
651
- unlinkSync(path);
652
- break;
653
659
  case constants.S_IFBLK:
654
660
  case constants.S_IFCHR:
661
+ unlinkSync(path);
662
+ break;
655
663
  case constants.S_IFIFO:
656
664
  case constants.S_IFSOCK:
657
665
  default:
658
- cache.stats.clearSync();
666
+ cache.stats.clear();
659
667
  throw new ErrnoError(Errno.EPERM, 'File type not supported', path, 'rm');
660
668
  }
661
669
  if (!options?._isIndirect) {
662
- cache.stats.clearSync();
670
+ cache.stats.clear();
663
671
  }
664
672
  }
665
673
  rmSync;
package/dist/stats.js CHANGED
@@ -109,10 +109,8 @@ export class StatsCommon {
109
109
  * @internal
110
110
  */
111
111
  hasAccess(mode) {
112
- if (credentials.euid === 0 || credentials.egid === 0) {
113
- // Running as root
112
+ if (this.isSymbolicLink() || credentials.euid === 0 || credentials.egid === 0)
114
113
  return true;
115
- }
116
114
  let perm = 0;
117
115
  // Owner permissions
118
116
  if (credentials.uid === this.uid) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -74,10 +74,12 @@
74
74
  "@types/readable-stream": "^4.0.10",
75
75
  "buffer": "^6.0.3",
76
76
  "eventemitter3": "^5.0.1",
77
- "minimatch": "^9.0.3",
78
77
  "readable-stream": "^4.5.2",
79
78
  "utilium": "^1.0.0"
80
79
  },
80
+ "optionalDependencies": {
81
+ "minimatch": "^9.0.3"
82
+ },
81
83
  "devDependencies": {
82
84
  "@eslint/js": "^9.8.0",
83
85
  "@types/eslint__js": "^8.42.3",
package/readme.md CHANGED
@@ -64,6 +64,8 @@ await configure({
64
64
  };
65
65
  ```
66
66
 
67
+ Note that while you aren't required to use absolute paths for the keys of `mounts`, it is a good practice to do so.
68
+
67
69
  > [!TIP]
68
70
  > When configuring a mount point, you can pass in
69
71
  >
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { readdirSync, statSync, writeFileSync } from 'node:fs';
3
- import { minimatch } from 'minimatch';
4
- import { join, relative, resolve } from 'node:path/posix';
3
+ import _path from 'node:path/posix';
5
4
  import { parseArgs } from 'node:util';
6
5
 
7
6
  const { values: options, positionals } = parseArgs({
@@ -37,6 +36,24 @@ if (options.quiet && options.verbose) {
37
36
  process.exit();
38
37
  }
39
38
 
39
+ let matchesGlob = _path.matchesGlob;
40
+
41
+ if (matchesGlob && options.verbose) {
42
+ console.debug('[debug] path.matchesGlob is available.');
43
+ }
44
+
45
+ if (!matchesGlob) {
46
+ console.warn('Warning: path.matchesGlob is not available, falling back to minimatch. (Node 20.17.0+ or 22.5.0+ needed)');
47
+
48
+ try {
49
+ const { minimatch } = await import('minimatch');
50
+ matchesGlob = minimatch;
51
+ } catch {
52
+ console.error('Fatal error: Failed to fall back to minimatch (is it installed?)');
53
+ process.exit(1);
54
+ }
55
+ }
56
+
40
57
  function fixSlash(path) {
41
58
  return path.replaceAll('\\', '/');
42
59
  }
@@ -71,7 +88,7 @@ const entries = new Map();
71
88
 
72
89
  function computeEntries(path) {
73
90
  try {
74
- if (options.ignore.some(pattern => minimatch(path, pattern))) {
91
+ if (options.ignore.some(pattern => matchesGlob(path, pattern))) {
75
92
  if (!options.quiet) console.log(`${color('yellow', 'skip')} ${path}`);
76
93
  return;
77
94
  }
@@ -79,7 +96,7 @@ function computeEntries(path) {
79
96
  const stats = statSync(path);
80
97
 
81
98
  if (stats.isFile()) {
82
- entries.set('/' + relative(resolvedRoot, path), stats);
99
+ entries.set('/' + _path.relative(resolvedRoot, path), stats);
83
100
  if (options.verbose) {
84
101
  console.log(`${color('green', 'file')} ${path}`);
85
102
  }
@@ -87,9 +104,9 @@ function computeEntries(path) {
87
104
  }
88
105
 
89
106
  for (const file of readdirSync(path)) {
90
- computeEntries(join(path, file));
107
+ computeEntries(_path.join(path, file));
91
108
  }
92
- entries.set('/' + relative(resolvedRoot, path), stats);
109
+ entries.set('/' + _path.relative(resolvedRoot, path), stats);
93
110
  if (options.verbose) {
94
111
  console.log(`${color('bright_green', ' dir')} ${path}`);
95
112
  }
@@ -102,7 +119,7 @@ function computeEntries(path) {
102
119
 
103
120
  computeEntries(resolvedRoot);
104
121
  if (!options.quiet) {
105
- console.log('Generated listing for ' + fixSlash(resolve(root)));
122
+ console.log('Generated listing for ' + fixSlash(_path.resolve(root)));
106
123
  }
107
124
 
108
125
  const index = {
package/src/config.ts CHANGED
@@ -5,7 +5,6 @@ import { DeviceFS } from './devices.js';
5
5
  import * as cache from './emulation/cache.js';
6
6
  import { config } from './emulation/config.js';
7
7
  import * as fs from './emulation/index.js';
8
- import type { AbsolutePath } from './emulation/path.js';
9
8
  import { Errno, ErrnoError } from './error.js';
10
9
  import { FileSystem } from './filesystem.js';
11
10
 
@@ -68,8 +67,11 @@ export async function resolveMountConfig<T extends Backend>(configuration: Mount
68
67
  return mount;
69
68
  }
70
69
 
70
+ /**
71
+ * An object mapping mount points to backends
72
+ */
71
73
  export interface ConfigMounts {
72
- [K: AbsolutePath]: Backend;
74
+ [K: string]: Backend;
73
75
  }
74
76
 
75
77
  /**
@@ -79,7 +81,7 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
79
81
  /**
80
82
  * An object mapping mount points to mount configuration
81
83
  */
82
- mounts: { [K in keyof T & AbsolutePath]: MountConfiguration<T[K]> };
84
+ mounts: { [K in keyof T]: MountConfiguration<T[K]> };
83
85
 
84
86
  /**
85
87
  * The uid to use
@@ -200,10 +202,8 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
200
202
  const toMount: [string, FileSystem][] = [];
201
203
  let unmountRoot = false;
202
204
 
203
- for (const [point, mountConfig] of Object.entries(configuration.mounts)) {
204
- if (!point.startsWith('/')) {
205
- throw new ErrnoError(Errno.EINVAL, 'Mount points must have absolute paths');
206
- }
205
+ for (const [_point, mountConfig] of Object.entries(configuration.mounts)) {
206
+ const point = _point.startsWith('/') ? _point : '/' + _point;
207
207
 
208
208
  if (isBackendConfig(mountConfig)) {
209
209
  mountConfig.disableAsyncCache ??= configuration.disableAsyncCache || false;
@@ -13,10 +13,17 @@ export class Cache<T> {
13
13
 
14
14
  protected async = new Map<string, Promise<T>>();
15
15
 
16
+ /**
17
+ * Whether the data exists in the cache
18
+ */
19
+ has(path: string): boolean {
20
+ return this.isEnabled && this.sync.has(path);
21
+ }
22
+
16
23
  /**
17
24
  * Gets data from the cache, if is exists and the cache is enabled.
18
25
  */
19
- getSync(path: string): T | undefined {
26
+ get(path: string): T | undefined {
20
27
  if (!this.isEnabled) return;
21
28
 
22
29
  return this.sync.get(path);
@@ -25,7 +32,7 @@ export class Cache<T> {
25
32
  /**
26
33
  * Adds data if the cache is enabled
27
34
  */
28
- setSync(path: string, value: T): void {
35
+ set(path: string, value: T): void {
29
36
  if (!this.isEnabled) return;
30
37
 
31
38
  this.sync.set(path, value);
@@ -33,18 +40,16 @@ export class Cache<T> {
33
40
  }
34
41
 
35
42
  /**
36
- * Clears the cache if it is enabled
43
+ * Whether the data exists in the cache
37
44
  */
38
- clearSync(): void {
39
- if (!this.isEnabled) return;
40
-
41
- this.sync.clear();
45
+ hasAsync(path: string): boolean {
46
+ return this.isEnabled && this.async.has(path);
42
47
  }
43
48
 
44
49
  /**
45
50
  * Gets data from the cache, if it exists and the cache is enabled.
46
51
  */
47
- get(path: string): Promise<T> | undefined {
52
+ getAsync(path: string): Promise<T> | undefined {
48
53
  if (!this.isEnabled) return;
49
54
 
50
55
  return this.async.get(path);
@@ -53,7 +58,7 @@ export class Cache<T> {
53
58
  /**
54
59
  * Adds data if the cache is enabled
55
60
  */
56
- set(path: string, value: Promise<T>): void {
61
+ setAsync(path: string, value: Promise<T>): void {
57
62
  if (!this.isEnabled) return;
58
63
 
59
64
  this.async.set(path, value);
@@ -65,7 +70,7 @@ export class Cache<T> {
65
70
  */
66
71
  clear(): void {
67
72
  if (!this.isEnabled) return;
68
-
73
+ this.sync.clear();
69
74
  this.async.clear();
70
75
  }
71
76
  }
@@ -16,7 +16,7 @@ import * as cache from './cache.js';
16
16
  import { config } from './config.js';
17
17
  import * as constants from './constants.js';
18
18
  import { Dir, Dirent } from './dir.js';
19
- import { dirname, join, parse } from './path.js';
19
+ import { dirname, join, parse, resolve } from './path.js';
20
20
  import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type InternalOptions, type ReaddirOptions } from './shared.js';
21
21
  import { ReadStream, WriteStream } from './streams.js';
22
22
  import { FSWatcher, emitChange } from './watchers.js';
@@ -395,6 +395,7 @@ export async function rename(oldPath: fs.PathLike, newPath: fs.PathLike): Promis
395
395
  if (src.mountPoint == dst.mountPoint) {
396
396
  await src.fs.rename(src.path, dst.path);
397
397
  emitChange('rename', oldPath.toString());
398
+ emitChange('change', newPath.toString());
398
399
  return;
399
400
  }
400
401
  await writeFile(newPath, await readFile(oldPath));
@@ -471,7 +472,7 @@ export async function unlink(path: fs.PathLike): Promise<void> {
471
472
  path = normalizePath(path);
472
473
  const { fs, path: resolved } = resolveMount(path);
473
474
  try {
474
- if (config.checkAccess && !(await (cache.stats.get(path) || fs.stat(resolved))).hasAccess(constants.W_OK)) {
475
+ if (config.checkAccess && !(await (cache.stats.getAsync(path) || fs.stat(resolved))).hasAccess(constants.W_OK)) {
475
476
  throw ErrnoError.With('EACCES', resolved, 'unlink');
476
477
  }
477
478
  await fs.unlink(resolved);
@@ -623,7 +624,7 @@ export async function rmdir(path: fs.PathLike): Promise<void> {
623
624
  path = await realpath(path);
624
625
  const { fs, path: resolved } = resolveMount(path);
625
626
  try {
626
- const stats = await (cache.stats.get(path) || fs.stat(resolved));
627
+ const stats = await (cache.stats.getAsync(path) || fs.stat(resolved));
627
628
  if (!stats) {
628
629
  throw ErrnoError.With('ENOENT', path, 'rmdir');
629
630
  }
@@ -718,8 +719,8 @@ export async function readdir(
718
719
 
719
720
  const { fs, path: resolved } = resolveMount(path);
720
721
 
721
- const _stats = cache.stats.get(path) || fs.stat(resolved).catch(handleError);
722
- cache.stats.set(path, _stats);
722
+ const _stats = cache.stats.getAsync(path) || fs.stat(resolved).catch(handleError);
723
+ cache.stats.setAsync(path, _stats);
723
724
  const stats = await _stats;
724
725
 
725
726
  if (!stats) {
@@ -740,8 +741,8 @@ export async function readdir(
740
741
  const addEntry = async (entry: string) => {
741
742
  let entryStats: Stats | undefined;
742
743
  if (options?.recursive || options?.withFileTypes) {
743
- const _entryStats = cache.stats.get(join(path, entry)) || fs.stat(join(resolved, entry)).catch(handleError);
744
- cache.stats.set(join(path, entry), _entryStats);
744
+ const _entryStats = cache.stats.getAsync(join(path, entry)) || fs.stat(join(resolved, entry)).catch(handleError);
745
+ cache.stats.setAsync(join(path, entry), _entryStats);
745
746
  entryStats = await _entryStats;
746
747
  }
747
748
  if (options?.withFileTypes) {
@@ -891,20 +892,25 @@ export async function realpath(path: fs.PathLike, options: fs.BufferEncodingOpti
891
892
  export async function realpath(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding): Promise<string>;
892
893
  export async function realpath(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Promise<string | Buffer> {
893
894
  path = normalizePath(path);
895
+ if (cache.paths.hasAsync(path)) return cache.paths.getAsync(path)!;
894
896
  const { base, dir } = parse(path);
895
- const lpath = join(dir == '/' ? '/' : await (cache.paths.get(dir) || realpath(dir)), base);
896
- const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
897
+ const realDir = dir == '/' ? '/' : await (cache.paths.getAsync(dir) || realpath(dir));
898
+ const lpath = join(realDir, base);
899
+ const { fs, path: resolvedPath } = resolveMount(lpath);
897
900
 
898
901
  try {
899
- const _stats = cache.stats.get(lpath) || fs.stat(resolvedPath);
900
- cache.stats.set(lpath, _stats);
902
+ const _stats = cache.stats.getAsync(lpath) || fs.stat(resolvedPath);
903
+ cache.stats.setAsync(lpath, _stats);
901
904
  if (!(await _stats).isSymbolicLink()) {
905
+ cache.paths.set(path, lpath);
902
906
  return lpath;
903
907
  }
904
908
 
905
- const target = mountPoint + (await readlink(lpath));
909
+ const target = resolve(realDir, await readlink(lpath));
906
910
 
907
- return await (cache.paths.get(target) || realpath(target));
911
+ const real = cache.paths.getAsync(target) || realpath(target);
912
+ cache.paths.setAsync(path, real);
913
+ return await real;
908
914
  } catch (e) {
909
915
  if ((e as ErrnoError).code == 'ENOENT') {
910
916
  return path;
@@ -968,7 +974,7 @@ access satisfies typeof promises.access;
968
974
  export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOptions) {
969
975
  path = normalizePath(path);
970
976
 
971
- const stats = await (cache.stats.get(path) ||
977
+ const stats = await (cache.stats.getAsync(path) ||
972
978
  stat(path).catch((error: ErrnoError) => {
973
979
  if (error.code == 'ENOENT' && options?.force) return undefined;
974
980
  throw error;
@@ -978,7 +984,7 @@ export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOpt
978
984
  return;
979
985
  }
980
986
 
981
- cache.stats.setSync(path, stats);
987
+ cache.stats.set(path, stats);
982
988
 
983
989
  switch (stats.mode & constants.S_IFMT) {
984
990
  case constants.S_IFDIR:
@@ -992,10 +998,10 @@ export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOpt
992
998
  break;
993
999
  case constants.S_IFREG:
994
1000
  case constants.S_IFLNK:
995
- await unlink(path);
996
- break;
997
1001
  case constants.S_IFBLK:
998
1002
  case constants.S_IFCHR:
1003
+ await unlink(path);
1004
+ break;
999
1005
  case constants.S_IFIFO:
1000
1006
  case constants.S_IFSOCK:
1001
1007
  default:
@@ -8,6 +8,7 @@ import type { FileSystem } from '../filesystem.js';
8
8
  import { normalizePath } from '../utils.js';
9
9
  import { resolve, type AbsolutePath } from './path.js';
10
10
  import { size_max } from './constants.js';
11
+ import { paths as pathCache } from './cache.js';
11
12
 
12
13
  // descriptors
13
14
  export const fdMap: Map<number, File> = new Map();
@@ -47,6 +48,7 @@ export function mount(mountPoint: string, fs: FileSystem): void {
47
48
  throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already in use.');
48
49
  }
49
50
  mounts.set(mountPoint, fs);
51
+ pathCache.clear();
50
52
  }
51
53
 
52
54
  /**
@@ -61,6 +63,7 @@ export function umount(mountPoint: string): void {
61
63
  throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already unmounted.');
62
64
  }
63
65
  mounts.delete(mountPoint);
66
+ pathCache.clear();
64
67
  }
65
68
 
66
69
  /**
@@ -10,7 +10,7 @@ import * as cache from './cache.js';
10
10
  import { config } from './config.js';
11
11
  import * as constants from './constants.js';
12
12
  import { Dir, Dirent } from './dir.js';
13
- import { dirname, join, parse } from './path.js';
13
+ import { dirname, join, parse, resolve } from './path.js';
14
14
  import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type InternalOptions, type ReaddirOptions } from './shared.js';
15
15
  import { emitChange } from './watchers.js';
16
16
 
@@ -26,6 +26,7 @@ export function renameSync(oldPath: fs.PathLike, newPath: fs.PathLike): void {
26
26
  if (oldMount === newMount) {
27
27
  oldMount.fs.renameSync(oldMount.path, newMount.path);
28
28
  emitChange('rename', oldPath.toString());
29
+ emitChange('change', newPath.toString());
29
30
  return;
30
31
  }
31
32
 
@@ -106,7 +107,7 @@ export function unlinkSync(path: fs.PathLike): void {
106
107
  path = normalizePath(path);
107
108
  const { fs, path: resolved } = resolveMount(path);
108
109
  try {
109
- if (config.checkAccess && !(cache.stats.getSync(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) {
110
+ if (config.checkAccess && !(cache.stats.get(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) {
110
111
  throw ErrnoError.With('EACCES', resolved, 'unlink');
111
112
  }
112
113
  fs.unlinkSync(resolved);
@@ -386,7 +387,7 @@ export function rmdirSync(path: fs.PathLike): void {
386
387
  path = normalizePath(path);
387
388
  const { fs, path: resolved } = resolveMount(realpathSync(path));
388
389
  try {
389
- const stats = cache.stats.getSync(path) || fs.statSync(resolved);
390
+ const stats = cache.stats.get(path) || fs.statSync(resolved);
390
391
  if (!stats.isDirectory()) {
391
392
  throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
392
393
  }
@@ -459,8 +460,8 @@ export function readdirSync(
459
460
  const { fs, path: resolved } = resolveMount(realpathSync(path));
460
461
  let entries: string[];
461
462
  try {
462
- const stats = cache.stats.getSync(path) || fs.statSync(resolved);
463
- cache.stats.setSync(path, stats);
463
+ const stats = cache.stats.get(path) || fs.statSync(resolved);
464
+ cache.stats.set(path, stats);
464
465
  if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
465
466
  throw ErrnoError.With('EACCES', resolved, 'readdir');
466
467
  }
@@ -475,8 +476,8 @@ export function readdirSync(
475
476
  // Iterate over entries and handle recursive case if needed
476
477
  const values: (string | Dirent | Buffer)[] = [];
477
478
  for (const entry of entries) {
478
- const entryStat = cache.stats.getSync(join(path, entry)) || fs.statSync(join(resolved, entry));
479
- cache.stats.setSync(join(path, entry), entryStat);
479
+ const entryStat = cache.stats.get(join(path, entry)) || fs.statSync(join(resolved, entry));
480
+ cache.stats.set(join(path, entry), entryStat);
480
481
 
481
482
  if (options?.withFileTypes) {
482
483
  values.push(new Dirent(entry, entryStat));
@@ -500,7 +501,7 @@ export function readdirSync(
500
501
  }
501
502
 
502
503
  if (!options?._isIndirect) {
503
- cache.stats.clearSync();
504
+ cache.stats.clear();
504
505
  }
505
506
  return values as string[] | Dirent[] | Buffer[];
506
507
  }
@@ -621,18 +622,24 @@ export function realpathSync(path: fs.PathLike, options: fs.BufferEncodingOption
621
622
  export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption): string;
622
623
  export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption | fs.BufferEncodingOption): string | Buffer {
623
624
  path = normalizePath(path);
625
+ if (cache.paths.has(path)) return cache.paths.get(path)!;
624
626
  const { base, dir } = parse(path);
625
- const lpath = join(dir == '/' ? '/' : cache.paths.getSync(dir) || realpathSync(dir), base);
626
- const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
627
+ const realDir = dir == '/' ? '/' : cache.paths.get(dir) || realpathSync(dir);
628
+ const lpath = join(realDir, base);
629
+ const { fs, path: resolvedPath } = resolveMount(lpath);
627
630
 
628
631
  try {
629
- const stats = fs.statSync(resolvedPath);
632
+ const stats = cache.stats.get(lpath) || fs.statSync(resolvedPath);
633
+ cache.stats.set(lpath, stats);
630
634
  if (!stats.isSymbolicLink()) {
635
+ cache.paths.set(path, lpath);
631
636
  return lpath;
632
637
  }
633
638
 
634
- const target = mountPoint + readlinkSync(lpath, options).toString();
635
- return cache.paths.getSync(target) || realpathSync(target);
639
+ const target = resolve(realDir, readlinkSync(lpath, options).toString());
640
+ const real = cache.paths.get(target) || realpathSync(target);
641
+ cache.paths.set(path, real);
642
+ return real;
636
643
  } catch (e) {
637
644
  if ((e as ErrnoError).code == 'ENOENT') {
638
645
  return path;
@@ -659,7 +666,7 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptio
659
666
 
660
667
  let stats: Stats | undefined;
661
668
  try {
662
- stats = cache.stats.getSync(path) || statSync(path);
669
+ stats = cache.stats.get(path) || statSync(path);
663
670
  } catch (error) {
664
671
  if ((error as ErrnoError).code != 'ENOENT' || !options?.force) throw error;
665
672
  }
@@ -668,7 +675,7 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptio
668
675
  return;
669
676
  }
670
677
 
671
- cache.stats.setSync(path, stats);
678
+ cache.stats.set(path, stats);
672
679
 
673
680
  switch (stats.mode & constants.S_IFMT) {
674
681
  case constants.S_IFDIR:
@@ -682,19 +689,19 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptio
682
689
  break;
683
690
  case constants.S_IFREG:
684
691
  case constants.S_IFLNK:
685
- unlinkSync(path);
686
- break;
687
692
  case constants.S_IFBLK:
688
693
  case constants.S_IFCHR:
694
+ unlinkSync(path);
695
+ break;
689
696
  case constants.S_IFIFO:
690
697
  case constants.S_IFSOCK:
691
698
  default:
692
- cache.stats.clearSync();
699
+ cache.stats.clear();
693
700
  throw new ErrnoError(Errno.EPERM, 'File type not supported', path, 'rm');
694
701
  }
695
702
 
696
703
  if (!options?._isIndirect) {
697
- cache.stats.clearSync();
704
+ cache.stats.clear();
698
705
  }
699
706
  }
700
707
  rmSync satisfies typeof fs.rmSync;
package/src/stats.ts CHANGED
@@ -247,10 +247,7 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
247
247
  * @internal
248
248
  */
249
249
  public hasAccess(mode: number): boolean {
250
- if (credentials.euid === 0 || credentials.egid === 0) {
251
- // Running as root
252
- return true;
253
- }
250
+ if (this.isSymbolicLink() || credentials.euid === 0 || credentials.egid === 0) return true;
254
251
 
255
252
  let perm = 0;
256
253
 
@@ -23,6 +23,10 @@ suite('Links', () => {
23
23
  assert.strictEqual(destination, target);
24
24
  });
25
25
 
26
+ test('read target contents', async () => {
27
+ assert.equal(await fs.promises.readFile(target, 'utf-8'), await fs.promises.readFile(symlink, 'utf-8'));
28
+ });
29
+
26
30
  test('unlink', async () => {
27
31
  await fs.promises.unlink(symlink);
28
32
  assert(!(await fs.promises.exists(symlink)));
@@ -71,18 +71,29 @@ suite('Watch Features', () => {
71
71
  });
72
72
 
73
73
  test('fs.watch should detect file renames', async () => {
74
- const oldFile = `${testDir}/oldFile.txt`;
75
- const newFile = `${testDir}/newFile.txt`;
74
+ const oldFileName = `oldFile.txt`;
75
+ const newFileName = `newFile.txt`;
76
+ const oldFile = `${testDir}/${oldFileName}`;
77
+ const newFile = `${testDir}/${newFileName}`;
76
78
 
77
79
  await fs.promises.writeFile(oldFile, 'Some content');
80
+ const oldFileResolver = Promise.withResolvers<void>();
81
+ const newFileResolver = Promise.withResolvers<void>();
78
82
 
83
+ const fileResolvers: Record<string, { resolver: PromiseWithResolvers<void>; eventType: string }> = {
84
+ [oldFileName]: { resolver: oldFileResolver, eventType: 'rename' },
85
+ [newFileName]: { resolver: newFileResolver, eventType: 'change' },
86
+ };
79
87
  using watcher = fs.watch(testDir, (eventType, filename) => {
80
- assert.strictEqual(eventType, 'rename');
81
- assert.strictEqual(filename, 'oldFile.txt');
88
+ const resolver = fileResolvers[filename];
89
+ assert.notEqual(resolver, undefined); // should have a resolver so file is expected
90
+ assert.strictEqual(eventType, resolver.eventType);
91
+ resolver.resolver.resolve();
82
92
  });
83
93
 
84
94
  // Rename the file to trigger the event
85
95
  await fs.promises.rename(oldFile, newFile);
96
+ await Promise.all([newFileResolver.promise, oldFileResolver.promise]);
86
97
  });
87
98
 
88
99
  test('fs.watch should detect file deletions', async () => {