@zenfs/core 0.9.2 → 0.9.4

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.
@@ -0,0 +1,1071 @@
1
+ import { Buffer } from 'buffer';
2
+ import type * as Node from 'node:fs';
3
+ import type * as promises from 'node:fs/promises';
4
+ import type { CreateReadStreamOptions, CreateWriteStreamOptions, FileChangeInfo, FileReadResult, FlagAndOpenMode } from 'node:fs/promises';
5
+ import type { ReadableStream as TReadableStream } from 'node:stream/web';
6
+ import type { Interface as ReadlineInterface } from 'readline';
7
+ import { ApiError, ErrorCode } from '../ApiError.js';
8
+ import { ActionType, File, isAppendable, isReadable, isWriteable, parseFlag, pathExistsAction, pathNotExistsAction } from '../file.js';
9
+ import { FileContents, FileSystem } from '../filesystem.js';
10
+ import { BigIntStats, FileType, type BigIntStatsFs, type Stats, type StatsFs } from '../stats.js';
11
+ import { normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
12
+ import * as constants from './constants.js';
13
+ import { Dir, Dirent } from './dir.js';
14
+ import { dirname, join, parse } from './path.js';
15
+ import type { PathLike } from './shared.js';
16
+ import { cred, fd2file, fdMap, fixError, getFdForFile, mounts, resolveMount } from './shared.js';
17
+ import { ReadStream, WriteStream } from './streams.js';
18
+ export * as constants from './constants.js';
19
+
20
+ export class FileHandle implements promises.FileHandle {
21
+ public constructor(
22
+ /**
23
+ * Gets the file descriptor for this file handle.
24
+ */
25
+ public readonly fd: number
26
+ ) {}
27
+
28
+ /**
29
+ * @internal
30
+ */
31
+ public get file(): File {
32
+ return fd2file(this.fd);
33
+ }
34
+
35
+ /**
36
+ * Asynchronous fchown(2) - Change ownership of a file.
37
+ */
38
+ public chown(uid: number, gid: number): Promise<void> {
39
+ return this.file.chown(uid, gid);
40
+ }
41
+
42
+ /**
43
+ * Asynchronous fchmod(2) - Change permissions of a file.
44
+ * @param mode A file mode. If a string is passed, it is parsed as an octal integer.
45
+ */
46
+ public chmod(mode: Node.Mode): Promise<void> {
47
+ const numMode = normalizeMode(mode, -1);
48
+ if (numMode < 0) {
49
+ throw new ApiError(ErrorCode.EINVAL, 'Invalid mode.');
50
+ }
51
+ return this.file.chmod(numMode);
52
+ }
53
+
54
+ /**
55
+ * Asynchronous fdatasync(2) - synchronize a file's in-core state with storage device.
56
+ */
57
+ public datasync(): Promise<void> {
58
+ return this.file.datasync();
59
+ }
60
+
61
+ /**
62
+ * Asynchronous fsync(2) - synchronize a file's in-core state with the underlying storage device.
63
+ */
64
+ public sync(): Promise<void> {
65
+ return this.file.sync();
66
+ }
67
+
68
+ /**
69
+ * Asynchronous ftruncate(2) - Truncate a file to a specified length.
70
+ * @param len If not specified, defaults to `0`.
71
+ */
72
+ public truncate(len?: number): Promise<void> {
73
+ if (len < 0) {
74
+ throw new ApiError(ErrorCode.EINVAL);
75
+ }
76
+ return this.file.truncate(len);
77
+ }
78
+
79
+ /**
80
+ * Asynchronously change file timestamps of the file.
81
+ * @param atime The last access time. If a string is provided, it will be coerced to number.
82
+ * @param mtime The last modified time. If a string is provided, it will be coerced to number.
83
+ */
84
+ public utimes(atime: string | number | Date, mtime: string | number | Date): Promise<void> {
85
+ return this.file.utimes(normalizeTime(atime), normalizeTime(mtime));
86
+ }
87
+
88
+ /**
89
+ * Asynchronously append data to a file, creating the file if it does not exist. The underlying file will _not_ be closed automatically.
90
+ * The `FileHandle` must have been opened for appending.
91
+ * @param data The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string.
92
+ * @param _options Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag.
93
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
94
+ * If `mode` is not supplied, the default of `0o666` is used.
95
+ * If `mode` is a string, it is parsed as an octal integer.
96
+ * If `flag` is not supplied, the default of `'a'` is used.
97
+ */
98
+ public async appendFile(data: string | Uint8Array, _options?: (Node.ObjectEncodingOptions & FlagAndOpenMode) | BufferEncoding): Promise<void> {
99
+ const options = normalizeOptions(_options, 'utf8', 'a', 0o644);
100
+ const flag = parseFlag(options.flag);
101
+ if (!isAppendable(flag)) {
102
+ throw new ApiError(ErrorCode.EINVAL, 'Flag passed to appendFile must allow for appending.');
103
+ }
104
+ if (typeof data != 'string' && !options.encoding) {
105
+ throw new ApiError(ErrorCode.EINVAL, 'Encoding not specified');
106
+ }
107
+ const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : data;
108
+ await this.file.write(encodedData, 0, encodedData.length, null);
109
+ }
110
+
111
+ /**
112
+ * Asynchronously reads data from the file.
113
+ * The `FileHandle` must have been opened for reading.
114
+ * @param buffer The buffer that the data will be written to.
115
+ * @param offset The offset in the buffer at which to start writing.
116
+ * @param length The number of bytes to read.
117
+ * @param position The offset from the beginning of the file from which data should be read. If `null`, data will be read from the current position.
118
+ */
119
+ public read<TBuffer extends NodeJS.ArrayBufferView>(buffer: TBuffer, offset?: number, length?: number, position?: number): Promise<FileReadResult<TBuffer>> {
120
+ if (isNaN(+position)) {
121
+ position = this.file.position!;
122
+ }
123
+ return this.file.read(buffer, offset, length, position);
124
+ }
125
+
126
+ /**
127
+ * Asynchronously reads the entire contents of a file. The underlying file will _not_ be closed automatically.
128
+ * The `FileHandle` must have been opened for reading.
129
+ * @param _options An object that may contain an optional flag.
130
+ * If a flag is not provided, it defaults to `'r'`.
131
+ */
132
+ public async readFile(_options?: { flag?: Node.OpenMode }): Promise<Buffer>;
133
+ public async readFile(_options: (Node.ObjectEncodingOptions & FlagAndOpenMode) | BufferEncoding): Promise<string>;
134
+ public async readFile(_options?: (Node.ObjectEncodingOptions & FlagAndOpenMode) | BufferEncoding): Promise<string | Buffer> {
135
+ const options = normalizeOptions(_options, null, 'r', 0o444);
136
+ const flag = parseFlag(options.flag);
137
+ if (!isReadable(flag)) {
138
+ throw new ApiError(ErrorCode.EINVAL, 'Flag passed must allow for reading.');
139
+ }
140
+
141
+ const { size } = await this.stat();
142
+ const data = new Uint8Array(size);
143
+ await this.file.read(data, 0, size, 0);
144
+ const buffer = Buffer.from(data);
145
+ return options.encoding ? buffer.toString(options.encoding) : buffer;
146
+ }
147
+
148
+ /**
149
+ * Returns a `ReadableStream` that may be used to read the files data.
150
+ *
151
+ * An error will be thrown if this method is called more than once or is called after the `FileHandle` is closed
152
+ * or closing.
153
+ *
154
+ * While the `ReadableStream` will read the file to completion, it will not close the `FileHandle` automatically. User code must still call the `fileHandle.close()` method.
155
+ *
156
+ * @since v17.0.0
157
+ * @experimental
158
+ */
159
+ public readableWebStream(options?: promises.ReadableWebStreamOptions): TReadableStream<Uint8Array> {
160
+ // Note: using an arrow function to preserve `this`
161
+ const start = async ({ close, enqueue, error }) => {
162
+ try {
163
+ const chunkSize = 64 * 1024,
164
+ maxChunks = 1e7;
165
+ let i = 0,
166
+ position = 0,
167
+ result: FileReadResult<Uint8Array>;
168
+
169
+ while (result.bytesRead > 0) {
170
+ result = await this.read(new Uint8Array(chunkSize), 0, chunkSize, position);
171
+ if (!result.bytesRead) {
172
+ close();
173
+ return;
174
+ }
175
+ enqueue(result.buffer.slice(0, result.bytesRead));
176
+ position += result.bytesRead;
177
+ if (++i >= maxChunks) {
178
+ throw new ApiError(ErrorCode.EFBIG, 'Too many iterations on readable stream', this.file.path, 'FileHandle.readableWebStream');
179
+ }
180
+ }
181
+ } catch (e) {
182
+ error(e);
183
+ }
184
+ };
185
+
186
+ return new globalThis.ReadableStream({ start, type: options.type });
187
+ }
188
+
189
+ public readLines(options?: promises.CreateReadStreamOptions): ReadlineInterface {
190
+ throw ApiError.With('ENOSYS', this.file.path, 'FileHandle.readLines');
191
+ }
192
+
193
+ public [Symbol.asyncDispose](): Promise<void> {
194
+ return this.close();
195
+ }
196
+
197
+ /**
198
+ * Asynchronous fstat(2) - Get file status.
199
+ */
200
+ public async stat(opts: Node.BigIntOptions): Promise<BigIntStats>;
201
+ public async stat(opts?: Node.StatOptions & { bigint?: false }): Promise<Stats>;
202
+ public async stat(opts?: Node.StatOptions): Promise<Stats | BigIntStats> {
203
+ const stats = await this.file.stat();
204
+ return opts?.bigint ? new BigIntStats(stats) : stats;
205
+ }
206
+
207
+ public async write(data: FileContents, posOrOff?: number, lenOrEnc?: BufferEncoding | number, position?: number): Promise<{ bytesWritten: number; buffer: FileContents }>;
208
+
209
+ /**
210
+ * Asynchronously writes `buffer` to the file.
211
+ * The `FileHandle` must have been opened for writing.
212
+ * @param buffer The buffer that the data will be written to.
213
+ * @param offset The part of the buffer to be written. If not supplied, defaults to `0`.
214
+ * @param length The number of bytes to write. If not supplied, defaults to `buffer.length - offset`.
215
+ * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position.
216
+ */
217
+ public async write<TBuffer extends Uint8Array>(buffer: TBuffer, offset?: number, length?: number, position?: number): Promise<{ bytesWritten: number; buffer: TBuffer }>;
218
+
219
+ /**
220
+ * Asynchronously writes `string` to the file.
221
+ * The `FileHandle` must have been opened for writing.
222
+ * It is unsafe to call `write()` multiple times on the same file without waiting for the `Promise`
223
+ * to be resolved (or rejected). For this scenario, `fs.createWriteStream` is strongly recommended.
224
+ * @param string A string to write.
225
+ * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position.
226
+ * @param encoding The expected string encoding.
227
+ */
228
+ public async write(data: string, position?: number, encoding?: BufferEncoding): Promise<{ bytesWritten: number; buffer: string }>;
229
+
230
+ public async write(data: FileContents, posOrOff?: number, lenOrEnc?: BufferEncoding | number, position?: number): Promise<{ bytesWritten: number; buffer: FileContents }> {
231
+ let buffer: Uint8Array,
232
+ offset: number = 0,
233
+ length: number;
234
+ if (typeof data === 'string') {
235
+ // Signature 1: (fd, string, [position?, [encoding?]])
236
+ position = typeof posOrOff === 'number' ? posOrOff : null;
237
+ const encoding = <BufferEncoding>(typeof lenOrEnc === 'string' ? lenOrEnc : 'utf8');
238
+ offset = 0;
239
+ buffer = Buffer.from(data, encoding);
240
+ length = buffer.length;
241
+ } else {
242
+ // Signature 2: (fd, buffer, offset, length, position?)
243
+ buffer = data;
244
+ offset = posOrOff;
245
+ length = lenOrEnc as number;
246
+ position = typeof position === 'number' ? position : null;
247
+ }
248
+
249
+ position ??= this.file.position!;
250
+ const bytesWritten = await this.file.write(buffer, offset, length, position);
251
+ return { buffer, bytesWritten };
252
+ }
253
+
254
+ /**
255
+ * Asynchronously writes data to a file, replacing the file if it already exists. The underlying file will _not_ be closed automatically.
256
+ * The `FileHandle` must have been opened for writing.
257
+ * It is unsafe to call `writeFile()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected).
258
+ * @param data The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string.
259
+ * @param _options Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag.
260
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
261
+ * If `mode` is not supplied, the default of `0o666` is used.
262
+ * If `mode` is a string, it is parsed as an octal integer.
263
+ * If `flag` is not supplied, the default of `'w'` is used.
264
+ */
265
+ public async writeFile(data: string | Uint8Array, _options?: Node.WriteFileOptions): Promise<void> {
266
+ const options = normalizeOptions(_options, 'utf8', 'w', 0o644);
267
+ const flag = parseFlag(options.flag);
268
+ if (!isWriteable(flag)) {
269
+ throw new ApiError(ErrorCode.EINVAL, 'Flag passed must allow for writing.');
270
+ }
271
+ if (typeof data != 'string' && !options.encoding) {
272
+ throw new ApiError(ErrorCode.EINVAL, 'Encoding not specified');
273
+ }
274
+ const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : data;
275
+ await this.file.write(encodedData, 0, encodedData.length, 0);
276
+ }
277
+
278
+ /**
279
+ * Asynchronous close(2) - close a `FileHandle`.
280
+ */
281
+ public async close(): Promise<void> {
282
+ await this.file.close();
283
+ fdMap.delete(this.fd);
284
+ }
285
+
286
+ /**
287
+ * Asynchronous `writev`. Writes from multiple buffers.
288
+ * @param buffers An array of Uint8Array buffers.
289
+ * @param position The position in the file where to begin writing.
290
+ * @returns The number of bytes written.
291
+ */
292
+ public async writev(buffers: Uint8Array[], position?: number): Promise<Node.WriteVResult> {
293
+ let bytesWritten = 0;
294
+
295
+ for (const buffer of buffers) {
296
+ bytesWritten += (await this.write(buffer, 0, buffer.length, position + bytesWritten)).bytesWritten;
297
+ }
298
+
299
+ return { bytesWritten, buffers };
300
+ }
301
+
302
+ /**
303
+ * Asynchronous `readv`. Reads into multiple buffers.
304
+ * @param buffers An array of Uint8Array buffers.
305
+ * @param position The position in the file where to begin reading.
306
+ * @returns The number of bytes read.
307
+ */
308
+ public async readv(buffers: NodeJS.ArrayBufferView[], position?: number): Promise<Node.ReadVResult> {
309
+ let bytesRead = 0;
310
+
311
+ for (const buffer of buffers) {
312
+ bytesRead += (await this.read(buffer, 0, buffer.byteLength, position + bytesRead)).bytesRead;
313
+ }
314
+
315
+ return { bytesRead, buffers };
316
+ }
317
+
318
+ /**
319
+ * Creates a `ReadStream` for reading from the file.
320
+ *
321
+ * @param options Options for the readable stream
322
+ * @returns A `ReadStream` object.
323
+ */
324
+ public createReadStream(options?: CreateReadStreamOptions): ReadStream {
325
+ const streamOptions = {
326
+ highWaterMark: options?.highWaterMark || 64 * 1024,
327
+ encoding: options?.encoding,
328
+
329
+ read: async (size: number) => {
330
+ try {
331
+ const result = await this.read(new Uint8Array(size), 0, size, this.file.position);
332
+ stream.push(!result.bytesRead ? null : result.buffer.slice(0, result.bytesRead)); // Push data or null for EOF
333
+ this.file.position += result.bytesRead;
334
+ } catch (error) {
335
+ stream.destroy(error);
336
+ }
337
+ },
338
+ };
339
+
340
+ const stream = new ReadStream(streamOptions);
341
+ stream.path = this.file.path;
342
+ return stream;
343
+ }
344
+
345
+ /**
346
+ * Creates a `WriteStream` for writing to the file.
347
+ *
348
+ * @param options Options for the writeable stream.
349
+ * @returns A `WriteStream` object
350
+ */
351
+ public createWriteStream(options?: CreateWriteStreamOptions): WriteStream {
352
+ const streamOptions = {
353
+ highWaterMark: options?.highWaterMark,
354
+ encoding: options?.encoding,
355
+
356
+ write: async (chunk: Uint8Array, encoding: BufferEncoding, callback: (error?: Error | null) => void) => {
357
+ try {
358
+ const { bytesWritten } = await this.write(chunk, null, encoding);
359
+ callback(bytesWritten == chunk.length ? null : new Error('Failed to write full chunk'));
360
+ } catch (error) {
361
+ callback(error);
362
+ }
363
+ },
364
+ };
365
+
366
+ const stream = new WriteStream(streamOptions);
367
+ stream.path = this.file.path;
368
+ return stream;
369
+ }
370
+ }
371
+
372
+ type FileSystemMethod = {
373
+ [K in keyof FileSystem]: FileSystem[K] extends (...args) => unknown
374
+ ? (name: K, resolveSymlinks: boolean, ...args: Parameters<FileSystem[K]>) => ReturnType<FileSystem[K]>
375
+ : never;
376
+ }[keyof FileSystem]; // https://stackoverflow.com/a/76335220/17637456
377
+
378
+ /**
379
+ * Utility for FS ops. It handles
380
+ * - path normalization (for the first parameter to the FS op)
381
+ * - path translation for errors
382
+ * - FS/mount point resolution
383
+ *
384
+ * It can't be used for functions which may operate on multiple mounted FSs or paths (e.g. `rename`)
385
+ * @param name the function name
386
+ * @param resolveSymlinks whether to resolve symlinks
387
+ * @param args the rest of the parameters are passed to the FS function. Note that the first parameter is required to be a path
388
+ * @returns
389
+ */
390
+ async function doOp<M extends FileSystemMethod, RT extends ReturnType<M> = ReturnType<M>>(...[name, resolveSymlinks, rawPath, ...args]: Parameters<M>): Promise<RT> {
391
+ rawPath = normalizePath(rawPath);
392
+ const _path = resolveSymlinks && (await exists(rawPath)) ? await realpath(rawPath) : rawPath;
393
+ const { fs, path } = resolveMount(_path);
394
+ try {
395
+ // @ts-expect-error 2556 (since ...args is not correctly picked up as being a tuple)
396
+ return fs[name](path, ...args) as Promise<RT>;
397
+ } catch (e) {
398
+ throw fixError(e, { [path]: rawPath });
399
+ }
400
+ }
401
+
402
+ // fs.promises
403
+
404
+ /**
405
+ * Renames a file
406
+ * @param oldPath
407
+ * @param newPath
408
+ */
409
+ export async function rename(oldPath: PathLike, newPath: PathLike): Promise<void> {
410
+ oldPath = normalizePath(oldPath);
411
+ newPath = normalizePath(newPath);
412
+ const src = resolveMount(oldPath);
413
+ const dst = resolveMount(newPath);
414
+ try {
415
+ if (src.mountPoint == dst.mountPoint) {
416
+ await src.fs.rename(src.path, dst.path, cred);
417
+ return;
418
+ }
419
+ await writeFile(newPath, await readFile(oldPath));
420
+ await unlink(oldPath);
421
+ } catch (e) {
422
+ throw fixError(e, { [src.path]: oldPath, [dst.path]: newPath });
423
+ }
424
+ }
425
+ rename satisfies typeof promises.rename;
426
+
427
+ /**
428
+ * Test whether or not the given path exists by checking with the file system.
429
+ * @param _path
430
+ */
431
+ export async function exists(_path: PathLike): Promise<boolean> {
432
+ try {
433
+ const { fs, path } = resolveMount(await realpath(_path));
434
+ return await fs.exists(path, cred);
435
+ } catch (e) {
436
+ if ((e as ApiError).errno == ErrorCode.ENOENT) {
437
+ return false;
438
+ }
439
+
440
+ throw e;
441
+ }
442
+ }
443
+
444
+ /**
445
+ * `stat`.
446
+ * @param path
447
+ * @returns Stats
448
+ */
449
+ export async function stat(path: PathLike, options: Node.BigIntOptions): Promise<BigIntStats>;
450
+ export async function stat(path: PathLike, options?: { bigint?: false }): Promise<Stats>;
451
+ export async function stat(path: PathLike, options?: Node.StatOptions): Promise<Stats | BigIntStats>;
452
+ export async function stat(path: PathLike, options?: Node.StatOptions): Promise<Stats | BigIntStats> {
453
+ const stats: Stats = await doOp('stat', true, path, cred);
454
+ return options?.bigint ? new BigIntStats(stats) : stats;
455
+ }
456
+ stat satisfies typeof promises.stat;
457
+
458
+ /**
459
+ * `lstat`.
460
+ * `lstat()` is identical to `stat()`, except that if path is a symbolic link,
461
+ * then the link itself is stat-ed, not the file that it refers to.
462
+ * @param path
463
+ * @return
464
+ */
465
+ export async function lstat(path: PathLike, options?: { bigint?: false }): Promise<Stats>;
466
+ export async function lstat(path: PathLike, options: { bigint: true }): Promise<BigIntStats>;
467
+ export async function lstat(path: PathLike, options?: Node.StatOptions): Promise<Stats | BigIntStats> {
468
+ const stats: Stats = await doOp('stat', false, path, cred);
469
+ return options?.bigint ? new BigIntStats(stats) : stats;
470
+ }
471
+ lstat satisfies typeof promises.lstat;
472
+
473
+ // FILE-ONLY METHODS
474
+
475
+ /**
476
+ * `truncate`.
477
+ * @param path
478
+ * @param len
479
+ */
480
+ export async function truncate(path: PathLike, len: number = 0): Promise<void> {
481
+ const handle = await open(path, 'r+');
482
+ try {
483
+ await handle.truncate(len);
484
+ } finally {
485
+ await handle.close();
486
+ }
487
+ }
488
+ truncate satisfies typeof promises.truncate;
489
+
490
+ /**
491
+ * `unlink`.
492
+ * @param path
493
+ */
494
+ export async function unlink(path: PathLike): Promise<void> {
495
+ return doOp('unlink', false, path, cred);
496
+ }
497
+ unlink satisfies typeof promises.unlink;
498
+
499
+ /**
500
+ * Opens a file. This helper handles the complexity of file flags.
501
+ * @internal
502
+ */
503
+ async function _open(_path: PathLike, _flag: string, _mode: Node.Mode = 0o644, resolveSymlinks: boolean): Promise<File> {
504
+ const path = normalizePath(_path),
505
+ mode = normalizeMode(_mode, 0o644),
506
+ flag = parseFlag(_flag);
507
+
508
+ try {
509
+ switch (pathExistsAction(flag)) {
510
+ case ActionType.THROW:
511
+ throw ApiError.With('EEXIST', path, '_open');
512
+ case ActionType.TRUNCATE:
513
+ /*
514
+ In a previous implementation, we deleted the file and
515
+ re-created it. However, this created a race condition if another
516
+ asynchronous request was trying to read the file, as the file
517
+ would not exist for a small period of time.
518
+ */
519
+ const file: File = await doOp('openFile', resolveSymlinks, path, flag, cred);
520
+ if (!file) {
521
+ throw new ApiError(ErrorCode.EIO, 'Impossible code path reached');
522
+ }
523
+ await file.truncate(0);
524
+ await file.sync();
525
+ return file;
526
+ case ActionType.NOP:
527
+ // Must await so thrown errors are caught by the catch below
528
+ return await doOp('openFile', resolveSymlinks, path, flag, cred);
529
+ default:
530
+ throw new ApiError(ErrorCode.EINVAL, 'Invalid file flag');
531
+ }
532
+ } catch (e) {
533
+ switch (pathNotExistsAction(flag)) {
534
+ case ActionType.CREATE:
535
+ // Ensure parent exists.
536
+ const parentStats: Stats = await doOp('stat', resolveSymlinks, dirname(path), cred);
537
+ if (parentStats && !parentStats.isDirectory()) {
538
+ throw ApiError.With('ENOTDIR', dirname(path), '_open');
539
+ }
540
+ return await doOp('createFile', resolveSymlinks, path, flag, mode, cred);
541
+ case ActionType.THROW:
542
+ throw ApiError.With('ENOENT', path, '_open');
543
+ default:
544
+ throw new ApiError(ErrorCode.EINVAL, 'Invalid file flag');
545
+ }
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Asynchronous file open.
551
+ * @see http://www.manpagez.com/man/2/open/
552
+ * @param flags Handles the complexity of the various file modes. See its API for more details.
553
+ * @param mode Mode to use to open the file. Can be ignored if the filesystem doesn't support permissions.
554
+ */
555
+ export async function open(path: PathLike, flag: string, mode: Node.Mode = 0o644): Promise<FileHandle> {
556
+ const file = await _open(path, flag, mode, true);
557
+ return new FileHandle(getFdForFile(file));
558
+ }
559
+ open satisfies typeof promises.open;
560
+
561
+ /**
562
+ * Opens a file without resolving symlinks
563
+ * @internal
564
+ */
565
+ export async function lopen(path: PathLike, flag: string, mode: Node.Mode = 0o644): Promise<FileHandle> {
566
+ const file: File = await _open(path, flag, mode, false);
567
+ return new FileHandle(getFdForFile(file));
568
+ }
569
+
570
+ /**
571
+ * Asynchronously reads the entire contents of a file.
572
+ */
573
+ async function _readFile(fname: string, flag: string, resolveSymlinks: boolean): Promise<Uint8Array> {
574
+ const file = await _open(normalizePath(fname), flag, 0o644, resolveSymlinks);
575
+
576
+ try {
577
+ const stat = await file.stat();
578
+ const data = new Uint8Array(stat.size);
579
+ await file.read(data, 0, stat.size, 0);
580
+ await file.close();
581
+ return data;
582
+ } catch (e) {
583
+ await file.close();
584
+ throw e;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Asynchronously reads the entire contents of a file.
590
+ * @param filename
591
+ * @param options
592
+ * options.encoding The string encoding for the file contents. Defaults to `null`.
593
+ * options.flag Defaults to `'r'`.
594
+ * @returns file data
595
+ */
596
+ export async function readFile(filename: PathLike, options?: { flag?: Node.OpenMode }): Promise<Buffer>;
597
+ export async function readFile(filename: PathLike, options: (Node.EncodingOption & { flag?: Node.OpenMode }) | BufferEncoding): Promise<string>;
598
+ export async function readFile(filename: PathLike, _options?: (Node.EncodingOption & { flag?: Node.OpenMode }) | BufferEncoding): Promise<Buffer | string> {
599
+ const options = normalizeOptions(_options, null, 'r', 0);
600
+ const flag = parseFlag(options.flag);
601
+ if (!isReadable(flag)) {
602
+ throw new ApiError(ErrorCode.EINVAL, 'Flag passed must allow for reading.');
603
+ }
604
+
605
+ const data: Buffer = Buffer.from(await _readFile(filename, options.flag, true));
606
+ return options.encoding ? data.toString(options.encoding) : data;
607
+ }
608
+ readFile satisfies typeof promises.readFile;
609
+
610
+ /**
611
+ * Asynchronously writes data to a file, replacing the file if it already exists.
612
+ *
613
+ * The encoding option is ignored if data is a buffer.
614
+ * @param filename
615
+ * @param data
616
+ * @param _options
617
+ * @option options encoding Defaults to `'utf8'`.
618
+ * @option options mode Defaults to `0644`.
619
+ * @option options flag Defaults to `'w'`.
620
+ */
621
+ export async function writeFile(filename: PathLike, data: FileContents, _options?: Node.WriteFileOptions): Promise<void> {
622
+ const options = normalizeOptions(_options, 'utf8', 'w+', 0o644);
623
+ const handle = await open(filename, options.flag, options.mode);
624
+ try {
625
+ await handle.writeFile(data, options);
626
+ } finally {
627
+ await handle.close();
628
+ }
629
+ }
630
+ writeFile satisfies typeof promises.writeFile;
631
+
632
+ /**
633
+ * Asynchronously append data to a file, creating the file if
634
+ * it not yet exists.
635
+ */
636
+ async function _appendFile(fname: string, data: Uint8Array, flag: string, mode: number, resolveSymlinks: boolean): Promise<void> {
637
+ const file = await _open(fname, flag, mode, resolveSymlinks);
638
+ try {
639
+ await file.write(data, 0, data.length, null);
640
+ } finally {
641
+ await file.close();
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Asynchronously append data to a file, creating the file if it not yet
647
+ * exists.
648
+ * @param filename
649
+ * @param data
650
+ * @param options
651
+ * @option options encoding Defaults to `'utf8'`.
652
+ * @option options mode Defaults to `0644`.
653
+ * @option options flag Defaults to `'a'`.
654
+ */
655
+ export async function appendFile(
656
+ filename: PathLike,
657
+ data: FileContents,
658
+ _options?: BufferEncoding | (Node.EncodingOption & { mode?: Node.Mode; flag?: Node.OpenMode })
659
+ ): Promise<void> {
660
+ const options = normalizeOptions(_options, 'utf8', 'a', 0o644);
661
+ const flag = parseFlag(options.flag);
662
+ if (!isAppendable(flag)) {
663
+ throw new ApiError(ErrorCode.EINVAL, 'Flag passed to appendFile must allow for appending.');
664
+ }
665
+ if (typeof data != 'string' && !options.encoding) {
666
+ throw new ApiError(ErrorCode.EINVAL, 'Encoding not specified');
667
+ }
668
+ const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : data;
669
+ await _appendFile(filename, encodedData, options.flag, options.mode, true);
670
+ }
671
+ appendFile satisfies typeof promises.appendFile;
672
+
673
+ // DIRECTORY-ONLY METHODS
674
+
675
+ /**
676
+ * `rmdir`.
677
+ * @param path
678
+ */
679
+ export async function rmdir(path: PathLike): Promise<void> {
680
+ return doOp('rmdir', true, path, cred);
681
+ }
682
+ rmdir satisfies typeof promises.rmdir;
683
+
684
+ /**
685
+ * `mkdir`.
686
+ * @param path
687
+ * @param mode defaults to `0777`
688
+ */
689
+ export async function mkdir(path: PathLike, mode?: Node.Mode | (Node.MakeDirectoryOptions & { recursive?: false })): Promise<void>;
690
+ export async function mkdir(path: PathLike, mode: Node.MakeDirectoryOptions & { recursive: true }): Promise<string>;
691
+ export async function mkdir(path: PathLike, mode?: Node.Mode | Node.MakeDirectoryOptions): Promise<string | void> {
692
+ await doOp('mkdir', true, path, normalizeMode(typeof mode == 'object' ? mode?.mode : mode, 0o777), cred);
693
+ }
694
+ mkdir satisfies typeof promises.mkdir;
695
+
696
+ /**
697
+ * `readdir`. Reads the contents of a directory.
698
+ * @param path
699
+ */
700
+ export async function readdir(path: PathLike, options?: (Node.EncodingOption & { withFileTypes?: false }) | BufferEncoding): Promise<string[]>;
701
+ export async function readdir(path: PathLike, options: Node.BufferEncodingOption & { withFileTypes?: false }): Promise<Buffer[]>;
702
+ export async function readdir(path: PathLike, options: Node.EncodingOption & { withFileTypes: true }): Promise<Dirent[]>;
703
+ export async function readdir(
704
+ path: PathLike,
705
+ options?: (Node.EncodingOption & { withFileTypes?: boolean }) | BufferEncoding | (Node.BufferEncodingOption & { withFileTypes?: boolean })
706
+ ): Promise<string[] | Dirent[] | Buffer[]> {
707
+ path = normalizePath(path);
708
+ const entries: string[] = await doOp('readdir', true, path, cred);
709
+ const points = [...mounts.keys()];
710
+ for (const point of points) {
711
+ if (point.startsWith(path)) {
712
+ const entry = point.slice(path.length);
713
+ if (entry.includes('/') || entry.length == 0) {
714
+ // ignore FSs mounted in subdirectories and any FS mounted to `path`.
715
+ continue;
716
+ }
717
+ entries.push(entry);
718
+ }
719
+ }
720
+ const values: (string | Dirent)[] = [];
721
+ for (const entry of entries) {
722
+ values.push(typeof options == 'object' && options?.withFileTypes ? new Dirent(entry, await stat(join(path, entry))) : entry);
723
+ }
724
+ return values as string[] | Dirent[];
725
+ }
726
+ readdir satisfies typeof promises.readdir;
727
+
728
+ // SYMLINK METHODS
729
+
730
+ /**
731
+ * `link`.
732
+ * @param existing
733
+ * @param newpath
734
+ */
735
+ export async function link(existing: PathLike, newpath: PathLike): Promise<void> {
736
+ newpath = normalizePath(newpath);
737
+ return doOp('link', false, existing, newpath, cred);
738
+ }
739
+ link satisfies typeof promises.link;
740
+
741
+ /**
742
+ * `symlink`.
743
+ * @param target target path
744
+ * @param path link path
745
+ * @param type can be either `'dir'` or `'file'` (default is `'file'`)
746
+ */
747
+ export async function symlink(target: PathLike, path: PathLike, type: Node.symlink.Type = 'file'): Promise<void> {
748
+ if (!['file', 'dir', 'junction'].includes(type)) {
749
+ throw new ApiError(ErrorCode.EINVAL, 'Invalid symlink type: ' + type);
750
+ }
751
+
752
+ if (await exists(path)) {
753
+ throw ApiError.With('EEXIST', path, 'symlink');
754
+ }
755
+
756
+ await writeFile(path, target);
757
+ const file = await _open(path, 'r+', 0o644, false);
758
+ await file._setType(FileType.SYMLINK);
759
+ }
760
+ symlink satisfies typeof promises.symlink;
761
+
762
+ /**
763
+ * readlink.
764
+ * @param path
765
+ */
766
+ export async function readlink(path: PathLike, options: Node.BufferEncodingOption): Promise<Buffer>;
767
+ export async function readlink(path: PathLike, options?: Node.EncodingOption | BufferEncoding): Promise<string>;
768
+ export async function readlink(path: PathLike, options?: Node.BufferEncodingOption | Node.EncodingOption | BufferEncoding): Promise<string | Buffer> {
769
+ const value: Buffer = Buffer.from(await _readFile(path, 'r', false));
770
+ const encoding: BufferEncoding | 'buffer' = typeof options == 'object' ? options.encoding : options;
771
+ if (encoding == 'buffer') {
772
+ return value;
773
+ }
774
+ return value.toString(encoding);
775
+ }
776
+ readlink satisfies typeof promises.readlink;
777
+
778
+ // PROPERTY OPERATIONS
779
+
780
+ /**
781
+ * `chown`.
782
+ * @param path
783
+ * @param uid
784
+ * @param gid
785
+ */
786
+ export async function chown(path: PathLike, uid: number, gid: number): Promise<void> {
787
+ const handle = await open(path, 'r+');
788
+ try {
789
+ await handle.chown(uid, gid);
790
+ } finally {
791
+ await handle.close();
792
+ }
793
+ }
794
+ chown satisfies typeof promises.chown;
795
+
796
+ /**
797
+ * `lchown`.
798
+ * @param path
799
+ * @param uid
800
+ * @param gid
801
+ */
802
+ export async function lchown(path: PathLike, uid: number, gid: number): Promise<void> {
803
+ const handle = await lopen(path, 'r+');
804
+ try {
805
+ await handle.chown(uid, gid);
806
+ } finally {
807
+ await handle.close();
808
+ }
809
+ }
810
+ lchown satisfies typeof promises.lchown;
811
+
812
+ /**
813
+ * `chmod`.
814
+ * @param path
815
+ * @param mode
816
+ */
817
+ export async function chmod(path: PathLike, mode: Node.Mode): Promise<void> {
818
+ const handle = await open(path, 'r+');
819
+ try {
820
+ await handle.chmod(mode);
821
+ } finally {
822
+ await handle.close();
823
+ }
824
+ }
825
+ chmod satisfies typeof promises.chmod;
826
+
827
+ /**
828
+ * `lchmod`.
829
+ * @param path
830
+ * @param mode
831
+ */
832
+ export async function lchmod(path: PathLike, mode: Node.Mode): Promise<void> {
833
+ const handle = await lopen(path, 'r+');
834
+ try {
835
+ await handle.chmod(mode);
836
+ } finally {
837
+ await handle.close();
838
+ }
839
+ }
840
+ lchmod satisfies typeof promises.lchmod;
841
+
842
+ /**
843
+ * Change file timestamps of the file referenced by the supplied path.
844
+ * @param path
845
+ * @param atime
846
+ * @param mtime
847
+ */
848
+ export async function utimes(path: PathLike, atime: string | number | Date, mtime: string | number | Date): Promise<void> {
849
+ const handle = await open(path, 'r+');
850
+ try {
851
+ await handle.utimes(atime, mtime);
852
+ } finally {
853
+ await handle.close();
854
+ }
855
+ }
856
+ utimes satisfies typeof promises.utimes;
857
+
858
+ /**
859
+ * Change file timestamps of the file referenced by the supplied path.
860
+ * @param path
861
+ * @param atime
862
+ * @param mtime
863
+ */
864
+ export async function lutimes(path: PathLike, atime: number | Date, mtime: number | Date): Promise<void> {
865
+ const handle = await lopen(path, 'r+');
866
+ try {
867
+ await handle.utimes(atime, mtime);
868
+ } finally {
869
+ await handle.close();
870
+ }
871
+ }
872
+ lutimes satisfies typeof promises.lutimes;
873
+
874
+ /**
875
+ * Asynchronous realpath(3) - return the canonicalized absolute pathname.
876
+ * @param path A path to a file. If a URL is provided, it must use the `file:` protocol.
877
+ * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used.
878
+ *
879
+ * Note: This *Can not* use doOp since doOp depends on it
880
+ */
881
+ export async function realpath(path: PathLike, options: Node.BufferEncodingOption): Promise<Buffer>;
882
+ export async function realpath(path: PathLike, options?: Node.EncodingOption | BufferEncoding): Promise<string>;
883
+ export async function realpath(path: PathLike, options?: Node.EncodingOption | BufferEncoding | Node.BufferEncodingOption): Promise<string | Buffer> {
884
+ path = normalizePath(path);
885
+ const { base, dir } = parse(path);
886
+ const lpath = join(dir == '/' ? '/' : await realpath(dir), base);
887
+ const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
888
+
889
+ try {
890
+ const stats = await fs.stat(resolvedPath, cred);
891
+ if (!stats.isSymbolicLink()) {
892
+ return lpath;
893
+ }
894
+
895
+ return realpath(mountPoint + (await readlink(lpath)));
896
+ } catch (e) {
897
+ throw fixError(e, { [resolvedPath]: lpath });
898
+ }
899
+ }
900
+ realpath satisfies typeof promises.realpath;
901
+
902
+ /**
903
+ * @todo Implement
904
+ */
905
+ export function watch(filename: PathLike, options: (Node.WatchOptions & { encoding: 'buffer' }) | 'buffer'): AsyncIterable<FileChangeInfo<Buffer>>;
906
+ export function watch(filename: PathLike, options?: Node.WatchOptions | BufferEncoding): AsyncIterable<FileChangeInfo<string>>;
907
+ export function watch(filename: PathLike, options: Node.WatchOptions | string): AsyncIterable<FileChangeInfo<string>> | AsyncIterable<FileChangeInfo<Buffer>> {
908
+ throw ApiError.With('ENOSYS', filename, 'watch');
909
+ }
910
+ watch satisfies typeof promises.watch;
911
+
912
+ /**
913
+ * `access`.
914
+ * @param path
915
+ * @param mode
916
+ */
917
+ export async function access(path: PathLike, mode: number = constants.F_OK): Promise<void> {
918
+ const stats = await stat(path);
919
+ if (!stats.hasAccess(mode, cred)) {
920
+ throw new ApiError(ErrorCode.EACCES);
921
+ }
922
+ }
923
+ access satisfies typeof promises.access;
924
+
925
+ /**
926
+ * Asynchronous `rm`. Removes files or directories (recursively).
927
+ * @param path The path to the file or directory to remove.
928
+ */
929
+ export async function rm(path: PathLike, options?: Node.RmOptions) {
930
+ path = normalizePath(path);
931
+
932
+ const stats = await stat(path);
933
+
934
+ switch (stats.mode & constants.S_IFMT) {
935
+ case constants.S_IFDIR:
936
+ if (options?.recursive) {
937
+ for (const entry of await readdir(path)) {
938
+ await rm(join(path, entry));
939
+ }
940
+ }
941
+
942
+ await rmdir(path);
943
+ return;
944
+ case constants.S_IFREG:
945
+ case constants.S_IFLNK:
946
+ await unlink(path);
947
+ return;
948
+ case constants.S_IFBLK:
949
+ case constants.S_IFCHR:
950
+ case constants.S_IFIFO:
951
+ case constants.S_IFSOCK:
952
+ default:
953
+ throw new ApiError(ErrorCode.EPERM, 'File type not supported', path, 'rm');
954
+ }
955
+ }
956
+ rm satisfies typeof promises.rm;
957
+
958
+ /**
959
+ * Asynchronous `mkdtemp`. Creates a unique temporary directory.
960
+ * @param prefix The directory prefix.
961
+ * @param options The encoding (or an object including `encoding`).
962
+ * @returns The path to the created temporary directory, encoded as a string or buffer.
963
+ */
964
+ export async function mkdtemp(prefix: string, options?: Node.EncodingOption): Promise<string>;
965
+ export async function mkdtemp(prefix: string, options?: Node.BufferEncodingOption): Promise<Buffer>;
966
+ export async function mkdtemp(prefix: string, options?: Node.EncodingOption | Node.BufferEncodingOption): Promise<string | Buffer> {
967
+ const encoding = typeof options === 'object' ? options.encoding : options || 'utf8';
968
+ const fsName = `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`;
969
+ const resolvedPath = '/tmp/' + fsName;
970
+
971
+ await mkdir(resolvedPath);
972
+
973
+ return encoding == 'buffer' ? Buffer.from(resolvedPath) : resolvedPath;
974
+ }
975
+ mkdtemp satisfies typeof promises.mkdtemp;
976
+
977
+ /**
978
+ * Asynchronous `copyFile`. Copies a file.
979
+ * @param src The source file.
980
+ * @param dest The destination file.
981
+ * @param mode Optional flags for the copy operation. Currently supports these flags:
982
+ * * `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails.
983
+ */
984
+ export async function copyFile(src: PathLike, dest: PathLike, mode?: number): Promise<void> {
985
+ src = normalizePath(src);
986
+ dest = normalizePath(dest);
987
+
988
+ if (mode && mode & constants.COPYFILE_EXCL && (await exists(dest))) {
989
+ throw new ApiError(ErrorCode.EEXIST, 'Destination file already exists.', dest, 'copyFile');
990
+ }
991
+
992
+ await writeFile(dest, await readFile(src));
993
+ }
994
+ copyFile satisfies typeof promises.copyFile;
995
+
996
+ /**
997
+ * Asynchronous `opendir`. Opens a directory.
998
+ * @param path The path to the directory.
999
+ * @param options Options for opening the directory.
1000
+ * @returns A `Dir` object representing the opened directory.
1001
+ */
1002
+ export async function opendir(path: PathLike, options?: Node.OpenDirOptions): Promise<Dir> {
1003
+ path = normalizePath(path);
1004
+ return new Dir(path);
1005
+ }
1006
+ opendir satisfies typeof promises.opendir;
1007
+
1008
+ /**
1009
+ * Asynchronous `cp`. Recursively copies a file or directory.
1010
+ * @param source The source file or directory.
1011
+ * @param destination The destination file or directory.
1012
+ * @param opts Options for the copy operation. Currently supports these options from Node.js 'fs.await cp':
1013
+ * * `dereference`: Dereference symbolic links.
1014
+ * * `errorOnExist`: Throw an error if the destination file or directory already exists.
1015
+ * * `filter`: A function that takes a source and destination path and returns a boolean, indicating whether to copy the given source element.
1016
+ * * `force`: Overwrite the destination if it exists, and overwrite existing readonly destination files.
1017
+ * * `preserveTimestamps`: Preserve file timestamps.
1018
+ * * `recursive`: If `true`, copies directories recursively.
1019
+ */
1020
+ export async function cp(source: PathLike, destination: PathLike, opts?: Node.CopyOptions): Promise<void> {
1021
+ source = normalizePath(source);
1022
+ destination = normalizePath(destination);
1023
+
1024
+ const srcStats = await lstat(source); // Use lstat to follow symlinks if not dereferencing
1025
+
1026
+ if (opts?.errorOnExist && (await exists(destination))) {
1027
+ throw new ApiError(ErrorCode.EEXIST, 'Destination file or directory already exists.', destination, 'cp');
1028
+ }
1029
+
1030
+ switch (srcStats.mode & constants.S_IFMT) {
1031
+ case constants.S_IFDIR:
1032
+ if (!opts?.recursive) {
1033
+ throw new ApiError(ErrorCode.EISDIR, source + ' is a directory (not copied)', source, 'cp');
1034
+ }
1035
+ await mkdir(destination, { recursive: true }); // Ensure the destination directory exists
1036
+ for (const dirent of await readdir(source, { withFileTypes: true })) {
1037
+ if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) {
1038
+ continue; // Skip if the filter returns false
1039
+ }
1040
+ await cp(join(source, dirent.name), join(destination, dirent.name), opts);
1041
+ }
1042
+ break;
1043
+ case constants.S_IFREG:
1044
+ case constants.S_IFLNK:
1045
+ await copyFile(source, destination);
1046
+ break;
1047
+ case constants.S_IFBLK:
1048
+ case constants.S_IFCHR:
1049
+ case constants.S_IFIFO:
1050
+ case constants.S_IFSOCK:
1051
+ default:
1052
+ throw new ApiError(ErrorCode.EPERM, 'File type not supported', source, 'rm');
1053
+ }
1054
+
1055
+ // Optionally preserve timestamps
1056
+ if (opts?.preserveTimestamps) {
1057
+ await utimes(destination, srcStats.atime, srcStats.mtime);
1058
+ }
1059
+ }
1060
+ cp satisfies typeof promises.cp;
1061
+
1062
+ /**
1063
+ * @since v18.15.0
1064
+ * @return Fulfills with an {fs.StatFs} for the file system.
1065
+ */
1066
+ export async function statfs(path: PathLike, opts?: Node.StatFsOptions & { bigint?: false }): Promise<StatsFs>;
1067
+ export async function statfs(path: PathLike, opts: Node.StatFsOptions & { bigint: true }): Promise<BigIntStatsFs>;
1068
+ export async function statfs(path: PathLike, opts?: Node.StatFsOptions): Promise<StatsFs | BigIntStatsFs>;
1069
+ export async function statfs(path: PathLike, opts?: Node.StatFsOptions): Promise<StatsFs | BigIntStatsFs> {
1070
+ throw ApiError.With('ENOSYS', path, 'statfs');
1071
+ }