@zenfs/core 1.1.6 → 1.2.1

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 (83) 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 +24 -1
  5. package/dist/config.js +5 -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/config.d.ts +10 -0
  10. package/dist/emulation/config.js +10 -0
  11. package/dist/emulation/promises.d.ts +9 -14
  12. package/dist/emulation/promises.js +71 -47
  13. package/dist/emulation/shared.d.ts +16 -0
  14. package/dist/emulation/sync.d.ts +11 -20
  15. package/dist/emulation/sync.js +44 -22
  16. package/dist/file.d.ts +1 -1
  17. package/dist/file.js +6 -3
  18. package/package.json +4 -2
  19. package/readme.md +1 -1
  20. package/scripts/test.js +19 -1
  21. package/src/backends/backend.ts +160 -0
  22. package/src/backends/fetch.ts +180 -0
  23. package/src/backends/file_index.ts +206 -0
  24. package/src/backends/memory.ts +50 -0
  25. package/src/backends/overlay.ts +560 -0
  26. package/src/backends/port/fs.ts +335 -0
  27. package/src/backends/port/readme.md +54 -0
  28. package/src/backends/port/rpc.ts +167 -0
  29. package/src/backends/readme.md +3 -0
  30. package/src/backends/store/fs.ts +700 -0
  31. package/src/backends/store/readme.md +9 -0
  32. package/src/backends/store/simple.ts +146 -0
  33. package/src/backends/store/store.ts +173 -0
  34. package/src/config.ts +185 -0
  35. package/src/credentials.ts +31 -0
  36. package/src/devices.ts +459 -0
  37. package/src/emulation/async.ts +834 -0
  38. package/src/emulation/cache.ts +44 -0
  39. package/src/emulation/config.ts +11 -0
  40. package/src/emulation/constants.ts +182 -0
  41. package/src/emulation/dir.ts +138 -0
  42. package/src/emulation/index.ts +8 -0
  43. package/src/emulation/path.ts +440 -0
  44. package/src/emulation/promises.ts +1134 -0
  45. package/src/emulation/shared.ts +153 -0
  46. package/src/emulation/streams.ts +34 -0
  47. package/src/emulation/sync.ts +868 -0
  48. package/src/emulation/watchers.ts +193 -0
  49. package/src/error.ts +307 -0
  50. package/src/file.ts +662 -0
  51. package/src/filesystem.ts +174 -0
  52. package/src/index.ts +25 -0
  53. package/src/inode.ts +132 -0
  54. package/src/mixins/async.ts +208 -0
  55. package/src/mixins/index.ts +5 -0
  56. package/src/mixins/mutexed.ts +257 -0
  57. package/src/mixins/readonly.ts +96 -0
  58. package/src/mixins/shared.ts +25 -0
  59. package/src/mixins/sync.ts +58 -0
  60. package/src/polyfills.ts +21 -0
  61. package/src/stats.ts +363 -0
  62. package/src/utils.ts +288 -0
  63. package/tests/common.ts +1 -11
  64. package/tests/fs/directory.test.ts +1 -1
  65. package/tests/fs/errors.test.ts +1 -1
  66. package/tests/fs/links.test.ts +1 -1
  67. package/tests/fs/open.test.ts +1 -1
  68. package/tests/fs/permissions.test.ts +4 -4
  69. package/tests/fs/readdir.test.ts +3 -3
  70. package/tests/fs/rename.test.ts +1 -1
  71. package/tests/fs/stat.test.ts +1 -1
  72. package/tests/fs/times.test.ts +2 -2
  73. package/tests/fs/truncate.test.ts +1 -1
  74. package/tests/port/channel.test.ts +3 -3
  75. package/tests/port/config.test.ts +4 -5
  76. package/tests/port/config.worker.js +5 -0
  77. package/tests/port/remote.test.ts +4 -5
  78. package/tests/port/remote.worker.js +5 -0
  79. package/tests/port/timeout.test.ts +2 -2
  80. package/tests/setup/common.ts +1 -1
  81. package/tests/setup/cow+fetch.ts +1 -1
  82. package/tests/port/config.worker.ts +0 -5
  83. package/tests/port/remote.worker.ts +0 -5
@@ -151,9 +151,6 @@ export class IndexFS extends Readonly(FileSystem) {
151
151
  if (!stats) {
152
152
  throw ErrnoError.With('ENOENT', path, 'readdir');
153
153
  }
154
- if (!stats.isDirectory()) {
155
- throw ErrnoError.With('ENOTDIR', path, 'readdir');
156
- }
157
154
  const content = JSON.parse(decodeUTF8(stats.fileData));
158
155
  if (!Array.isArray(content)) {
159
156
  throw ErrnoError.With('ENODATA', path, 'readdir');
@@ -314,10 +314,6 @@ export class UnmutexedOverlayFS extends FileSystem {
314
314
  }
315
315
  async readdir(path) {
316
316
  this.checkInitialized();
317
- const dirStats = await this.stat(path);
318
- if (!dirStats.isDirectory()) {
319
- throw ErrnoError.With('ENOTDIR', path, 'readdir');
320
- }
321
317
  // Readdir in both, check delete log on RO file system's listing, merge, return.
322
318
  const contents = [];
323
319
  try {
@@ -341,10 +337,6 @@ export class UnmutexedOverlayFS extends FileSystem {
341
337
  }
342
338
  readdirSync(path) {
343
339
  this.checkInitialized();
344
- const dirStats = this.statSync(path);
345
- if (!dirStats.isDirectory()) {
346
- throw ErrnoError.With('ENOTDIR', path, 'readdir');
347
- }
348
340
  // Readdir in both, check delete log on RO file system's listing, merge, return.
349
341
  let contents = [];
350
342
  try {
@@ -580,16 +580,12 @@ export class StoreFS extends FileSystem {
580
580
  * listing.
581
581
  */
582
582
  async getDirListing(tx, inode, path) {
583
- if (!inode.toStats().isDirectory()) {
584
- throw ErrnoError.With('ENOTDIR', path, 'getDirListing');
585
- }
586
583
  const data = await tx.get(inode.ino);
584
+ /*
585
+ Occurs when data is undefined,or corresponds to something other than a directory listing.
586
+ The latter should never occur unless the file system is corrupted.
587
+ */
587
588
  if (!data) {
588
- /*
589
- Occurs when data is undefined, or corresponds to something other
590
- than a directory listing. The latter should never occur unless
591
- the file system is corrupted.
592
- */
593
589
  throw ErrnoError.With('ENOENT', path, 'getDirListing');
594
590
  }
595
591
  return decodeDirListing(data);
@@ -598,9 +594,6 @@ export class StoreFS extends FileSystem {
598
594
  * Given the Inode of a directory, retrieves the corresponding directory listing.
599
595
  */
600
596
  getDirListingSync(tx, inode, p) {
601
- if (!inode.toStats().isDirectory()) {
602
- throw ErrnoError.With('ENOTDIR', p, 'getDirListing');
603
- }
604
597
  const data = tx.getSync(inode.ino);
605
598
  if (!data) {
606
599
  throw ErrnoError.With('ENOENT', p, 'getDirListing');
@@ -758,9 +751,6 @@ export class StoreFS extends FileSystem {
758
751
  if (!isDir && fileNode.toStats().isDirectory()) {
759
752
  throw ErrnoError.With('EISDIR', path, 'remove');
760
753
  }
761
- if (isDir && !fileNode.toStats().isDirectory()) {
762
- throw ErrnoError.With('ENOTDIR', path, 'remove');
763
- }
764
754
  await tx.set(parentNode.ino, encodeDirListing(listing));
765
755
  if (--fileNode.nlink < 1) {
766
756
  // remove file
@@ -801,9 +791,6 @@ export class StoreFS extends FileSystem {
801
791
  if (!isDir && fileNode.toStats().isDirectory()) {
802
792
  throw ErrnoError.With('EISDIR', path, 'remove');
803
793
  }
804
- if (isDir && !fileNode.toStats().isDirectory()) {
805
- throw ErrnoError.With('ENOTDIR', path, 'remove');
806
- }
807
794
  // Update directory listing.
808
795
  tx.setSync(parentNode.ino, encodeDirListing(listing));
809
796
  if (--fileNode.nlink < 1) {
package/dist/config.d.ts CHANGED
@@ -34,10 +34,33 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
34
34
  gid: number;
35
35
  /**
36
36
  * Whether to automatically add normal Linux devices
37
- * @default false
38
37
  * @experimental
38
+ * @default false
39
39
  */
40
40
  addDevices: boolean;
41
+ /**
42
+ * If true, enables caching stats for certain operations.
43
+ * This should reduce the number of stat calls performed.
44
+ * @experimental
45
+ * @default false
46
+ */
47
+ cacheStats: boolean;
48
+ /**
49
+ * If true, disables *all* permissions checking.
50
+ *
51
+ * This can increase performance.
52
+ * @experimental
53
+ * @default false
54
+ */
55
+ disableAccessChecks: boolean;
56
+ /**
57
+ * If true, disables `read` and `readSync` from immediately syncing the updated atime to the file system.
58
+ *
59
+ * This can increase performance.
60
+ * @experimental
61
+ * @default false
62
+ */
63
+ disableSyncOnRead: boolean;
41
64
  }
42
65
  /**
43
66
  * Configures ZenFS with single mount point /
package/dist/config.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { checkOptions, isBackend, isBackendConfig } from './backends/backend.js';
2
2
  import { credentials } from './credentials.js';
3
3
  import { DeviceFS, fullDevice, nullDevice, randomDevice, zeroDevice } from './devices.js';
4
+ import * as cache from './emulation/cache.js';
4
5
  import * as fs from './emulation/index.js';
6
+ import { config } from './emulation/config.js';
5
7
  import { Errno, ErrnoError } from './error.js';
6
8
  import { FileSystem } from './filesystem.js';
7
9
  function isMountConfig(arg) {
@@ -66,6 +68,9 @@ export async function configure(configuration) {
66
68
  const uid = 'uid' in configuration ? configuration.uid || 0 : 0;
67
69
  const gid = 'gid' in configuration ? configuration.gid || 0 : 0;
68
70
  Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
71
+ cache.setEnabled(configuration.cacheStats ?? false);
72
+ config.checkAccess = !configuration.disableAccessChecks;
73
+ config.syncOnRead = !configuration.disableSyncOnRead;
69
74
  if (configuration.addDevices) {
70
75
  const devfs = new DeviceFS();
71
76
  devfs.createDevice('/null', nullDevice);
package/dist/devices.js CHANGED
@@ -254,15 +254,9 @@ export class DeviceFS extends StoreFS {
254
254
  return super.unlinkSync(path);
255
255
  }
256
256
  async rmdir(path) {
257
- if (this.devices.has(path)) {
258
- throw ErrnoError.With('ENOTDIR', path, 'rmdir');
259
- }
260
257
  return super.rmdir(path);
261
258
  }
262
259
  rmdirSync(path) {
263
- if (this.devices.has(path)) {
264
- throw ErrnoError.With('ENOTDIR', path, 'rmdir');
265
- }
266
260
  return super.rmdirSync(path);
267
261
  }
268
262
  async mkdir(path, mode) {
@@ -278,9 +272,6 @@ export class DeviceFS extends StoreFS {
278
272
  return super.mkdirSync(path, mode);
279
273
  }
280
274
  async readdir(path) {
281
- if (this.devices.has(path)) {
282
- throw ErrnoError.With('ENOTDIR', path, 'readdir');
283
- }
284
275
  const entries = await super.readdir(path);
285
276
  for (const dev of this.devices.keys()) {
286
277
  if (dirname(dev) == path) {
@@ -290,9 +281,6 @@ export class DeviceFS extends StoreFS {
290
281
  return entries;
291
282
  }
292
283
  readdirSync(path) {
293
- if (this.devices.has(path)) {
294
- throw ErrnoError.With('ENOTDIR', path, 'readdirSync');
295
- }
296
284
  const entries = super.readdirSync(path);
297
285
  for (const dev of this.devices.keys()) {
298
286
  if (dirname(dev) == path) {
@@ -0,0 +1,21 @@
1
+ import type { Stats } from '../stats.js';
2
+ /**
3
+ * Whether the cache is enabled
4
+ */
5
+ export declare let isEnabled: boolean;
6
+ /**
7
+ * Sets whether the cache is enabled or not
8
+ */
9
+ export declare function setEnabled(value: boolean): void;
10
+ /**
11
+ * Gets stats from the cache, if they exist and the cache is enabled.
12
+ */
13
+ export declare function getStats(path: string): Stats | undefined;
14
+ /**
15
+ * Adds stats if the cache is enabled
16
+ */
17
+ export declare function setStats(path: string, value: Stats): void;
18
+ /**
19
+ * Clears the cache if it is enabled
20
+ */
21
+ export declare function clearStats(): void;
@@ -0,0 +1,36 @@
1
+ /* Experimental caching */
2
+ /**
3
+ * Whether the cache is enabled
4
+ */
5
+ export let isEnabled = false;
6
+ /**
7
+ * Sets whether the cache is enabled or not
8
+ */
9
+ export function setEnabled(value) {
10
+ isEnabled = value;
11
+ }
12
+ const stats = new Map();
13
+ /**
14
+ * Gets stats from the cache, if they exist and the cache is enabled.
15
+ */
16
+ export function getStats(path) {
17
+ if (!isEnabled)
18
+ return;
19
+ return stats.get(path);
20
+ }
21
+ /**
22
+ * Adds stats if the cache is enabled
23
+ */
24
+ export function setStats(path, value) {
25
+ if (!isEnabled)
26
+ return;
27
+ stats.set(path, value);
28
+ }
29
+ /**
30
+ * Clears the cache if it is enabled
31
+ */
32
+ export function clearStats() {
33
+ if (!isEnabled)
34
+ return;
35
+ stats.clear();
36
+ }
@@ -0,0 +1,10 @@
1
+ export declare const config: {
2
+ /**
3
+ * Whether to perform access checks
4
+ */
5
+ checkAccess: boolean;
6
+ /**
7
+ * Whether to sync atime updates immediately when reading from a file
8
+ */
9
+ syncOnRead: boolean;
10
+ };
@@ -0,0 +1,10 @@
1
+ export const config = {
2
+ /**
3
+ * Whether to perform access checks
4
+ */
5
+ checkAccess: true,
6
+ /**
7
+ * Whether to sync atime updates immediately when reading from a file
8
+ */
9
+ syncOnRead: true,
10
+ };
@@ -9,6 +9,7 @@ import type { FileContents } from '../filesystem.js';
9
9
  import '../polyfills.js';
10
10
  import { BigIntStats, type Stats } from '../stats.js';
11
11
  import { Dir, Dirent } from './dir.js';
12
+ import { type InternalOptions, type ReaddirOptions } from './shared.js';
12
13
  import { ReadStream, WriteStream } from './streams.js';
13
14
  export * as constants from './constants.js';
14
15
  export declare class FileHandle implements promises.FileHandle {
@@ -248,30 +249,24 @@ export declare function mkdir(path: fs.PathLike, options?: fs.Mode | (fs.MakeDir
248
249
  export declare function mkdir(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): Promise<string | undefined>;
249
250
  /**
250
251
  * Asynchronous readdir(3) - read a directory.
252
+ *
253
+ * Note: The order of entries is not guaranteed
251
254
  * @param path A path to a file. If a URL is provided, it must use the `file:` protocol.
252
255
  * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'`.
253
256
  */
254
- export declare function readdir(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & {
257
+ export declare function readdir(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & {
255
258
  withFileTypes?: false;
256
- recursive?: boolean;
257
259
  }) | BufferEncoding | null): Promise<string[]>;
258
- export declare function readdir(path: fs.PathLike, options: fs.BufferEncodingOption & {
260
+ export declare function readdir(path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & {
259
261
  withFileTypes?: false;
260
- recursive?: boolean;
261
262
  }): Promise<Buffer[]>;
262
- export declare function readdir(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & {
263
+ export declare function readdir(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & {
263
264
  withFileTypes?: false;
264
- recursive?: boolean;
265
265
  }) | BufferEncoding | null): Promise<string[] | Buffer[]>;
266
- export declare function readdir(path: fs.PathLike, options: fs.ObjectEncodingOptions & {
266
+ export declare function readdir(path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & {
267
267
  withFileTypes: true;
268
- recursive?: boolean;
269
268
  }): Promise<Dirent[]>;
270
- export declare function readdir(path: fs.PathLike, options?: {
271
- withFileTypes?: boolean;
272
- recursive?: boolean;
273
- encoding?: BufferEncoding | 'buffer' | null;
274
- } | BufferEncoding | 'buffer' | null): Promise<string[] | Dirent[] | Buffer[]>;
269
+ export declare function readdir(path: fs.PathLike, options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null): Promise<string[] | Dirent[] | Buffer[]>;
275
270
  export declare function link(targetPath: fs.PathLike, linkPath: fs.PathLike): Promise<void>;
276
271
  /**
277
272
  * `symlink`.
@@ -311,7 +306,7 @@ export declare function access(path: fs.PathLike, mode?: number): Promise<void>;
311
306
  * Asynchronous `rm`. Removes files or directories (recursively).
312
307
  * @param path The path to the file or directory to remove.
313
308
  */
314
- export declare function rm(path: fs.PathLike, options?: fs.RmOptions): Promise<void>;
309
+ export declare function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOptions): Promise<void>;
315
310
  /**
316
311
  * Asynchronous `mkdtemp`. Creates a unique temporary directory.
317
312
  * @param prefix The directory prefix.
@@ -52,10 +52,12 @@ import { flagToMode, isAppendable, isExclusive, isReadable, isTruncating, isWrit
52
52
  import '../polyfills.js';
53
53
  import { BigIntStats } from '../stats.js';
54
54
  import { decodeUTF8, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
55
+ import * as cache from './cache.js';
55
56
  import * as constants from './constants.js';
56
57
  import { Dir, Dirent } from './dir.js';
57
58
  import { dirname, join, parse } from './path.js';
58
59
  import { _statfs, fd2file, fdMap, file2fd, fixError, mounts, resolveMount } from './shared.js';
60
+ import { config } from './config.js';
59
61
  import { ReadStream, WriteStream } from './streams.js';
60
62
  import { FSWatcher, emitChange } from './watchers.js';
61
63
  export * as constants from './constants.js';
@@ -217,7 +219,7 @@ export class FileHandle {
217
219
  }
218
220
  async stat(opts) {
219
221
  const stats = await this.file.stat();
220
- if (!stats.hasAccess(constants.R_OK)) {
222
+ if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
221
223
  throw ErrnoError.With('EACCES', this.file.path, 'stat');
222
224
  }
223
225
  return opts?.bigint ? new BigIntStats(stats) : stats;
@@ -351,7 +353,7 @@ export async function rename(oldPath, newPath) {
351
353
  newPath = normalizePath(newPath);
352
354
  const src = resolveMount(oldPath);
353
355
  const dst = resolveMount(newPath);
354
- if (!(await stat(dirname(oldPath))).hasAccess(constants.W_OK)) {
356
+ if (config.checkAccess && !(await stat(dirname(oldPath))).hasAccess(constants.W_OK)) {
355
357
  throw ErrnoError.With('EACCES', oldPath, 'rename');
356
358
  }
357
359
  try {
@@ -389,7 +391,7 @@ export async function stat(path, options) {
389
391
  const { fs, path: resolved } = resolveMount(await realpath(path));
390
392
  try {
391
393
  const stats = await fs.stat(resolved);
392
- if (!stats.hasAccess(constants.R_OK)) {
394
+ if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
393
395
  throw ErrnoError.With('EACCES', resolved, 'stat');
394
396
  }
395
397
  return options?.bigint ? new BigIntStats(stats) : stats;
@@ -433,7 +435,7 @@ export async function unlink(path) {
433
435
  path = normalizePath(path);
434
436
  const { fs, path: resolved } = resolveMount(path);
435
437
  try {
436
- if (!(await fs.stat(resolved)).hasAccess(constants.W_OK)) {
438
+ if (config.checkAccess && !(cache.getStats(path) || (await fs.stat(resolved))).hasAccess(constants.W_OK)) {
437
439
  throw ErrnoError.With('EACCES', resolved, 'unlink');
438
440
  }
439
441
  await fs.unlink(resolved);
@@ -460,7 +462,7 @@ async function _open(path, _flag, _mode = 0o644, resolveSymlinks) {
460
462
  }
461
463
  // Create the file
462
464
  const parentStats = await fs.stat(dirname(resolved));
463
- if (!parentStats.hasAccess(constants.W_OK)) {
465
+ if (config.checkAccess && !parentStats.hasAccess(constants.W_OK)) {
464
466
  throw ErrnoError.With('EACCES', dirname(path), '_open');
465
467
  }
466
468
  if (!parentStats.isDirectory()) {
@@ -468,7 +470,7 @@ async function _open(path, _flag, _mode = 0o644, resolveSymlinks) {
468
470
  }
469
471
  return new FileHandle(await fs.createFile(resolved, flag, mode));
470
472
  }
471
- if (!stats.hasAccess(flagToMode(flag))) {
473
+ if (config.checkAccess && !stats.hasAccess(flagToMode(flag))) {
472
474
  throw ErrnoError.With('EACCES', path, '_open');
473
475
  }
474
476
  if (isExclusive(flag)) {
@@ -579,11 +581,14 @@ export async function appendFile(path, data, _options) {
579
581
  appendFile;
580
582
  // DIRECTORY-ONLY METHODS
581
583
  export async function rmdir(path) {
582
- path = normalizePath(path);
583
584
  path = await realpath(path);
584
585
  const { fs, path: resolved } = resolveMount(path);
585
586
  try {
586
- if (!(await fs.stat(resolved)).hasAccess(constants.W_OK)) {
587
+ const stats = cache.getStats(path) || (await fs.stat(resolved));
588
+ if (!stats.isDirectory()) {
589
+ throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
590
+ }
591
+ if (config.checkAccess && !stats.hasAccess(constants.W_OK)) {
587
592
  throw ErrnoError.With('EACCES', resolved, 'rmdir');
588
593
  }
589
594
  await fs.rmdir(resolved);
@@ -597,12 +602,12 @@ rmdir;
597
602
  export async function mkdir(path, options) {
598
603
  options = typeof options === 'object' ? options : { mode: options };
599
604
  const mode = normalizeMode(options?.mode, 0o777);
600
- path = await realpath(normalizePath(path));
605
+ path = await realpath(path);
601
606
  const { fs, path: resolved } = resolveMount(path);
602
607
  const errorPaths = { [resolved]: path };
603
608
  try {
604
609
  if (!options?.recursive) {
605
- if (!(await fs.stat(dirname(resolved))).hasAccess(constants.W_OK)) {
610
+ if (config.checkAccess && !(await fs.stat(dirname(resolved))).hasAccess(constants.W_OK)) {
606
611
  throw ErrnoError.With('EACCES', dirname(resolved), 'mkdir');
607
612
  }
608
613
  await fs.mkdir(resolved, mode);
@@ -615,7 +620,7 @@ export async function mkdir(path, options) {
615
620
  errorPaths[dir] = origDir;
616
621
  }
617
622
  for (const dir of dirs) {
618
- if (!(await fs.stat(dirname(dir))).hasAccess(constants.W_OK)) {
623
+ if (config.checkAccess && !(await fs.stat(dirname(dir))).hasAccess(constants.W_OK)) {
619
624
  throw ErrnoError.With('EACCES', dirname(dir), 'mkdir');
620
625
  }
621
626
  await fs.mkdir(dir, mode);
@@ -630,14 +635,20 @@ export async function mkdir(path, options) {
630
635
  mkdir;
631
636
  export async function readdir(path, options) {
632
637
  options = typeof options === 'object' ? options : { encoding: options };
633
- path = await realpath(normalizePath(path));
638
+ path = await realpath(path);
639
+ const handleError = (e) => {
640
+ throw fixError(e, { [resolved]: path });
641
+ };
634
642
  const { fs, path: resolved } = resolveMount(path);
635
- if (!(await fs.stat(resolved)).hasAccess(constants.R_OK)) {
643
+ const stats = cache.getStats(path) || (await fs.stat(resolved).catch(handleError));
644
+ cache.setStats(path, stats);
645
+ if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
636
646
  throw ErrnoError.With('EACCES', path, 'readdir');
637
647
  }
638
- const entries = await fs.readdir(resolved).catch((e) => {
639
- throw fixError(e, { [resolved]: path });
640
- });
648
+ if (!stats.isDirectory()) {
649
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
650
+ }
651
+ const entries = await fs.readdir(resolved).catch(handleError);
641
652
  for (const point of mounts.keys()) {
642
653
  if (point.startsWith(path)) {
643
654
  const entry = point.slice(path.length);
@@ -649,26 +660,24 @@ export async function readdir(path, options) {
649
660
  }
650
661
  }
651
662
  const values = [];
652
- for (const entry of entries) {
653
- let stats;
663
+ const addEntry = async (entry) => {
664
+ let entryStats;
654
665
  if (options?.recursive || options?.withFileTypes) {
655
- stats = await fs.stat(join(resolved, entry)).catch((error) => {
656
- throw fixError(error, { [resolved]: path });
657
- });
666
+ entryStats = cache.getStats(join(path, entry)) || (await fs.stat(join(resolved, entry)).catch(handleError));
667
+ cache.setStats(join(path, entry), entryStats);
658
668
  }
659
669
  if (options?.withFileTypes) {
660
- values.push(new Dirent(entry, stats));
670
+ values.push(new Dirent(entry, entryStats));
661
671
  }
662
- else if (options?.encoding === 'buffer') {
672
+ else if (options?.encoding == 'buffer') {
663
673
  values.push(Buffer.from(entry));
664
674
  }
665
675
  else {
666
676
  values.push(entry);
667
677
  }
668
- if (!options?.recursive || !stats?.isDirectory()) {
669
- continue;
670
- }
671
- for (const subEntry of await readdir(join(path, entry), options)) {
678
+ if (!options?.recursive || !entryStats?.isDirectory())
679
+ return;
680
+ for (const subEntry of await readdir(join(path, entry), { ...options, _isIndirect: true })) {
672
681
  if (subEntry instanceof Dirent) {
673
682
  subEntry.path = join(entry, subEntry.path);
674
683
  values.push(subEntry);
@@ -681,26 +690,30 @@ export async function readdir(path, options) {
681
690
  values.push(join(entry, subEntry));
682
691
  }
683
692
  }
693
+ };
694
+ await Promise.all(entries.map(addEntry));
695
+ if (!options?._isIndirect) {
696
+ cache.clearStats();
684
697
  }
685
698
  return values;
686
699
  }
687
700
  readdir;
688
701
  export async function link(targetPath, linkPath) {
689
702
  targetPath = normalizePath(targetPath);
690
- if (!(await stat(dirname(targetPath))).hasAccess(constants.R_OK)) {
691
- throw ErrnoError.With('EACCES', dirname(targetPath), 'link');
692
- }
693
703
  linkPath = normalizePath(linkPath);
694
- if (!(await stat(dirname(linkPath))).hasAccess(constants.W_OK)) {
695
- throw ErrnoError.With('EACCES', dirname(linkPath), 'link');
696
- }
697
704
  const { fs, path } = resolveMount(targetPath);
698
705
  const link = resolveMount(linkPath);
699
706
  if (fs != link.fs) {
700
707
  throw ErrnoError.With('EXDEV', linkPath, 'link');
701
708
  }
702
709
  try {
703
- if (!(await fs.stat(path)).hasAccess(constants.W_OK)) {
710
+ if (config.checkAccess && !(await fs.stat(dirname(targetPath))).hasAccess(constants.R_OK)) {
711
+ throw ErrnoError.With('EACCES', dirname(path), 'link');
712
+ }
713
+ if (config.checkAccess && !(await stat(dirname(linkPath))).hasAccess(constants.W_OK)) {
714
+ throw ErrnoError.With('EACCES', dirname(linkPath), 'link');
715
+ }
716
+ if (config.checkAccess && !(await fs.stat(path)).hasAccess(constants.W_OK | constants.R_OK)) {
704
717
  throw ErrnoError.With('EACCES', path, 'link');
705
718
  }
706
719
  return await fs.link(path, link.path);
@@ -907,6 +920,8 @@ export function watch(filename, options = {}) {
907
920
  }
908
921
  watch;
909
922
  export async function access(path, mode = constants.F_OK) {
923
+ if (!config.checkAccess)
924
+ return;
910
925
  const stats = await stat(path);
911
926
  if (!stats.hasAccess(mode)) {
912
927
  throw new ErrnoError(Errno.EACCES);
@@ -919,33 +934,39 @@ access;
919
934
  */
920
935
  export async function rm(path, options) {
921
936
  path = normalizePath(path);
922
- const stats = await stat(path).catch((error) => {
923
- if (error.code != 'ENOENT' || !options?.force)
924
- throw error;
925
- });
937
+ const stats = cache.getStats(path) ||
938
+ (await stat(path).catch((error) => {
939
+ if (error.code != 'ENOENT' || !options?.force)
940
+ throw error;
941
+ }));
926
942
  if (!stats) {
927
943
  return;
928
944
  }
945
+ cache.setStats(path, stats);
929
946
  switch (stats.mode & constants.S_IFMT) {
930
947
  case constants.S_IFDIR:
931
948
  if (options?.recursive) {
932
- for (const entry of await readdir(path)) {
933
- await rm(join(path, entry), options);
949
+ for (const entry of await readdir(path, { _isIndirect: true })) {
950
+ await rm(join(path, entry), { ...options, _isIndirect: true });
934
951
  }
935
952
  }
936
953
  await rmdir(path);
937
- return;
954
+ break;
938
955
  case constants.S_IFREG:
939
956
  case constants.S_IFLNK:
940
957
  await unlink(path);
941
- return;
958
+ break;
942
959
  case constants.S_IFBLK:
943
960
  case constants.S_IFCHR:
944
961
  case constants.S_IFIFO:
945
962
  case constants.S_IFSOCK:
946
963
  default:
964
+ cache.clearStats();
947
965
  throw new ErrnoError(Errno.EPERM, 'File type not supported', path, 'rm');
948
966
  }
967
+ if (!options?._isIndirect) {
968
+ cache.clearStats();
969
+ }
949
970
  }
950
971
  rm;
951
972
  export async function mkdtemp(prefix, options) {
@@ -1005,18 +1026,21 @@ export async function cp(source, destination, opts) {
1005
1026
  throw new ErrnoError(Errno.EEXIST, 'Destination file or directory already exists.', destination, 'cp');
1006
1027
  }
1007
1028
  switch (srcStats.mode & constants.S_IFMT) {
1008
- case constants.S_IFDIR:
1029
+ case constants.S_IFDIR: {
1009
1030
  if (!opts?.recursive) {
1010
1031
  throw new ErrnoError(Errno.EISDIR, source + ' is a directory (not copied)', source, 'cp');
1011
1032
  }
1012
- await mkdir(destination, { recursive: true }); // Ensure the destination directory exists
1013
- for (const dirent of await readdir(source, { withFileTypes: true })) {
1033
+ const [entries] = await Promise.all([readdir(source, { withFileTypes: true }), mkdir(destination, { recursive: true })] // Ensure the destination directory exists
1034
+ );
1035
+ const _cp = async (dirent) => {
1014
1036
  if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) {
1015
- continue; // Skip if the filter returns false
1037
+ return; // Skip if the filter returns false
1016
1038
  }
1017
1039
  await cp(join(source, dirent.name), join(destination, dirent.name), opts);
1018
- }
1040
+ };
1041
+ await Promise.all(entries.map(_cp));
1019
1042
  break;
1043
+ }
1020
1044
  case constants.S_IFREG:
1021
1045
  case constants.S_IFLNK:
1022
1046
  await copyFile(source, destination);
@@ -43,3 +43,19 @@ export declare function mountObject(mounts: MountObject): void;
43
43
  * @hidden
44
44
  */
45
45
  export declare function _statfs<const T extends boolean>(fs: FileSystem, bigint?: T): T extends true ? BigIntStatsFs : StatsFs;
46
+ /**
47
+ * Options used for caching, among other things.
48
+ * @internal *UNSTABLE*
49
+ */
50
+ export interface InternalOptions {
51
+ /**
52
+ * If true, then this readdir was called from another function.
53
+ * In this case, don't clear the cache when done.
54
+ * @internal *UNSTABLE*
55
+ */
56
+ _isIndirect?: boolean;
57
+ }
58
+ export interface ReaddirOptions extends InternalOptions {
59
+ withFileTypes?: boolean;
60
+ recursive?: boolean;
61
+ }