@zenfs/core 2.5.7 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/backends/cow.d.ts +13 -0
  2. package/dist/backends/cow.js +48 -11
  3. package/dist/backends/single_buffer.d.ts +2 -2
  4. package/dist/backends/store/fs.d.ts +6 -6
  5. package/dist/backends/store/fs.js +14 -10
  6. package/dist/internal/devices.d.ts +6 -6
  7. package/dist/internal/filesystem.d.ts +4 -4
  8. package/dist/internal/index_fs.d.ts +4 -4
  9. package/dist/internal/index_fs.js +6 -8
  10. package/dist/internal/inode.js +3 -0
  11. package/dist/node/async.js +1 -1
  12. package/dist/node/dir.d.ts +3 -1
  13. package/dist/node/dir.js +23 -5
  14. package/dist/node/promises.d.ts +4 -2
  15. package/dist/node/promises.js +130 -89
  16. package/dist/node/streams.d.ts +4 -0
  17. package/dist/node/streams.js +9 -10
  18. package/dist/node/sync.js +71 -40
  19. package/dist/utils.d.ts +8 -1
  20. package/dist/utils.js +26 -7
  21. package/dist/vfs/async.js +224 -85
  22. package/dist/vfs/file.d.ts +13 -7
  23. package/dist/vfs/file.js +661 -357
  24. package/dist/vfs/shared.d.ts +1 -0
  25. package/dist/vfs/shared.js +19 -3
  26. package/dist/vfs/sync.js +214 -85
  27. package/dist/vfs/vcache.d.ts +66 -0
  28. package/dist/vfs/vcache.js +249 -0
  29. package/dist/vfs/vnode.d.ts +66 -0
  30. package/dist/vfs/vnode.js +200 -0
  31. package/dist/vfs/watchers.js +4 -2
  32. package/dist/vfs/xattr.js +2 -2
  33. package/eslint.shared.js +1 -1
  34. package/package.json +6 -3
  35. package/scripts/make-index.js +7 -1
  36. package/scripts/test.js +28 -21
  37. package/tests/backend/port.test.ts +3 -3
  38. package/tests/backend/store.test.ts +49 -0
  39. package/tests/common/inode.test.ts +8 -0
  40. package/tests/common.ts +62 -0
  41. package/tests/fetch/fetch.ts +6 -0
  42. package/tests/fetch/run.sh +2 -2
  43. package/tests/fetch/server.js +3 -3
  44. package/tests/fs/append.test.ts +5 -5
  45. package/tests/fs/concurrency.test.ts +52 -0
  46. package/tests/fs/dir.test.ts +13 -15
  47. package/tests/fs/directory.test.ts +32 -31
  48. package/tests/fs/errors.test.ts +151 -31
  49. package/tests/fs/exists.test.ts +4 -4
  50. package/tests/fs/links.test.ts +4 -11
  51. package/tests/fs/open.test.ts +4 -4
  52. package/tests/fs/permissions.test.ts +15 -15
  53. package/tests/fs/read.test.ts +9 -9
  54. package/tests/fs/readFile.test.ts +9 -9
  55. package/tests/fs/rename.test.ts +8 -8
  56. package/tests/fs/scaling.test.ts +4 -4
  57. package/tests/fs/stat.test.ts +20 -20
  58. package/tests/fs/streams.test.ts +9 -6
  59. package/tests/fs/tempdir.test.ts +33 -10
  60. package/tests/fs/times.test.ts +11 -11
  61. package/tests/fs/truncate.test.ts +6 -6
  62. package/tests/fs/watch.test.ts +107 -53
  63. package/tests/fs/write.test.ts +10 -10
  64. package/tests/fs/xattr.test.ts +3 -3
  65. package/tests/native/run.sh +3 -0
  66. package/tests/native/setup.ts +230 -0
  67. package/types/readable-stream.d.ts +2 -2
@@ -70,6 +70,19 @@ export declare class CopyOnWriteFS extends FileSystem {
70
70
  /** The journal to use for persisting deletions */
71
71
  journal?: Journal);
72
72
  isDeleted(path: string): boolean;
73
+ /**
74
+ * Inode numbers presented to the VFS for each path.
75
+ *
76
+ * The readable and writable layers allocate inode numbers independently, so they can collide.
77
+ * They can not be used as-is, since the VFS uses `ino` to tell files apart.
78
+ * This also keeps a file's inode number stable when it is copied up to the writable layer.
79
+ */
80
+ protected inos: Map<string, number>;
81
+ private _nextIno;
82
+ /** Moves the public inode numbers for a rename, including a directory's descendants */
83
+ protected renameInos(oldPath: string, newPath: string): void;
84
+ /** Replaces the layer's inode number with the public one for `path` */
85
+ protected remapIno(path: string, inode: InodeLike): InodeLike;
73
86
  /**
74
87
  * @todo Consider trying to track information on the writable as well
75
88
  */
@@ -1,17 +1,17 @@
1
1
  import { EventEmitter } from 'eventemitter3';
2
2
  import { withErrno } from 'kerium';
3
3
  import { debug, err, warn } from 'kerium/log';
4
- import { canary } from 'utilium';
4
+ import { canary, pick } from 'utilium';
5
5
  import { resolveMountConfig } from '../config.js';
6
6
  import { FileSystem } from '../internal/filesystem.js';
7
- import { isDirectory } from '../internal/inode.js';
7
+ import { _inode_fields, isDirectory } from '../internal/inode.js';
8
8
  import { dirname, join } from '../path.js';
9
9
  const journalOperations = new Set(['delete']);
10
10
  /** Because TS doesn't work right w/o it */
11
11
  function isJournalOp(op) {
12
12
  return journalOperations.has(op);
13
13
  }
14
- const maxOpLength = Math.max(...journalOperations.values().map(op => op.length));
14
+ const maxOpLength = Math.max(...[...journalOperations].map(op => op.length));
15
15
  const journalMagicString = '#journal@v0\n';
16
16
  /**
17
17
  * Tracks various operations for the CoW backend
@@ -102,6 +102,35 @@ export class CopyOnWriteFS extends FileSystem {
102
102
  isDeleted(path) {
103
103
  return this.journal.isDeleted(path);
104
104
  }
105
+ /**
106
+ * Inode numbers presented to the VFS for each path.
107
+ *
108
+ * The readable and writable layers allocate inode numbers independently, so they can collide.
109
+ * They can not be used as-is, since the VFS uses `ino` to tell files apart.
110
+ * This also keeps a file's inode number stable when it is copied up to the writable layer.
111
+ */
112
+ inos = new Map();
113
+ _nextIno = 1;
114
+ /** Moves the public inode numbers for a rename, including a directory's descendants */
115
+ renameInos(oldPath, newPath) {
116
+ for (const [path, ino] of [...this.inos]) {
117
+ if (path != oldPath && !path.startsWith(oldPath + '/'))
118
+ continue;
119
+ this.inos.delete(path);
120
+ this.inos.set(newPath + path.slice(oldPath.length), ino);
121
+ }
122
+ }
123
+ /** Replaces the layer's inode number with the public one for `path` */
124
+ remapIno(path, inode) {
125
+ let ino = this.inos.get(path);
126
+ if (ino === undefined) {
127
+ ino = this._nextIno++;
128
+ this.inos.set(path, ino);
129
+ }
130
+ /* Note the layer's inode is not mutated, since it may be a persistent object.
131
+ `Inode` fields are accessors, so a copy must be explicit rather than a spread. */
132
+ return { ...pick(inode, _inode_fields), attributes: inode.attributes, ino };
133
+ }
105
134
  /**
106
135
  * @todo Consider trying to track information on the writable as well
107
136
  */
@@ -139,6 +168,7 @@ export class CopyOnWriteFS extends FileSystem {
139
168
  if (this.isDeleted(oldPath))
140
169
  throw withErrno('ENOENT');
141
170
  }
171
+ this.renameInos(oldPath, newPath);
142
172
  }
143
173
  renameSync(oldPath, newPath) {
144
174
  this.copyForWriteSync(oldPath);
@@ -149,25 +179,26 @@ export class CopyOnWriteFS extends FileSystem {
149
179
  if (this.isDeleted(oldPath))
150
180
  throw withErrno('ENOENT');
151
181
  }
182
+ this.renameInos(oldPath, newPath);
152
183
  }
153
184
  async stat(path) {
154
185
  try {
155
- return await this.writable.stat(path);
186
+ return this.remapIno(path, await this.writable.stat(path));
156
187
  }
157
188
  catch {
158
189
  if (this.isDeleted(path))
159
190
  throw withErrno('ENOENT');
160
- return await this.readable.stat(path);
191
+ return this.remapIno(path, await this.readable.stat(path));
161
192
  }
162
193
  }
163
194
  statSync(path) {
164
195
  try {
165
- return this.writable.statSync(path);
196
+ return this.remapIno(path, this.writable.statSync(path));
166
197
  }
167
198
  catch {
168
199
  if (this.isDeleted(path))
169
200
  throw withErrno('ENOENT');
170
- return this.readable.statSync(path);
201
+ return this.remapIno(path, this.readable.statSync(path));
171
202
  }
172
203
  }
173
204
  async touch(path, metadata) {
@@ -180,11 +211,11 @@ export class CopyOnWriteFS extends FileSystem {
180
211
  }
181
212
  async createFile(path, options) {
182
213
  await this.createParentDirectories(path);
183
- return await this.writable.createFile(path, options);
214
+ return this.remapIno(path, await this.writable.createFile(path, options));
184
215
  }
185
216
  createFileSync(path, options) {
186
217
  this.createParentDirectoriesSync(path);
187
- return this.writable.createFileSync(path, options);
218
+ return this.remapIno(path, this.writable.createFileSync(path, options));
188
219
  }
189
220
  async link(srcpath, dstpath) {
190
221
  await this.copyForWrite(srcpath);
@@ -204,6 +235,7 @@ export class CopyOnWriteFS extends FileSystem {
204
235
  if (await this.exists(path)) {
205
236
  this.journal.add('delete', path);
206
237
  }
238
+ this.inos.delete(path);
207
239
  }
208
240
  unlinkSync(path) {
209
241
  if (!this.existsSync(path))
@@ -215,6 +247,7 @@ export class CopyOnWriteFS extends FileSystem {
215
247
  if (this.existsSync(path)) {
216
248
  this.journal.add('delete', path);
217
249
  }
250
+ this.inos.delete(path);
218
251
  }
219
252
  async rmdir(path) {
220
253
  if (!(await this.exists(path)))
@@ -223,12 +256,14 @@ export class CopyOnWriteFS extends FileSystem {
223
256
  await this.writable.rmdir(path);
224
257
  }
225
258
  if (!(await this.exists(path))) {
259
+ this.inos.delete(path);
226
260
  return;
227
261
  }
228
262
  // Check if directory is empty.
229
263
  if ((await this.readdir(path)).length)
230
264
  throw withErrno('ENOTEMPTY');
231
265
  this.journal.add('delete', path);
266
+ this.inos.delete(path);
232
267
  }
233
268
  rmdirSync(path) {
234
269
  if (!this.existsSync(path))
@@ -237,24 +272,26 @@ export class CopyOnWriteFS extends FileSystem {
237
272
  this.writable.rmdirSync(path);
238
273
  }
239
274
  if (!this.existsSync(path)) {
275
+ this.inos.delete(path);
240
276
  return;
241
277
  }
242
278
  // Check if directory is empty.
243
279
  if (this.readdirSync(path).length)
244
280
  throw withErrno('ENOTEMPTY');
245
281
  this.journal.add('delete', path);
282
+ this.inos.delete(path);
246
283
  }
247
284
  async mkdir(path, options) {
248
285
  if (await this.exists(path))
249
286
  throw withErrno('EEXIST');
250
287
  await this.createParentDirectories(path);
251
- return await this.writable.mkdir(path, options);
288
+ return this.remapIno(path, await this.writable.mkdir(path, options));
252
289
  }
253
290
  mkdirSync(path, options) {
254
291
  if (this.existsSync(path))
255
292
  throw withErrno('EEXIST');
256
293
  this.createParentDirectoriesSync(path);
257
- return this.writable.mkdirSync(path, options);
294
+ return this.remapIno(path, this.writable.mkdirSync(path, options));
258
295
  }
259
296
  async readdir(path) {
260
297
  if (this.isDeleted(path) || !(await this.exists(path)))
@@ -5,7 +5,7 @@ import type { UsageInfo } from '../internal/filesystem.js';
5
5
  import { StoreFS } from './store/fs.js';
6
6
  import { SyncMapTransaction, type SyncMapStore } from './store/map.js';
7
7
  import type { Store } from './store/store.js';
8
- type Lock = Disposable & (() => void);
8
+ type SBLock = Disposable & (() => void);
9
9
  declare const MetadataEntry_base: import("memium").StructConstructor<BufferView<any>>;
10
10
  declare class MetadataEntry extends MetadataEntry_base {
11
11
  static name: string;
@@ -52,7 +52,7 @@ export declare class MetadataBlock extends MetadataBlock_base<ArrayBufferLike> {
52
52
  * Wait for the block to be unlocked.
53
53
  */
54
54
  waitUnlocked(depth?: number): void;
55
- lock(): Lock;
55
+ lock(): SBLock;
56
56
  }
57
57
  declare const SuperBlock_base: import("memium/decorators").StructFromTypedArray<BigUint64Array<ArrayBuffer>>;
58
58
  /**
@@ -63,18 +63,18 @@ export declare class StoreFS<T extends Store = Store> extends FileSystem {
63
63
  */
64
64
  rename(oldPath: string, newPath: string): Promise<void>;
65
65
  renameSync(oldPath: string, newPath: string): void;
66
- stat(path: string): Promise<InodeLike>;
67
- statSync(path: string): InodeLike;
66
+ stat(path: string): Promise<Inode>;
67
+ statSync(path: string): Inode;
68
68
  touch(path: string, metadata: Partial<InodeLike>): Promise<void>;
69
69
  touchSync(path: string, metadata: Partial<InodeLike>): void;
70
- createFile(path: string, options: CreationOptions): Promise<InodeLike>;
71
- createFileSync(path: string, options: CreationOptions): InodeLike;
70
+ createFile(path: string, options: CreationOptions): Promise<Inode>;
71
+ createFileSync(path: string, options: CreationOptions): Inode;
72
72
  unlink(path: string): Promise<void>;
73
73
  unlinkSync(path: string): void;
74
74
  rmdir(path: string): Promise<void>;
75
75
  rmdirSync(path: string): void;
76
- mkdir(path: string, options: CreationOptions): Promise<InodeLike>;
77
- mkdirSync(path: string, options: CreationOptions): InodeLike;
76
+ mkdir(path: string, options: CreationOptions): Promise<Inode>;
77
+ mkdirSync(path: string, options: CreationOptions): Inode;
78
78
  readdir(path: string): Promise<string[]>;
79
79
  readdirSync(path: string): string[];
80
80
  /**
@@ -454,12 +454,10 @@ export class StoreFS extends FileSystem {
454
454
  }
455
455
  }
456
456
  async createFile(path, options) {
457
- options.mode |= S_IFREG;
458
- return await this.commitNew(path, options, new Uint8Array());
457
+ return await this.commitNew(path, { ...options, mode: options.mode | S_IFREG }, new Uint8Array());
459
458
  }
460
459
  createFileSync(path, options) {
461
- options.mode |= S_IFREG;
462
- return this.commitNewSync(path, options, new Uint8Array());
460
+ return this.commitNewSync(path, { ...options, mode: options.mode | S_IFREG }, new Uint8Array());
463
461
  }
464
462
  async unlink(path) {
465
463
  return this.remove(path, false);
@@ -478,12 +476,10 @@ export class StoreFS extends FileSystem {
478
476
  this.removeSync(path, true);
479
477
  }
480
478
  async mkdir(path, options) {
481
- options.mode |= S_IFDIR;
482
- return await this.commitNew(path, options, encodeUTF8('{}'));
479
+ return await this.commitNew(path, { ...options, mode: options.mode | S_IFDIR }, encodeUTF8('{}'));
483
480
  }
484
481
  mkdirSync(path, options) {
485
- options.mode |= S_IFDIR;
486
- return this.commitNewSync(path, options, encodeUTF8('{}'));
482
+ return this.commitNewSync(path, { ...options, mode: options.mode | S_IFDIR }, encodeUTF8('{}'));
487
483
  }
488
484
  async readdir(path) {
489
485
  const env_11 = { stack: [], error: void 0, hasError: false };
@@ -618,13 +614,17 @@ export class StoreFS extends FileSystem {
618
614
  try {
619
615
  const tx = __addDisposableResource(env_17, this.transaction(), true);
620
616
  const inode = await this.findInode(tx, path);
617
+ const end = offset + data.byteLength;
621
618
  let buffer = data;
622
619
  if (!tx.flag('partial')) {
623
- buffer = extendBuffer((await tx.get(inode.data)) ?? new Uint8Array(), offset + data.byteLength);
620
+ buffer = extendBuffer((await tx.get(inode.data)) ?? new Uint8Array(), end);
624
621
  buffer.set(data, offset);
625
622
  offset = 0;
626
623
  }
627
624
  await tx.set(inode.data, buffer, offset);
625
+ // Update the inode in the same transaction, so data and metadata can not get out of sync (#286)
626
+ inode.update({ size: Math.max(inode.size, end), mtimeMs: Date.now() });
627
+ await tx.set(inode.ino, inode);
628
628
  this._add(inode.ino, path);
629
629
  await tx.commit();
630
630
  }
@@ -643,13 +643,17 @@ export class StoreFS extends FileSystem {
643
643
  try {
644
644
  const tx = __addDisposableResource(env_18, this.transaction(), false);
645
645
  const inode = this.findInodeSync(tx, path);
646
+ const end = offset + data.byteLength;
646
647
  let buffer = data;
647
648
  if (!tx.flag('partial')) {
648
- buffer = extendBuffer(tx.getSync(inode.data) ?? new Uint8Array(), offset + data.byteLength);
649
+ buffer = extendBuffer(tx.getSync(inode.data) ?? new Uint8Array(), end);
649
650
  buffer.set(data, offset);
650
651
  offset = 0;
651
652
  }
652
653
  tx.setSync(inode.data, buffer, offset);
654
+ // Update the inode in the same transaction, so data and metadata can not get out of sync (#286)
655
+ inode.update({ size: Math.max(inode.size, end), mtimeMs: Date.now() });
656
+ tx.setSync(inode.ino, inode);
653
657
  this._add(inode.ino, path);
654
658
  tx.commitSync();
655
659
  }
@@ -103,18 +103,18 @@ export declare class DeviceFS extends StoreFS<InMemoryStore> {
103
103
  constructor();
104
104
  rename(oldPath: string, newPath: string): Promise<void>;
105
105
  renameSync(oldPath: string, newPath: string): void;
106
- stat(path: string): Promise<InodeLike>;
107
- statSync(path: string): InodeLike;
106
+ stat(path: string): Promise<Inode>;
107
+ statSync(path: string): Inode;
108
108
  touch(path: string, metadata: InodeLike): Promise<void>;
109
109
  touchSync(path: string, metadata: InodeLike): void;
110
- createFile(path: string, options: CreationOptions): Promise<InodeLike>;
111
- createFileSync(path: string, options: CreationOptions): InodeLike;
110
+ createFile(path: string, options: CreationOptions): Promise<Inode>;
111
+ createFileSync(path: string, options: CreationOptions): Inode;
112
112
  unlink(path: string): Promise<void>;
113
113
  unlinkSync(path: string): void;
114
114
  rmdir(path: string): Promise<void>;
115
115
  rmdirSync(path: string): void;
116
- mkdir(path: string, options: CreationOptions): Promise<InodeLike>;
117
- mkdirSync(path: string, options: CreationOptions): InodeLike;
116
+ mkdir(path: string, options: CreationOptions): Promise<Inode>;
117
+ mkdirSync(path: string, options: CreationOptions): Inode;
118
118
  readdir(path: string): Promise<string[]>;
119
119
  readdirSync(path: string): string[];
120
120
  link(target: string, link: string): Promise<void>;
@@ -91,21 +91,21 @@ export interface FileSystemAttributes {
91
91
  * @category Internals
92
92
  * @internal
93
93
  */
94
- export interface CreationOptions extends Partial<InodeLike> {
94
+ export interface CreationOptions extends Readonly<Partial<InodeLike>> {
95
95
  /**
96
96
  * The uid to create the file.
97
97
  * This is ignored if the FS supports setuid and the setuid bit is set
98
98
  */
99
- uid: number;
99
+ readonly uid: number;
100
100
  /**
101
101
  * The gid to create the file.
102
102
  * This is ignored if the FS supports setgid and the setgid bit is set
103
103
  */
104
- gid: number;
104
+ readonly gid: number;
105
105
  /**
106
106
  * The mode to create the file with.
107
107
  */
108
- mode: number;
108
+ readonly mode: number;
109
109
  }
110
110
  /**
111
111
  * Options for streaming operations
@@ -28,12 +28,12 @@ export declare abstract class IndexFS extends FileSystem {
28
28
  rmdir(path: string): Promise<void>;
29
29
  rmdirSync(path: string): void;
30
30
  protected create(path: string, options: CreationOptions): Inode;
31
- createFile(path: string, options: CreationOptions): Promise<InodeLike>;
32
- createFileSync(path: string, options: CreationOptions): InodeLike;
31
+ createFile(path: string, options: CreationOptions): Promise<Inode>;
32
+ createFileSync(path: string, options: CreationOptions): Inode;
33
33
  protected _mkdir?(path: string, options: CreationOptions): Promise<void>;
34
34
  protected _mkdirSync?(path: string, options: CreationOptions): void;
35
- mkdir(path: string, options: CreationOptions): Promise<InodeLike>;
36
- mkdirSync(path: string, options: CreationOptions): InodeLike;
35
+ mkdir(path: string, options: CreationOptions): Promise<Inode>;
36
+ mkdirSync(path: string, options: CreationOptions): Inode;
37
37
  link(target: string, link: string): Promise<void>;
38
38
  linkSync(target: string, link: string): void;
39
39
  readdir(path: string): Promise<string[]>;
@@ -94,13 +94,13 @@ export class IndexFS extends FileSystem {
94
94
  const inode = this.index.get(path);
95
95
  if (!inode)
96
96
  throw withErrno('ENOENT');
97
- return inode;
97
+ return new Inode(inode);
98
98
  }
99
99
  statSync(path) {
100
100
  const inode = this.index.get(path);
101
101
  if (!inode)
102
102
  throw withErrno('ENOENT');
103
- return inode;
103
+ return new Inode(inode);
104
104
  }
105
105
  async touch(path, metadata) {
106
106
  const inode = this.index.get(path) ?? _throw(withErrno('ENOENT'));
@@ -165,21 +165,19 @@ export class IndexFS extends FileSystem {
165
165
  return inode;
166
166
  }
167
167
  async createFile(path, options) {
168
- options.mode |= S_IFREG;
169
- return this.create(path, options);
168
+ return this.create(path, { ...options, mode: options.mode | S_IFREG });
170
169
  }
171
170
  createFileSync(path, options) {
172
- options.mode |= S_IFREG;
173
- return this.create(path, options);
171
+ return this.create(path, { ...options, mode: options.mode | S_IFREG });
174
172
  }
175
173
  async mkdir(path, options) {
176
- options.mode |= S_IFDIR;
174
+ options = { ...options, mode: options.mode | S_IFDIR };
177
175
  const inode = this.create(path, options);
178
176
  await this._mkdir?.(path, options);
179
177
  return inode;
180
178
  }
181
179
  mkdirSync(path, options) {
182
- options.mode |= S_IFDIR;
180
+ options = { ...options, mode: options.mode | S_IFDIR };
183
181
  const inode = this.create(path, options);
184
182
  this._mkdirSync?.(path, options);
185
183
  return inode;
@@ -451,6 +451,9 @@ let Inode = (() => {
451
451
  data = args[0];
452
452
  args = [sizeof(Inode)];
453
453
  }
454
+ if (args[0] instanceof Inode) {
455
+ args = [new Uint8Array(args[0].buffer, args[0].byteOffset, args[0].byteLength)];
456
+ }
454
457
  super(...args);
455
458
  __runInitializers(this, ___data_extraInitializers);
456
459
  if (this.byteLength < sizeof(Inode)) {
@@ -373,7 +373,7 @@ export function watchFile(path, options, listener) {
373
373
  listener(curr, prev);
374
374
  }
375
375
  });
376
- statWatchers.set(normalizedPath, { watcher, listeners: new Set() });
376
+ statWatchers.set(normalizedPath, { watcher, listeners: new Set([listener]) });
377
377
  }
378
378
  watchFile;
379
379
  /**
@@ -22,9 +22,11 @@ export declare class Dirent<Name extends string | Buffer = string> implements _D
22
22
  */
23
23
  get path(): string;
24
24
  /**
25
+ * `vfs.path` is relative to the directory that was read,
26
+ * so `parentPath` is resolved against it— like Node does.
25
27
  * @internal
26
28
  */
27
- static from(vfs: VFSDirent, encoding?: BufferEncoding | 'buffer' | null): Dirent<string>;
29
+ static from(vfs: VFSDirent, encoding?: BufferEncoding | 'buffer' | null, parentPath?: string): Dirent<string>;
28
30
  isFile(): boolean;
29
31
  isDirectory(): boolean;
30
32
  isBlockDevice(): boolean;
package/dist/node/dir.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Buffer } from 'buffer';
2
- import { withErrno } from 'kerium';
3
2
  import { warn } from 'kerium/log';
4
- import { parse } from '../path.js';
3
+ import { join, parse } from '../path.js';
5
4
  import { DirType } from '../vfs/dir.js';
6
5
  import { readdir } from './promises.js';
7
6
  import { readdirSync } from './sync.js';
@@ -32,12 +31,14 @@ export class Dirent {
32
31
  return this._parentPath;
33
32
  }
34
33
  /**
34
+ * `vfs.path` is relative to the directory that was read,
35
+ * so `parentPath` is resolved against it— like Node does.
35
36
  * @internal
36
37
  */
37
- static from(vfs, encoding) {
38
+ static from(vfs, encoding, parentPath = '.') {
38
39
  const dirent = new Dirent();
39
40
  const { base, dir } = parse(vfs.path);
40
- dirent._parentPath = dir || '.';
41
+ dirent._parentPath = join(parentPath, dir);
41
42
  dirent._name = base;
42
43
  dirent.ino = vfs.ino;
43
44
  dirent.type = vfs.type;
@@ -66,6 +67,14 @@ export class Dirent {
66
67
  return this.type === DirType.SOCK;
67
68
  }
68
69
  }
70
+ /**
71
+ * Thrown when a `Dir` is used after it has been closed.
72
+ * Note this is a plain `Error` with a Node error code, not an `Exception`.
73
+ * @internal @hidden
74
+ */
75
+ function dirClosed() {
76
+ return Object.assign(new Error('Directory handle was closed'), { code: 'ERR_DIR_CLOSED' });
77
+ }
69
78
  /**
70
79
  * A class representing a directory stream.
71
80
  */
@@ -75,7 +84,7 @@ export class Dir {
75
84
  closed = false;
76
85
  checkClosed() {
77
86
  if (this.closed)
78
- throw withErrno('EBADF', 'Can not use closed Dir');
87
+ throw dirClosed();
79
88
  }
80
89
  _entries;
81
90
  constructor(path, context) {
@@ -83,6 +92,13 @@ export class Dir {
83
92
  this.context = context;
84
93
  }
85
94
  close(cb) {
95
+ if (this.closed) {
96
+ const error = dirClosed();
97
+ if (!cb)
98
+ return Promise.reject(error);
99
+ cb(error);
100
+ return;
101
+ }
86
102
  this.closed = true;
87
103
  if (!cb) {
88
104
  return Promise.resolve();
@@ -94,6 +110,8 @@ export class Dir {
94
110
  * Subsequent reads will result in errors.
95
111
  */
96
112
  closeSync() {
113
+ if (this.closed)
114
+ throw dirClosed();
97
115
  this.closed = true;
98
116
  }
99
117
  async _read() {
@@ -18,8 +18,10 @@ import { ReadStream, WriteStream } from './streams.js';
18
18
  export * as constants from '../constants.js';
19
19
  export declare class FileHandle implements promises.FileHandle {
20
20
  protected context: V_Context;
21
- readonly fd: number;
22
21
  protected vfs: Handle;
22
+ protected _fd: number;
23
+ /** The file descriptor, or -1 once the handle has been closed */
24
+ get fd(): number;
23
25
  constructor(context: V_Context, fd: number);
24
26
  private _emitChange;
25
27
  /**
@@ -239,7 +241,7 @@ export declare function writeFile(this: V_Context, path: fs.PathLike | promises.
239
241
  * @option mode Defaults to `0644`.
240
242
  * @option flag Defaults to `'a'`.
241
243
  */
242
- export declare function appendFile(this: V_Context, path: fs.PathLike | promises.FileHandle, data: FileContents, _options?: BufferEncoding | (fs.EncodingOption & {
244
+ export declare function appendFile(this: V_Context, path: fs.PathLike | promises.FileHandle, data: FileContents | Iterable<string | ArrayBufferView> | AsyncIterable<string | ArrayBufferView>, _options?: BufferEncoding | (fs.EncodingOption & {
243
245
  mode?: fs.Mode;
244
246
  flag?: fs.OpenMode;
245
247
  }) | null): Promise<void>;