@zenfs/core 1.3.3 → 1.3.5

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.
@@ -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
  }
@@ -550,7 +551,7 @@ export function symlinkSync(target: fs.PathLike, path: fs.PathLike, type: fs.sym
550
551
 
551
552
  writeFileSync(path, target.toString());
552
553
  const file = _openSync(path, 'r+', 0o644, false);
553
- file._setTypeSync(constants.S_IFLNK);
554
+ file.chmodSync(constants.S_IFLNK);
554
555
  }
555
556
  symlinkSync satisfies typeof fs.symlinkSync;
556
557
 
@@ -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;
@@ -4,7 +4,7 @@ import type * as fs from 'node:fs';
4
4
  import { ErrnoError } from '../error.js';
5
5
  import { isStatsEqual, type Stats } from '../stats.js';
6
6
  import { normalizePath } from '../utils.js';
7
- import { dirname, basename } from './path.js';
7
+ import { basename, dirname } from './path.js';
8
8
  import { statSync } from './sync.js';
9
9
 
10
10
  /**
@@ -171,23 +171,24 @@ export function removeWatcher(path: string, watcher: FSWatcher) {
171
171
  }
172
172
 
173
173
  export function emitChange(eventType: fs.WatchEventType, filename: string) {
174
- let normalizedFilename: string = normalizePath(filename);
174
+ filename = normalizePath(filename);
175
175
  // Notify watchers on the specific file
176
- if (watchers.has(normalizedFilename)) {
177
- for (const watcher of watchers.get(normalizedFilename)!) {
176
+ if (watchers.has(filename)) {
177
+ for (const watcher of watchers.get(filename)!) {
178
178
  watcher.emit('change', eventType, basename(filename));
179
179
  }
180
180
  }
181
181
 
182
182
  // Notify watchers on parent directories if they are watching recursively
183
- let parent = dirname(normalizedFilename);
184
- while (parent !== normalizedFilename && parent !== '/') {
183
+ let parent = filename,
184
+ normalizedFilename;
185
+ while (parent !== normalizedFilename) {
186
+ normalizedFilename = parent;
187
+ parent = dirname(parent);
185
188
  if (watchers.has(parent)) {
186
189
  for (const watcher of watchers.get(parent)!) {
187
- watcher.emit('change', eventType, basename(filename));
190
+ watcher.emit('change', eventType, filename.slice(parent.length + (parent == '/' ? 0 : 1)));
188
191
  }
189
192
  }
190
- normalizedFilename = parent;
191
- parent = dirname(parent);
192
193
  }
193
194
  }
package/src/file.ts CHANGED
@@ -4,7 +4,7 @@ import { O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_SYNC, O_TRUNC, O_WRONLY,
4
4
  import { Errno, ErrnoError } from './error.js';
5
5
  import type { FileSystem } from './filesystem.js';
6
6
  import './polyfills.js';
7
- import { Stats, type FileType } from './stats.js';
7
+ import { _chown, Stats } from './stats.js';
8
8
 
9
9
  /**
10
10
  Typescript does not include a type declaration for resizable array buffers.
@@ -251,18 +251,6 @@ export abstract class File<FS extends FileSystem = FileSystem> {
251
251
  * Change the file timestamps of the file.
252
252
  */
253
253
  public abstract utimesSync(atime: Date, mtime: Date): void;
254
-
255
- /**
256
- * Set the file type
257
- * @internal
258
- */
259
- public abstract _setType(type: FileType): Promise<void>;
260
-
261
- /**
262
- * Set the file type
263
- * @internal
264
- */
265
- public abstract _setTypeSync(type: FileType): void;
266
254
  }
267
255
 
268
256
  /**
@@ -573,8 +561,8 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
573
561
  throw ErrnoError.With('EBADF', this.path, 'File.chmod');
574
562
  }
575
563
  this.dirty = true;
576
- this.stats.chmod(mode);
577
- if (config.syncImmediately) await this.sync();
564
+ this.stats.mode = (this.stats.mode & (mode > S_IFMT ? ~S_IFMT : S_IFMT)) | mode;
565
+ if (config.syncImmediately || mode > S_IFMT) await this.sync();
578
566
  }
579
567
 
580
568
  public chmodSync(mode: number): void {
@@ -582,8 +570,8 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
582
570
  throw ErrnoError.With('EBADF', this.path, 'File.chmod');
583
571
  }
584
572
  this.dirty = true;
585
- this.stats.chmod(mode);
586
- if (config.syncImmediately) this.syncSync();
573
+ this.stats.mode = (this.stats.mode & (mode > S_IFMT ? ~S_IFMT : S_IFMT)) | mode;
574
+ if (config.syncImmediately || mode > S_IFMT) this.syncSync();
587
575
  }
588
576
 
589
577
  public async chown(uid: number, gid: number): Promise<void> {
@@ -591,7 +579,7 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
591
579
  throw ErrnoError.With('EBADF', this.path, 'File.chown');
592
580
  }
593
581
  this.dirty = true;
594
- this.stats.chown(uid, gid);
582
+ _chown(this.stats, uid, gid);
595
583
  if (config.syncImmediately) await this.sync();
596
584
  }
597
585
 
@@ -600,7 +588,7 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
600
588
  throw ErrnoError.With('EBADF', this.path, 'File.chown');
601
589
  }
602
590
  this.dirty = true;
603
- this.stats.chown(uid, gid);
591
+ _chown(this.stats, uid, gid);
604
592
  if (config.syncImmediately) this.syncSync();
605
593
  }
606
594
 
@@ -623,24 +611,6 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
623
611
  this.stats.mtime = mtime;
624
612
  if (config.syncImmediately) this.syncSync();
625
613
  }
626
-
627
- public async _setType(type: FileType): Promise<void> {
628
- if (this.closed) {
629
- throw ErrnoError.With('EBADF', this.path, 'File._setType');
630
- }
631
- this.dirty = true;
632
- this.stats.mode = (this.stats.mode & ~S_IFMT) | type;
633
- await this.sync();
634
- }
635
-
636
- public _setTypeSync(type: FileType): void {
637
- if (this.closed) {
638
- throw ErrnoError.With('EBADF', this.path, 'File._setType');
639
- }
640
- this.dirty = true;
641
- this.stats.mode = (this.stats.mode & ~S_IFMT) | type;
642
- this.syncSync();
643
- }
644
614
  }
645
615
 
646
616
  /**
package/src/stats.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type * as Node from 'node:fs';
2
- import { credentials, type Credentials } from './credentials.js';
2
+ import { credentials } from './credentials.js';
3
3
  import {
4
4
  R_OK,
5
5
  S_IFBLK,
@@ -67,6 +67,10 @@ export interface StatsLike<T extends number | bigint = number | bigint> {
67
67
  * Inode number
68
68
  */
69
69
  ino: T;
70
+ /**
71
+ * Number of hard links
72
+ */
73
+ nlink: T;
70
74
  }
71
75
 
72
76
  /**
@@ -258,7 +262,7 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
258
262
  }
259
263
 
260
264
  // Group permissions
261
- if (credentials.gid === this.gid) {
265
+ if (credentials.gid === this.gid || credentials.groups.includes(Number(this.gid))) {
262
266
  if (this.mode & S_IRGRP) perm |= R_OK;
263
267
  if (this.mode & S_IWGRP) perm |= W_OK;
264
268
  if (this.mode & S_IXGRP) perm |= X_OK;
@@ -273,25 +277,11 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
273
277
  return (perm & mode) === mode;
274
278
  }
275
279
 
276
- /**
277
- * Convert the current stats object into a credentials object
278
- * @internal
279
- */
280
- public cred(uid: number = Number(this.uid), gid: number = Number(this.gid)): Credentials {
281
- return {
282
- uid,
283
- gid,
284
- suid: Number(this.uid),
285
- sgid: Number(this.gid),
286
- euid: uid,
287
- egid: gid,
288
- };
289
- }
290
-
291
280
  /**
292
281
  * Change the mode of the file.
293
282
  * We use this helper function to prevent messing up the type of the file.
294
283
  * @internal
284
+ * @deprecated This will be removed in the next minor release since it is internal
295
285
  */
296
286
  public chmod(mode: number): void {
297
287
  this.mode = this._convert((this.mode & S_IFMT) | mode);
@@ -301,8 +291,9 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
301
291
  * Change the owner user/group of the file.
302
292
  * This function makes sure it is a valid UID/GID (that is, a 32 unsigned int)
303
293
  * @internal
294
+ * @deprecated This will be removed in the next minor release since it is internal
304
295
  */
305
- public chown(uid: number | bigint, gid: number | bigint): void {
296
+ public chown(uid: number, gid: number): void {
306
297
  uid = Number(uid);
307
298
  gid = Number(gid);
308
299
  if (!isNaN(uid) && 0 <= uid && uid < 2 ** 32) {
@@ -330,6 +321,18 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
330
321
  }
331
322
  }
332
323
 
324
+ /**
325
+ * @hidden @internal
326
+ */
327
+ export function _chown(stats: Partial<StatsLike<number>>, uid: number, gid: number) {
328
+ if (!isNaN(uid) && 0 <= uid && uid < 2 ** 32) {
329
+ stats.uid = uid;
330
+ }
331
+ if (!isNaN(gid) && 0 <= gid && gid < 2 ** 32) {
332
+ stats.gid = gid;
333
+ }
334
+ }
335
+
333
336
  /**
334
337
  * Implementation of Node's `Stats`.
335
338
  *
@@ -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 () => {
@@ -115,6 +126,27 @@ suite('Watch Features', () => {
115
126
  resolve();
116
127
  })();
117
128
 
129
+ await fs.promises.unlink(tempFile);
130
+ await promise;
131
+ });
132
+ test('fs.promises.watch should detect file creations recursively', async () => {
133
+ const rootDir = '/';
134
+ const subDir = `${testDir}sub-dir`;
135
+ const tempFile = `${subDir}/tempFile.txt`;
136
+ await fs.promises.mkdir(subDir);
137
+ const watcher = fs.promises.watch(rootDir);
138
+
139
+ await fs.promises.writeFile(tempFile, 'Temporary content');
140
+ const { promise, resolve } = Promise.withResolvers<void>();
141
+ (async () => {
142
+ for await (const event of watcher) {
143
+ assert.equal(event.eventType, 'rename');
144
+ assert.equal(event.filename, tempFile.substring(rootDir.length));
145
+ break;
146
+ }
147
+ resolve();
148
+ })();
149
+
118
150
  await fs.promises.unlink(tempFile);
119
151
  await promise;
120
152
  });