@zenfs/core 0.8.0 → 0.9.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.
@@ -2,10 +2,12 @@ import { Buffer } from 'buffer';
2
2
  import { ApiError, ErrorCode } from '../ApiError.js';
3
3
  import { ActionType, isAppendable, isReadable, isWriteable, parseFlag, pathExistsAction, pathNotExistsAction } from '../file.js';
4
4
  import { BigIntStats, FileType } from '../stats.js';
5
- import { F_OK } from './constants.js';
6
- import { Dirent } from './dir.js';
5
+ import { normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
6
+ import * as constants from './constants.js';
7
+ import { Dir, Dirent } from './dir.js';
7
8
  import { dirname, join, parse } from './path.js';
8
- import { cred, fd2file, fdMap, fixError, getFdForFile, mounts, normalizeMode, normalizeOptions, normalizePath, normalizeTime, resolveMount } from './shared.js';
9
+ import { cred, fd2file, fdMap, fixError, getFdForFile, mounts, resolveMount } from './shared.js';
10
+ import { ReadStream, WriteStream } from './streams.js';
9
11
  export * as constants from './constants.js';
10
12
  export class FileHandle {
11
13
  constructor(
@@ -15,12 +17,12 @@ export class FileHandle {
15
17
  fd) {
16
18
  this.fd = fd;
17
19
  }
20
+ /**
21
+ * @internal
22
+ */
18
23
  get file() {
19
24
  return fd2file(this.fd);
20
25
  }
21
- get path() {
22
- return this.file.path;
23
- }
24
26
  /**
25
27
  * Asynchronous fchown(2) - Change ownership of a file.
26
28
  */
@@ -128,13 +130,35 @@ export class FileHandle {
128
130
  * @experimental
129
131
  */
130
132
  readableWebStream(options) {
131
- throw ApiError.With('ENOTSUP', this.path, 'FileHandle.readableWebStream');
133
+ // Note: using an arrow function to preserve `this`
134
+ const start = async ({ close, enqueue, error }) => {
135
+ try {
136
+ const chunkSize = 64 * 1024, maxChunks = 1e7;
137
+ let i = 0, position = 0, result;
138
+ while (result.bytesRead > 0) {
139
+ result = await this.read(new Uint8Array(chunkSize), 0, chunkSize, position);
140
+ if (!result.bytesRead) {
141
+ close();
142
+ return;
143
+ }
144
+ enqueue(result.buffer.slice(0, result.bytesRead));
145
+ position += result.bytesRead;
146
+ if (++i >= maxChunks) {
147
+ throw new ApiError(ErrorCode.EFBIG, 'Too many iterations on readable stream', this.file.path, 'FileHandle.readableWebStream');
148
+ }
149
+ }
150
+ }
151
+ catch (e) {
152
+ error(e);
153
+ }
154
+ };
155
+ return new ReadableStream({ start, type: options.type });
132
156
  }
133
157
  readLines(options) {
134
- throw ApiError.With('ENOTSUP', this.path, 'FileHandle.readLines');
158
+ throw ApiError.With('ENOSYS', this.file.path, 'FileHandle.readLines');
135
159
  }
136
160
  [Symbol.asyncDispose]() {
137
- throw ApiError.With('ENOTSUP', this.path, 'FileHandle.@@asyncDispose');
161
+ return this.close();
138
162
  }
139
163
  async stat(opts) {
140
164
  const stats = await this.file.stat();
@@ -191,26 +215,80 @@ export class FileHandle {
191
215
  await this.file.close();
192
216
  fdMap.delete(this.fd);
193
217
  }
194
- /* eslint-disable @typescript-eslint/no-unused-vars */
195
218
  /**
196
- * See `fs.writev` promisified version.
197
- * @todo Implement
219
+ * Asynchronous `writev`. Writes from multiple buffers.
220
+ * @param buffers An array of Uint8Array buffers.
221
+ * @param position The position in the file where to begin writing.
222
+ * @returns The number of bytes written.
198
223
  */
199
- writev(buffers, position) {
200
- throw ApiError.With('ENOTSUP', this.path, 'FileHandle.writev');
224
+ async writev(buffers, position) {
225
+ let bytesWritten = 0;
226
+ for (const buffer of buffers) {
227
+ bytesWritten += (await this.write(buffer, 0, buffer.length, position + bytesWritten)).bytesWritten;
228
+ }
229
+ return { bytesWritten, buffers };
201
230
  }
202
231
  /**
203
- * See `fs.readv` promisified version.
204
- * @todo Implement
232
+ * Asynchronous `readv`. Reads into multiple buffers.
233
+ * @param buffers An array of Uint8Array buffers.
234
+ * @param position The position in the file where to begin reading.
235
+ * @returns The number of bytes read.
205
236
  */
206
- readv(buffers, position) {
207
- throw ApiError.With('ENOTSUP', this.path, 'FileHandle.readv');
237
+ async readv(buffers, position) {
238
+ let bytesRead = 0;
239
+ for (const buffer of buffers) {
240
+ bytesRead += (await this.read(buffer, 0, buffer.byteLength, position + bytesRead)).bytesRead;
241
+ }
242
+ return { bytesRead, buffers };
208
243
  }
244
+ /**
245
+ * Creates a `ReadStream` for reading from the file.
246
+ *
247
+ * @param options Options for the readable stream
248
+ * @returns A `ReadStream` object.
249
+ */
209
250
  createReadStream(options) {
210
- throw ApiError.With('ENOTSUP', this.path, 'createReadStream');
251
+ const streamOptions = {
252
+ highWaterMark: options?.highWaterMark || 64 * 1024,
253
+ encoding: options?.encoding,
254
+ read: async (size) => {
255
+ try {
256
+ const result = await this.read(new Uint8Array(size), 0, size, this.file.position);
257
+ stream.push(!result.bytesRead ? null : result.buffer.slice(0, result.bytesRead)); // Push data or null for EOF
258
+ this.file.position += result.bytesRead;
259
+ }
260
+ catch (error) {
261
+ stream.destroy(error);
262
+ }
263
+ },
264
+ };
265
+ const stream = new ReadStream(streamOptions);
266
+ stream.path = this.file.path;
267
+ return stream;
211
268
  }
269
+ /**
270
+ * Creates a `WriteStream` for writing to the file.
271
+ *
272
+ * @param options Options for the writeable stream.
273
+ * @returns A `WriteStream` object
274
+ */
212
275
  createWriteStream(options) {
213
- throw ApiError.With('ENOTSUP', this.path, 'createWriteStream');
276
+ const streamOptions = {
277
+ highWaterMark: options?.highWaterMark,
278
+ encoding: options?.encoding,
279
+ write: async (chunk, encoding, callback) => {
280
+ try {
281
+ const { bytesWritten } = await this.write(chunk, null, encoding);
282
+ callback(bytesWritten == chunk.length ? null : new Error('Failed to write full chunk'));
283
+ }
284
+ catch (error) {
285
+ callback(error);
286
+ }
287
+ },
288
+ };
289
+ const stream = new WriteStream(streamOptions);
290
+ stream.path = this.file.path;
291
+ return stream;
214
292
  }
215
293
  }
216
294
  /**
@@ -405,7 +483,7 @@ export async function readFile(filename, _options) {
405
483
  }
406
484
  readFile;
407
485
  /**
408
- * Synchronously writes data to a file, replacing the file if it already exists.
486
+ * Asynchronously writes data to a file, replacing the file if it already exists.
409
487
  *
410
488
  * The encoding option is ignored if data is a buffer.
411
489
  * @param filename
@@ -647,7 +725,7 @@ export async function realpath(path, options) {
647
725
  }
648
726
  realpath;
649
727
  export function watch(filename, options) {
650
- throw ApiError.With('ENOTSUP', filename, 'watch');
728
+ throw ApiError.With('ENOSYS', filename, 'watch');
651
729
  }
652
730
  watch;
653
731
  /**
@@ -655,44 +733,133 @@ watch;
655
733
  * @param path
656
734
  * @param mode
657
735
  */
658
- export async function access(path, mode = F_OK) {
736
+ export async function access(path, mode = constants.F_OK) {
659
737
  const stats = await stat(path);
660
738
  if (!stats.hasAccess(mode, cred)) {
661
739
  throw new ApiError(ErrorCode.EACCES);
662
740
  }
663
741
  }
664
742
  access;
665
- /* eslint-disable @typescript-eslint/no-unused-vars */
666
743
  /**
667
- * @todo Implement
744
+ * Asynchronous `rm`. Removes files or directories (recursively).
745
+ * @param path The path to the file or directory to remove.
668
746
  */
669
747
  export async function rm(path, options) {
670
- throw ApiError.With('ENOTSUP', path, 'rm');
748
+ path = normalizePath(path);
749
+ const stats = await stat(path);
750
+ switch (stats.mode & constants.S_IFMT) {
751
+ case constants.S_IFDIR:
752
+ if (options?.recursive) {
753
+ for (const entry of await readdir(path)) {
754
+ await rm(join(path, entry));
755
+ }
756
+ }
757
+ await rmdir(path);
758
+ return;
759
+ case constants.S_IFREG:
760
+ case constants.S_IFLNK:
761
+ await unlink(path);
762
+ return;
763
+ case constants.S_IFBLK:
764
+ case constants.S_IFCHR:
765
+ case constants.S_IFIFO:
766
+ case constants.S_IFSOCK:
767
+ default:
768
+ throw new ApiError(ErrorCode.EPERM, 'File type not supported', path, 'rm');
769
+ }
671
770
  }
672
771
  rm;
673
772
  export async function mkdtemp(prefix, options) {
674
- throw ApiError.With('ENOTSUP', prefix, 'mkdtemp');
773
+ const encoding = typeof options === 'object' ? options.encoding : options || 'utf8';
774
+ const fsName = `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`;
775
+ const resolvedPath = '/tmp/' + fsName;
776
+ await mkdir(resolvedPath);
777
+ return encoding == 'buffer' ? Buffer.from(resolvedPath) : resolvedPath;
675
778
  }
676
779
  mkdtemp;
677
780
  /**
678
- * @todo Implement
781
+ * Asynchronous `copyFile`. Copies a file.
782
+ * @param src The source file.
783
+ * @param dest The destination file.
784
+ * @param mode Optional flags for the copy operation. Currently supports these flags:
785
+ * * `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails.
679
786
  */
680
787
  export async function copyFile(src, dest, mode) {
681
- throw ApiError.With('ENOTSUP', src, 'copyFile');
788
+ src = normalizePath(src);
789
+ dest = normalizePath(dest);
790
+ if (mode && mode & constants.COPYFILE_EXCL && (await exists(dest))) {
791
+ throw new ApiError(ErrorCode.EEXIST, 'Destination file already exists.', dest, 'copyFile');
792
+ }
793
+ await writeFile(dest, await readFile(src));
682
794
  }
683
795
  copyFile;
684
796
  /**
685
- * @todo Implement
797
+ * Asynchronous `opendir`. Opens a directory.
798
+ * @param path The path to the directory.
799
+ * @param options Options for opening the directory.
800
+ * @returns A `Dir` object representing the opened directory.
686
801
  */
687
802
  export async function opendir(path, options) {
688
- throw ApiError.With('ENOTSUP', path, 'opendir');
803
+ path = normalizePath(path);
804
+ return new Dir(path);
689
805
  }
690
806
  opendir;
807
+ /**
808
+ * Asynchronous `cp`. Recursively copies a file or directory.
809
+ * @param source The source file or directory.
810
+ * @param destination The destination file or directory.
811
+ * @param opts Options for the copy operation. Currently supports these options from Node.js 'fs.await cp':
812
+ * * `dereference`: Dereference symbolic links.
813
+ * * `errorOnExist`: Throw an error if the destination file or directory already exists.
814
+ * * `filter`: A function that takes a source and destination path and returns a boolean, indicating whether to copy the given source element.
815
+ * * `force`: Overwrite the destination if it exists, and overwrite existing readonly destination files.
816
+ * * `preserveTimestamps`: Preserve file timestamps.
817
+ * * `recursive`: If `true`, copies directories recursively.
818
+ */
691
819
  export async function cp(source, destination, opts) {
692
- throw ApiError.With('ENOTSUP', source, 'cp');
820
+ source = normalizePath(source);
821
+ destination = normalizePath(destination);
822
+ const srcStats = await lstat(source); // Use lstat to follow symlinks if not dereferencing
823
+ if (opts?.errorOnExist && (await exists(destination))) {
824
+ throw new ApiError(ErrorCode.EEXIST, 'Destination file or directory already exists.', destination, 'cp');
825
+ }
826
+ switch (srcStats.mode & constants.S_IFMT) {
827
+ case constants.S_IFDIR:
828
+ if (!opts?.recursive) {
829
+ throw new ApiError(ErrorCode.EISDIR, source + ' is a directory (not copied)', source, 'cp');
830
+ }
831
+ await mkdir(destination, { recursive: true }); // Ensure the destination directory exists
832
+ for (const dirent of await readdir(source, { withFileTypes: true })) {
833
+ if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) {
834
+ continue; // Skip if the filter returns false
835
+ }
836
+ await cp(join(source, dirent.name), join(destination, dirent.name), opts);
837
+ }
838
+ break;
839
+ case constants.S_IFREG:
840
+ case constants.S_IFLNK:
841
+ await copyFile(source, destination);
842
+ break;
843
+ case constants.S_IFBLK:
844
+ case constants.S_IFCHR:
845
+ case constants.S_IFIFO:
846
+ case constants.S_IFSOCK:
847
+ default:
848
+ throw new ApiError(ErrorCode.EPERM, 'File type not supported', source, 'rm');
849
+ }
850
+ // Optionally preserve timestamps
851
+ if (opts?.preserveTimestamps) {
852
+ await utimes(destination, srcStats.atime, srcStats.mtime);
853
+ }
693
854
  }
694
855
  cp;
695
856
  export async function statfs(path, opts) {
696
- throw ApiError.With('ENOTSUP', path, 'statfs');
857
+ throw ApiError.With('ENOSYS', path, 'statfs');
858
+ }
859
+ export async function openAsBlob(path, options) {
860
+ const handle = await open(path, 'r');
861
+ const buffer = await handle.readFile();
862
+ await handle.close();
863
+ return new Blob([buffer], options);
697
864
  }
698
- /* eslint-enable @typescript-eslint/no-unused-vars */
865
+ openAsBlob;
@@ -1,51 +1,6 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
1
  import { Cred } from '../cred.js';
4
- import { FileSystem } from '../filesystem.js';
5
2
  import type { File } from '../file.js';
6
- import type { EncodingOption, OpenMode, WriteFileOptions } from 'node:fs';
7
- /**
8
- * converts Date or number to a integer UNIX timestamp
9
- * Grabbed from NodeJS sources (lib/fs.js)
10
- *
11
- * @internal
12
- */
13
- export declare function _toUnixTimestamp(time: Date | number): number;
14
- /**
15
- * Normalizes a mode
16
- * @internal
17
- */
18
- export declare function normalizeMode(mode: string | number | unknown, def?: number): number;
19
- /**
20
- * Normalizes a time
21
- * @internal
22
- */
23
- export declare function normalizeTime(time: string | number | Date): Date;
24
- /**
25
- * Normalizes a path
26
- * @internal
27
- */
28
- export declare function normalizePath(p: string): string;
29
- /**
30
- * Normalizes options
31
- * @param options options to normalize
32
- * @param encoding default encoding
33
- * @param flag default flag
34
- * @param mode default mode
35
- * @internal
36
- */
37
- export declare function normalizeOptions(options?: WriteFileOptions | (EncodingOption & {
38
- flag?: OpenMode;
39
- }), encoding?: BufferEncoding, flag?: string, mode?: number): {
40
- encoding: BufferEncoding;
41
- flag: string;
42
- mode: number;
43
- };
44
- /**
45
- * Do nothing
46
- * @internal
47
- */
48
- export declare function nop(): void;
3
+ import { FileSystem } from '../filesystem.js';
49
4
  export declare let cred: Cred;
50
5
  export declare function setCred(val: Cred): void;
51
6
  export declare const fdMap: Map<number, File>;
@@ -1,101 +1,9 @@
1
1
  // Utilities and shared data
2
- import { resolve } from './path.js';
3
2
  import { ApiError, ErrorCode } from '../ApiError.js';
4
- import { rootCred } from '../cred.js';
5
3
  import { InMemory } from '../backends/InMemory.js';
6
- /**
7
- * converts Date or number to a integer UNIX timestamp
8
- * Grabbed from NodeJS sources (lib/fs.js)
9
- *
10
- * @internal
11
- */
12
- export function _toUnixTimestamp(time) {
13
- if (typeof time === 'number') {
14
- return Math.floor(time);
15
- }
16
- if (time instanceof Date) {
17
- return Math.floor(time.getTime() / 1000);
18
- }
19
- throw new Error('Cannot parse time: ' + time);
20
- }
21
- /**
22
- * Normalizes a mode
23
- * @internal
24
- */
25
- export function normalizeMode(mode, def) {
26
- if (typeof mode == 'number') {
27
- return mode;
28
- }
29
- if (typeof mode == 'string') {
30
- const parsed = parseInt(mode, 8);
31
- if (!isNaN(parsed)) {
32
- return parsed;
33
- }
34
- }
35
- if (typeof def == 'number') {
36
- return def;
37
- }
38
- throw new ApiError(ErrorCode.EINVAL, 'Invalid mode: ' + mode?.toString());
39
- }
40
- /**
41
- * Normalizes a time
42
- * @internal
43
- */
44
- export function normalizeTime(time) {
45
- if (time instanceof Date) {
46
- return time;
47
- }
48
- if (typeof time == 'number') {
49
- return new Date(time * 1000);
50
- }
51
- if (typeof time == 'string') {
52
- return new Date(time);
53
- }
54
- throw new ApiError(ErrorCode.EINVAL, 'Invalid time.');
55
- }
56
- /**
57
- * Normalizes a path
58
- * @internal
59
- */
60
- export function normalizePath(p) {
61
- // Node doesn't allow null characters in paths.
62
- if (p.includes('\x00')) {
63
- throw new ApiError(ErrorCode.EINVAL, 'Path must be a string without null bytes.');
64
- }
65
- if (p.length == 0) {
66
- throw new ApiError(ErrorCode.EINVAL, 'Path must not be empty.');
67
- }
68
- return resolve(p.replaceAll(/[/\\]+/g, '/'));
69
- }
70
- /**
71
- * Normalizes options
72
- * @param options options to normalize
73
- * @param encoding default encoding
74
- * @param flag default flag
75
- * @param mode default mode
76
- * @internal
77
- */
78
- export function normalizeOptions(options, encoding = 'utf8', flag, mode = 0) {
79
- if (typeof options != 'object' || options === null) {
80
- return {
81
- encoding: typeof options == 'string' ? options : encoding,
82
- flag,
83
- mode,
84
- };
85
- }
86
- return {
87
- encoding: typeof options?.encoding == 'string' ? options.encoding : encoding,
88
- flag: typeof options?.flag == 'string' ? options.flag : flag,
89
- mode: normalizeMode('mode' in options ? options?.mode : null, mode),
90
- };
91
- }
92
- /**
93
- * Do nothing
94
- * @internal
95
- */
96
- export function nop() {
97
- // do nothing
98
- }
4
+ import { rootCred } from '../cred.js';
5
+ import { normalizePath } from '../utils.js';
6
+ import { resolve } from './path.js';
99
7
  // credentials
100
8
  export let cred = rootCred;
101
9
  export function setCred(val) {
@@ -297,36 +297,64 @@ export declare function realpathSync(path: PathLike, options?: EncodingOption):
297
297
  */
298
298
  export declare function accessSync(path: PathLike, mode?: number): void;
299
299
  /**
300
- * @todo Implement
300
+ * Synchronous `rm`. Removes files or directories (recursively).
301
+ * @param path The path to the file or directory to remove.
301
302
  */
302
- export declare function rmSync(path: PathLike): void;
303
+ export declare function rmSync(path: PathLike, options?: Node.RmOptions): void;
303
304
  /**
304
- * @todo Implement
305
+ * Synchronous `mkdtemp`. Creates a unique temporary directory.
306
+ * @param prefix The directory prefix.
307
+ * @param options The encoding (or an object including `encoding`).
308
+ * @returns The path to the created temporary directory, encoded as a string or buffer.
305
309
  */
306
310
  export declare function mkdtempSync(prefix: string, options: BufferEncodingOption): Buffer;
307
311
  export declare function mkdtempSync(prefix: string, options?: EncodingOption): string;
308
312
  /**
309
- * @todo Implement
313
+ * Synchronous `copyFile`. Copies a file.
314
+ * @param src The source file.
315
+ * @param dest The destination file.
316
+ * @param flags Optional flags for the copy operation. Currently supports these flags:
317
+ * * `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails.
310
318
  */
311
- export declare function copyFileSync(src: string, dest: string, flags?: number): void;
319
+ export declare function copyFileSync(src: PathLike, dest: PathLike, flags?: number): void;
312
320
  /**
313
- * @todo Implement
321
+ * Synchronous `readv`. Reads from a file descriptor into multiple buffers.
322
+ * @param fd The file descriptor.
323
+ * @param buffers An array of Uint8Array buffers.
324
+ * @param position The position in the file where to begin reading.
325
+ * @returns The number of bytes read.
314
326
  */
315
327
  export declare function readvSync(fd: number, buffers: readonly Uint8Array[], position?: number): number;
316
328
  /**
317
- * @todo Implement
329
+ * Synchronous `writev`. Writes from multiple buffers into a file descriptor.
330
+ * @param fd The file descriptor.
331
+ * @param buffers An array of Uint8Array buffers.
332
+ * @param position The position in the file where to begin writing.
333
+ * @returns The number of bytes written.
318
334
  */
319
335
  export declare function writevSync(fd: number, buffers: readonly Uint8Array[], position?: number): number;
320
336
  /**
321
- * @todo Implement
337
+ * Synchronous `opendir`. Opens a directory.
338
+ * @param path The path to the directory.
339
+ * @param options Options for opening the directory.
340
+ * @returns A `Dir` object representing the opened directory.
322
341
  */
323
342
  export declare function opendirSync(path: PathLike, options?: Node.OpenDirOptions): Dir;
324
343
  /**
325
- * @todo Implement
344
+ * Synchronous `cp`. Recursively copies a file or directory.
345
+ * @param source The source file or directory.
346
+ * @param destination The destination file or directory.
347
+ * @param opts Options for the copy operation. Currently supports these options from Node.js 'fs.cpSync':
348
+ * * `dereference`: Dereference symbolic links.
349
+ * * `errorOnExist`: Throw an error if the destination file or directory already exists.
350
+ * * `filter`: A function that takes a source and destination path and returns a boolean, indicating whether to copy the given source element.
351
+ * * `force`: Overwrite the destination if it exists, and overwrite existing readonly destination files.
352
+ * * `preserveTimestamps`: Preserve file timestamps.
353
+ * * `recursive`: If `true`, copies directories recursively.
326
354
  */
327
355
  export declare function cpSync(source: PathLike, destination: PathLike, opts?: Node.CopySyncOptions): void;
328
356
  /**
329
- * Synchronous statfs(2). Returns information about the mounted file system which contains path. The callback gets two arguments (err, stats) where stats is an <fs.StatFs> object.
357
+ * Synchronous statfs(2). Returns information about the mounted file system which contains path.
330
358
  * In case of an error, the err.code will be one of Common System Errors.
331
359
  * @param path A path to an existing file or directory on the file system to be queried.
332
360
  * @param callback