@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,9 +2,11 @@ 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 { Dirent } from './dir.js';
5
+ import { normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
6
+ import { COPYFILE_EXCL, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK } from './constants.js';
7
+ import { Dir, Dirent } from './dir.js';
6
8
  import { dirname, join, parse } from './path.js';
7
- 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';
8
10
  function doOp(...[name, resolveSymlinks, path, ...args]) {
9
11
  path = normalizePath(path);
10
12
  const { fs, path: resolvedPath } = resolveMount(resolveSymlinks && existsSync(path) ? realpathSync(path) : path);
@@ -104,22 +106,22 @@ function _openSync(_path, _flag, _mode, resolveSymlinks) {
104
106
  // Ensure parent exists.
105
107
  const parentStats = doOp('statSync', resolveSymlinks, dirname(path), cred);
106
108
  if (!parentStats.isDirectory()) {
107
- throw ApiError.With('ENOTDIR', dirname(path), '_openSync');
109
+ throw ApiError.With('ENOTDIR', dirname(path), '_open');
108
110
  }
109
111
  return doOp('createFileSync', resolveSymlinks, path, flag, mode, cred);
110
112
  case ActionType.THROW:
111
- throw ApiError.With('ENOENT', path, '_openSync');
113
+ throw ApiError.With('ENOENT', path, '_open');
112
114
  default:
113
115
  throw new ApiError(ErrorCode.EINVAL, 'Invalid FileFlag object.');
114
116
  }
115
117
  }
116
118
  if (!stats.hasAccess(mode, cred)) {
117
- throw ApiError.With('EACCES', path, '_openSync');
119
+ throw ApiError.With('EACCES', path, '_open');
118
120
  }
119
121
  // File exists.
120
122
  switch (pathExistsAction(flag)) {
121
123
  case ActionType.THROW:
122
- throw ApiError.With('EEXIST', path, '_openSync');
124
+ throw ApiError.With('EEXIST', path, '_open');
123
125
  case ActionType.TRUNCATE:
124
126
  // Delete file.
125
127
  doOp('unlinkSync', resolveSymlinks, path, cred);
@@ -426,7 +428,7 @@ export function symlinkSync(target, path, type = 'file') {
426
428
  throw new ApiError(ErrorCode.EINVAL, 'Invalid type: ' + type);
427
429
  }
428
430
  if (existsSync(path)) {
429
- throw ApiError.With('EEXIST', path, 'symlinkSync');
431
+ throw ApiError.With('EEXIST', path, 'symlink');
430
432
  }
431
433
  writeFileSync(path, target);
432
434
  const file = _openSync(path, 'r+', 0o644, false);
@@ -542,54 +544,151 @@ export function accessSync(path, mode = 0o600) {
542
544
  }
543
545
  }
544
546
  accessSync;
545
- /* eslint-disable @typescript-eslint/no-unused-vars */
546
547
  /**
547
- * @todo Implement
548
+ * Synchronous `rm`. Removes files or directories (recursively).
549
+ * @param path The path to the file or directory to remove.
548
550
  */
549
- export function rmSync(path) {
550
- throw ApiError.With('ENOTSUP', path, 'rmSync');
551
+ export function rmSync(path, options) {
552
+ path = normalizePath(path);
553
+ const stats = statSync(path);
554
+ switch (stats.mode & S_IFMT) {
555
+ case S_IFDIR:
556
+ if (options?.recursive) {
557
+ for (const entry of readdirSync(path)) {
558
+ rmSync(join(path, entry));
559
+ }
560
+ }
561
+ rmdirSync(path);
562
+ return;
563
+ case S_IFREG:
564
+ case S_IFLNK:
565
+ unlinkSync(path);
566
+ return;
567
+ case S_IFBLK:
568
+ case S_IFCHR:
569
+ case S_IFIFO:
570
+ case S_IFSOCK:
571
+ default:
572
+ throw new ApiError(ErrorCode.EPERM, 'File type not supported', path, 'rm');
573
+ }
551
574
  }
552
575
  rmSync;
553
576
  export function mkdtempSync(prefix, options) {
554
- throw ApiError.With('ENOTSUP', prefix, 'mkdtempSync');
577
+ const encoding = typeof options === 'object' ? options.encoding : options || 'utf8';
578
+ const fsName = `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`;
579
+ const resolvedPath = '/tmp/' + fsName;
580
+ mkdirSync(resolvedPath);
581
+ return encoding == 'buffer' ? Buffer.from(resolvedPath) : resolvedPath;
555
582
  }
556
583
  mkdtempSync;
557
584
  /**
558
- * @todo Implement
585
+ * Synchronous `copyFile`. Copies a file.
586
+ * @param src The source file.
587
+ * @param dest The destination file.
588
+ * @param flags Optional flags for the copy operation. Currently supports these flags:
589
+ * * `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails.
559
590
  */
560
591
  export function copyFileSync(src, dest, flags) {
561
- throw ApiError.With('ENOTSUP', src, 'copyFileSync');
592
+ src = normalizePath(src);
593
+ dest = normalizePath(dest);
594
+ if (flags && flags & COPYFILE_EXCL && existsSync(dest)) {
595
+ throw new ApiError(ErrorCode.EEXIST, 'Destination file already exists.', dest, 'copyFile');
596
+ }
597
+ writeFileSync(dest, readFileSync(src));
562
598
  }
563
599
  copyFileSync;
564
600
  /**
565
- * @todo Implement
601
+ * Synchronous `readv`. Reads from a file descriptor into multiple buffers.
602
+ * @param fd The file descriptor.
603
+ * @param buffers An array of Uint8Array buffers.
604
+ * @param position The position in the file where to begin reading.
605
+ * @returns The number of bytes read.
566
606
  */
567
607
  export function readvSync(fd, buffers, position) {
568
- throw ApiError.With('ENOTSUP', fd2file(fd).path, 'readvSync');
608
+ const file = fd2file(fd);
609
+ let bytesRead = 0;
610
+ for (const buffer of buffers) {
611
+ bytesRead += file.readSync(buffer, 0, buffer.length, position + bytesRead);
612
+ }
613
+ return bytesRead;
569
614
  }
570
615
  readvSync;
571
616
  /**
572
- * @todo Implement
617
+ * Synchronous `writev`. Writes from multiple buffers into a file descriptor.
618
+ * @param fd The file descriptor.
619
+ * @param buffers An array of Uint8Array buffers.
620
+ * @param position The position in the file where to begin writing.
621
+ * @returns The number of bytes written.
573
622
  */
574
623
  export function writevSync(fd, buffers, position) {
575
- throw ApiError.With('ENOTSUP', fd2file(fd).path, 'writevSync');
624
+ const file = fd2file(fd);
625
+ let bytesWritten = 0;
626
+ for (const buffer of buffers) {
627
+ bytesWritten += file.writeSync(buffer, 0, buffer.length, position + bytesWritten);
628
+ }
629
+ return bytesWritten;
576
630
  }
577
631
  writevSync;
578
632
  /**
579
- * @todo Implement
633
+ * Synchronous `opendir`. Opens a directory.
634
+ * @param path The path to the directory.
635
+ * @param options Options for opening the directory.
636
+ * @returns A `Dir` object representing the opened directory.
580
637
  */
581
638
  export function opendirSync(path, options) {
582
- throw ApiError.With('ENOTSUP', path, 'opendirSync');
639
+ path = normalizePath(path);
640
+ return new Dir(path); // Re-use existing `Dir` class
583
641
  }
584
642
  opendirSync;
585
643
  /**
586
- * @todo Implement
644
+ * Synchronous `cp`. Recursively copies a file or directory.
645
+ * @param source The source file or directory.
646
+ * @param destination The destination file or directory.
647
+ * @param opts Options for the copy operation. Currently supports these options from Node.js 'fs.cpSync':
648
+ * * `dereference`: Dereference symbolic links.
649
+ * * `errorOnExist`: Throw an error if the destination file or directory already exists.
650
+ * * `filter`: A function that takes a source and destination path and returns a boolean, indicating whether to copy the given source element.
651
+ * * `force`: Overwrite the destination if it exists, and overwrite existing readonly destination files.
652
+ * * `preserveTimestamps`: Preserve file timestamps.
653
+ * * `recursive`: If `true`, copies directories recursively.
587
654
  */
588
655
  export function cpSync(source, destination, opts) {
589
- throw ApiError.With('ENOTSUP', source, 'cpSync');
656
+ source = normalizePath(source);
657
+ destination = normalizePath(destination);
658
+ const srcStats = lstatSync(source); // Use lstat to follow symlinks if not dereferencing
659
+ if (opts?.errorOnExist && existsSync(destination)) {
660
+ throw new ApiError(ErrorCode.EEXIST, 'Destination file or directory already exists.', destination, 'cp');
661
+ }
662
+ switch (srcStats.mode & S_IFMT) {
663
+ case S_IFDIR:
664
+ if (!opts?.recursive) {
665
+ throw new ApiError(ErrorCode.EISDIR, source + ' is a directory (not copied)', source, 'cp');
666
+ }
667
+ mkdirSync(destination, { recursive: true }); // Ensure the destination directory exists
668
+ for (const dirent of readdirSync(source, { withFileTypes: true })) {
669
+ if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) {
670
+ continue; // Skip if the filter returns false
671
+ }
672
+ cpSync(join(source, dirent.name), join(destination, dirent.name), opts);
673
+ }
674
+ break;
675
+ case S_IFREG:
676
+ case S_IFLNK:
677
+ copyFileSync(source, destination);
678
+ break;
679
+ case S_IFBLK:
680
+ case S_IFCHR:
681
+ case S_IFIFO:
682
+ case S_IFSOCK:
683
+ default:
684
+ throw new ApiError(ErrorCode.EPERM, 'File type not supported', source, 'rm');
685
+ }
686
+ // Optionally preserve timestamps
687
+ if (opts?.preserveTimestamps) {
688
+ utimesSync(destination, srcStats.atime, srcStats.mtime);
689
+ }
590
690
  }
591
691
  cpSync;
592
692
  export function statfsSync(path, options) {
593
- throw ApiError.With('ENOTSUP', path, 'statfsSync');
693
+ throw ApiError.With('ENOSYS', path, 'statfs');
594
694
  }
595
- /* eslint-enable @typescript-eslint/no-unused-vars */
@@ -220,5 +220,8 @@ declare abstract class ReadonlyFileSystem extends FileSystem {
220
220
  sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void>;
221
221
  syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
222
222
  }
223
+ /**
224
+ * Implements the non-readonly methods to throw `EROFS`
225
+ */
223
226
  export declare function Readonly<T extends abstract new (...args: any[]) => FileSystem>(FS: T): (abstract new (...args: any[]) => ReadonlyFileSystem) & T;
224
227
  export {};
@@ -24,7 +24,6 @@ export class FileSystem {
24
24
  freeSpace: 0,
25
25
  };
26
26
  }
27
- /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
28
27
  constructor(options) {
29
28
  // unused
30
29
  }
@@ -57,9 +56,6 @@ export class FileSystem {
57
56
  * Implements the asynchronous API in terms of the synchronous API.
58
57
  */
59
58
  export function Sync(FS) {
60
- /**
61
- * Implements the asynchronous API in terms of the synchronous API.
62
- */
63
59
  class _SyncFileSystem extends FS {
64
60
  async ready() {
65
61
  return this;
@@ -239,6 +235,9 @@ export function Async(FS) {
239
235
  }
240
236
  return _AsyncFileSystem;
241
237
  }
238
+ /**
239
+ * Implements the non-readonly methods to throw `EROFS`
240
+ */
242
241
  export function Readonly(FS) {
243
242
  class _ReadonlyFileSystem extends FS {
244
243
  metadata() {
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export * from './config.js';
9
9
  export * from './cred.js';
10
10
  export * from './file.js';
11
11
  export * from './filesystem.js';
12
- export * from './FileIndex.js';
12
+ export * from './backends/FileIndex.js';
13
13
  export * from './inode.js';
14
14
  export * from './mutex.js';
15
15
  export * from './stats.js';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ export * from './config.js';
9
9
  export * from './cred.js';
10
10
  export * from './file.js';
11
11
  export * from './filesystem.js';
12
- export * from './FileIndex.js';
12
+ export * from './backends/FileIndex.js';
13
13
  export * from './inode.js';
14
14
  export * from './mutex.js';
15
15
  export * from './stats.js';
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
1
3
  import type { OptionalTuple } from 'utilium';
2
4
  import { ApiError } from './ApiError.js';
3
5
  import { Cred } from './cred.js';
@@ -41,3 +43,46 @@ export declare function decodeDirListing(data: Uint8Array): Record<string, bigin
41
43
  */
42
44
  export declare function encodeDirListing(data: Record<string, bigint>): Uint8Array;
43
45
  export type Callback<Args extends unknown[] = []> = (e?: ApiError, ...args: OptionalTuple<Args>) => unknown;
46
+ import type { EncodingOption, OpenMode, WriteFileOptions } from 'node:fs';
47
+ /**
48
+ * converts Date or number to a integer UNIX timestamp
49
+ * Grabbed from NodeJS sources (lib/fs.js)
50
+ *
51
+ * @internal
52
+ */
53
+ export declare function _toUnixTimestamp(time: Date | number): number;
54
+ /**
55
+ * Normalizes a mode
56
+ * @internal
57
+ */
58
+ export declare function normalizeMode(mode: string | number | unknown, def?: number): number;
59
+ /**
60
+ * Normalizes a time
61
+ * @internal
62
+ */
63
+ export declare function normalizeTime(time: string | number | Date): Date;
64
+ /**
65
+ * Normalizes a path
66
+ * @internal
67
+ */
68
+ export declare function normalizePath(p: string): string;
69
+ /**
70
+ * Normalizes options
71
+ * @param options options to normalize
72
+ * @param encoding default encoding
73
+ * @param flag default flag
74
+ * @param mode default mode
75
+ * @internal
76
+ */
77
+ export declare function normalizeOptions(options?: WriteFileOptions | (EncodingOption & {
78
+ flag?: OpenMode;
79
+ }), encoding?: BufferEncoding, flag?: string, mode?: number): {
80
+ encoding: BufferEncoding;
81
+ flag: string;
82
+ mode: number;
83
+ };
84
+ /**
85
+ * Do nothing
86
+ * @internal
87
+ */
88
+ export declare function nop(): void;
package/dist/utils.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ApiError, ErrorCode } from './ApiError.js';
2
- import { dirname } from './emulation/path.js';
2
+ import { dirname, resolve } from './emulation/path.js';
3
3
  /**
4
4
  * Synchronous recursive makedir.
5
5
  * @hidden
@@ -133,3 +133,96 @@ export function encodeDirListing(data) {
133
133
  return v.toString();
134
134
  }));
135
135
  }
136
+ /**
137
+ * converts Date or number to a integer UNIX timestamp
138
+ * Grabbed from NodeJS sources (lib/fs.js)
139
+ *
140
+ * @internal
141
+ */
142
+ export function _toUnixTimestamp(time) {
143
+ if (typeof time === 'number') {
144
+ return Math.floor(time);
145
+ }
146
+ if (time instanceof Date) {
147
+ return Math.floor(time.getTime() / 1000);
148
+ }
149
+ throw new Error('Cannot parse time: ' + time);
150
+ }
151
+ /**
152
+ * Normalizes a mode
153
+ * @internal
154
+ */
155
+ export function normalizeMode(mode, def) {
156
+ if (typeof mode == 'number') {
157
+ return mode;
158
+ }
159
+ if (typeof mode == 'string') {
160
+ const parsed = parseInt(mode, 8);
161
+ if (!isNaN(parsed)) {
162
+ return parsed;
163
+ }
164
+ }
165
+ if (typeof def == 'number') {
166
+ return def;
167
+ }
168
+ throw new ApiError(ErrorCode.EINVAL, 'Invalid mode: ' + mode?.toString());
169
+ }
170
+ /**
171
+ * Normalizes a time
172
+ * @internal
173
+ */
174
+ export function normalizeTime(time) {
175
+ if (time instanceof Date) {
176
+ return time;
177
+ }
178
+ if (typeof time == 'number') {
179
+ return new Date(time * 1000);
180
+ }
181
+ if (typeof time == 'string') {
182
+ return new Date(time);
183
+ }
184
+ throw new ApiError(ErrorCode.EINVAL, 'Invalid time.');
185
+ }
186
+ /**
187
+ * Normalizes a path
188
+ * @internal
189
+ */
190
+ export function normalizePath(p) {
191
+ // Node doesn't allow null characters in paths.
192
+ if (p.includes('\x00')) {
193
+ throw new ApiError(ErrorCode.EINVAL, 'Path must be a string without null bytes.');
194
+ }
195
+ if (p.length == 0) {
196
+ throw new ApiError(ErrorCode.EINVAL, 'Path must not be empty.');
197
+ }
198
+ return resolve(p.replaceAll(/[/\\]+/g, '/'));
199
+ }
200
+ /**
201
+ * Normalizes options
202
+ * @param options options to normalize
203
+ * @param encoding default encoding
204
+ * @param flag default flag
205
+ * @param mode default mode
206
+ * @internal
207
+ */
208
+ export function normalizeOptions(options, encoding = 'utf8', flag, mode = 0) {
209
+ if (typeof options != 'object' || options === null) {
210
+ return {
211
+ encoding: typeof options == 'string' ? options : encoding,
212
+ flag,
213
+ mode,
214
+ };
215
+ }
216
+ return {
217
+ encoding: typeof options?.encoding == 'string' ? options.encoding : encoding,
218
+ flag: typeof options?.flag == 'string' ? options.flag : flag,
219
+ mode: normalizeMode('mode' in options ? options?.mode : null, mode),
220
+ };
221
+ }
222
+ /**
223
+ * Do nothing
224
+ * @internal
225
+ */
226
+ export function nop() {
227
+ // do nothing
228
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A filesystem in your browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist",