@zenfs/core 1.1.6 → 1.2.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 (58) hide show
  1. package/dist/backends/file_index.js +0 -3
  2. package/dist/backends/overlay.js +0 -8
  3. package/dist/backends/store/fs.js +4 -17
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +4 -0
  6. package/dist/devices.js +0 -12
  7. package/dist/emulation/cache.d.ts +21 -0
  8. package/dist/emulation/cache.js +36 -0
  9. package/dist/emulation/promises.d.ts +9 -14
  10. package/dist/emulation/promises.js +71 -48
  11. package/dist/emulation/shared.d.ts +22 -0
  12. package/dist/emulation/shared.js +6 -0
  13. package/dist/emulation/sync.d.ts +11 -20
  14. package/dist/emulation/sync.js +44 -23
  15. package/package.json +4 -2
  16. package/scripts/test.js +14 -1
  17. package/src/backends/backend.ts +160 -0
  18. package/src/backends/fetch.ts +180 -0
  19. package/src/backends/file_index.ts +206 -0
  20. package/src/backends/memory.ts +50 -0
  21. package/src/backends/overlay.ts +560 -0
  22. package/src/backends/port/fs.ts +335 -0
  23. package/src/backends/port/readme.md +54 -0
  24. package/src/backends/port/rpc.ts +167 -0
  25. package/src/backends/readme.md +3 -0
  26. package/src/backends/store/fs.ts +700 -0
  27. package/src/backends/store/readme.md +9 -0
  28. package/src/backends/store/simple.ts +146 -0
  29. package/src/backends/store/store.ts +173 -0
  30. package/src/config.ts +173 -0
  31. package/src/credentials.ts +31 -0
  32. package/src/devices.ts +459 -0
  33. package/src/emulation/async.ts +834 -0
  34. package/src/emulation/cache.ts +44 -0
  35. package/src/emulation/constants.ts +182 -0
  36. package/src/emulation/dir.ts +138 -0
  37. package/src/emulation/index.ts +8 -0
  38. package/src/emulation/path.ts +440 -0
  39. package/src/emulation/promises.ts +1133 -0
  40. package/src/emulation/shared.ts +160 -0
  41. package/src/emulation/streams.ts +34 -0
  42. package/src/emulation/sync.ts +867 -0
  43. package/src/emulation/watchers.ts +193 -0
  44. package/src/error.ts +307 -0
  45. package/src/file.ts +661 -0
  46. package/src/filesystem.ts +174 -0
  47. package/src/index.ts +25 -0
  48. package/src/inode.ts +132 -0
  49. package/src/mixins/async.ts +208 -0
  50. package/src/mixins/index.ts +5 -0
  51. package/src/mixins/mutexed.ts +257 -0
  52. package/src/mixins/readonly.ts +96 -0
  53. package/src/mixins/shared.ts +25 -0
  54. package/src/mixins/sync.ts +58 -0
  55. package/src/polyfills.ts +21 -0
  56. package/src/stats.ts +363 -0
  57. package/src/utils.ts +288 -0
  58. package/tests/fs/readdir.test.ts +3 -3
@@ -0,0 +1,174 @@
1
+ import type { ErrnoError } from './error.js';
2
+ import type { File } from './file.js';
3
+ import { ZenFsType, type Stats } from './stats.js';
4
+
5
+ export type FileContents = ArrayBufferView | string;
6
+
7
+ /**
8
+ * Metadata about a FileSystem
9
+ */
10
+ export interface FileSystemMetadata {
11
+ /**
12
+ * The name of the FS
13
+ */
14
+ name: string;
15
+
16
+ /**
17
+ * Whether the FS is readonly or not
18
+ */
19
+ readonly: boolean;
20
+
21
+ /**
22
+ * The total space
23
+ */
24
+ totalSpace: number;
25
+
26
+ /**
27
+ * The available space
28
+ */
29
+ freeSpace: number;
30
+
31
+ /**
32
+ * If set, disables File from using a resizable array buffer.
33
+ * @default false
34
+ */
35
+ noResizableBuffers: boolean;
36
+
37
+ /**
38
+ * If set, disables caching on async file systems.
39
+ * This means *sync operations will not work*.
40
+ * It has no affect on sync file systems.
41
+ * @default false
42
+ */
43
+ noAsyncCache: boolean;
44
+
45
+ /**
46
+ * The optimal block size to use with the file system
47
+ * @default 4096
48
+ */
49
+ blockSize?: number;
50
+
51
+ /**
52
+ * Total number of (file) nodes available
53
+ */
54
+ totalNodes?: number;
55
+
56
+ /**
57
+ * Number of free (file) nodes available
58
+ */
59
+ freeNodes?: number;
60
+
61
+ /**
62
+ * The type of the FS
63
+ */
64
+ type: number;
65
+ }
66
+
67
+ /**
68
+ * Provides a consistent and easy to use internal API.
69
+ * Default implementations for `exists` and `existsSync` are included.
70
+ * If you are extending this class, note that every path is an absolute path and all arguments are present.
71
+ * @internal
72
+ */
73
+ export abstract class FileSystem {
74
+ /**
75
+ * Get metadata about the current file system
76
+ */
77
+ public metadata(): FileSystemMetadata {
78
+ return {
79
+ name: this.constructor.name.toLowerCase(),
80
+ readonly: false,
81
+ totalSpace: 0,
82
+ freeSpace: 0,
83
+ noResizableBuffers: false,
84
+ noAsyncCache: this._disableSync ?? false,
85
+ type: ZenFsType,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Whether the sync cache should be disabled.
91
+ * Only affects async things.
92
+ * @internal @protected
93
+ */
94
+ _disableSync?: boolean;
95
+
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
97
+ public constructor(...args: any[]) {}
98
+
99
+ public async ready(): Promise<void> {}
100
+
101
+ public abstract rename(oldPath: string, newPath: string): Promise<void>;
102
+ public abstract renameSync(oldPath: string, newPath: string): void;
103
+
104
+ public abstract stat(path: string): Promise<Stats>;
105
+ public abstract statSync(path: string): Stats;
106
+
107
+ /**
108
+ * Opens the file at `path` with `flag`. The file must exist.
109
+ * @param path The path to open.
110
+ * @param flag The flag to use when opening the file.
111
+ */
112
+ public abstract openFile(path: string, flag: string): Promise<File>;
113
+
114
+ /**
115
+ * Opens the file at `path` with `flag`. The file must exist.
116
+ * @param path The path to open.
117
+ * @param flag The flag to use when opening the file.
118
+ */
119
+ public abstract openFileSync(path: string, flag: string): File;
120
+
121
+ /**
122
+ * Create the file at `path` with `mode`. Then, open it with `flag`.
123
+ */
124
+ public abstract createFile(path: string, flag: string, mode: number): Promise<File>;
125
+
126
+ /**
127
+ * Create the file at `path` with `mode`. Then, open it with `flag`.
128
+ */
129
+ public abstract createFileSync(path: string, flag: string, mode: number): File;
130
+
131
+ public abstract unlink(path: string): Promise<void>;
132
+ public abstract unlinkSync(path: string): void;
133
+
134
+ // Directory operations
135
+
136
+ public abstract rmdir(path: string): Promise<void>;
137
+ public abstract rmdirSync(path: string): void;
138
+
139
+ public abstract mkdir(path: string, mode: number): Promise<void>;
140
+ public abstract mkdirSync(path: string, mode: number): void;
141
+
142
+ public abstract readdir(path: string): Promise<string[]>;
143
+ public abstract readdirSync(path: string): string[];
144
+
145
+ /**
146
+ * Test whether or not `path` exists.
147
+ */
148
+ public async exists(path: string): Promise<boolean> {
149
+ try {
150
+ await this.stat(path);
151
+ return true;
152
+ } catch (e) {
153
+ return (e as ErrnoError).code != 'ENOENT';
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Test whether or not `path` exists.
159
+ */
160
+ public existsSync(path: string): boolean {
161
+ try {
162
+ this.statSync(path);
163
+ return true;
164
+ } catch (e) {
165
+ return (e as ErrnoError).code != 'ENOENT';
166
+ }
167
+ }
168
+
169
+ public abstract link(target: string, link: string): Promise<void>;
170
+ public abstract linkSync(target: string, link: string): void;
171
+
172
+ public abstract sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void>;
173
+ public abstract syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
174
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export * from './error.js';
2
+ export * from './backends/port/fs.js';
3
+ export * from './backends/fetch.js';
4
+ export * from './backends/memory.js';
5
+ export * from './backends/file_index.js';
6
+ export * from './backends/overlay.js';
7
+ export * from './backends/store/fs.js';
8
+ export * from './backends/store/simple.js';
9
+ export * from './backends/store/store.js';
10
+ export * from './backends/backend.js';
11
+ export * from './config.js';
12
+ export * from './credentials.js';
13
+ export * from './devices.js';
14
+ export { default as devices } from './devices.js';
15
+ export * from './file.js';
16
+ export * from './filesystem.js';
17
+ export * from './inode.js';
18
+ export * from './mixins/index.js';
19
+ export * from './stats.js';
20
+ export * from './utils.js';
21
+
22
+ export * from './emulation/index.js';
23
+ import * as fs from './emulation/index.js';
24
+ export { fs };
25
+ export default fs;
package/src/inode.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { Stats, type StatsLike } from './stats.js';
2
+ import { types as t, struct, sizeof, serialize, deserialize } from 'utilium';
3
+
4
+ /**
5
+ * Alias for an ino.
6
+ * This will be helpful if in the future inode numbers/IDs are changed to strings or numbers.
7
+ */
8
+ export type Ino = bigint;
9
+
10
+ /**
11
+ * Room inode
12
+ * @hidden
13
+ */
14
+ export const rootIno = 0n;
15
+
16
+ /**
17
+ * Generates a random 32 bit integer, then converts to a hex string
18
+ */
19
+ function _random() {
20
+ return Math.round(Math.random() * 2 ** 32).toString(16);
21
+ }
22
+
23
+ /**
24
+ * Generate a random ino
25
+ * @internal
26
+ */
27
+ export function randomIno(): Ino {
28
+ return BigInt('0x' + _random() + _random());
29
+ }
30
+
31
+ /**
32
+ * Generic inode definition that can easily be serialized.
33
+ */
34
+ @struct()
35
+ export class Inode implements StatsLike {
36
+ public get data(): Uint8Array {
37
+ return serialize(this);
38
+ }
39
+
40
+ public constructor(buffer?: ArrayBufferLike) {
41
+ if (buffer) {
42
+ if (buffer.byteLength < sizeof(Inode)) {
43
+ throw new RangeError(`Can not create an inode from a buffer less than ${sizeof(Inode)} bytes`);
44
+ }
45
+
46
+ deserialize(this, buffer);
47
+ return;
48
+ }
49
+
50
+ // set defaults on a fresh inode
51
+ this.ino = randomIno();
52
+ this.nlink = 1;
53
+ this.size = 4096;
54
+ const now = Date.now();
55
+ this.atimeMs = now;
56
+ this.mtimeMs = now;
57
+ this.ctimeMs = now;
58
+ this.birthtimeMs = now;
59
+ }
60
+
61
+ @t.uint64 public ino!: Ino;
62
+ @t.uint32 public size!: number;
63
+ @t.uint16 public mode!: number;
64
+ @t.uint32 public nlink!: number;
65
+ @t.uint32 public uid!: number;
66
+ @t.uint32 public gid!: number;
67
+ @t.float64 public atimeMs!: number;
68
+ @t.float64 public birthtimeMs!: number;
69
+ @t.float64 public mtimeMs!: number;
70
+ @t.float64 public ctimeMs!: number;
71
+
72
+ /**
73
+ * Handy function that converts the Inode to a Node Stats object.
74
+ */
75
+ public toStats(): Stats {
76
+ return new Stats(this);
77
+ }
78
+
79
+ /**
80
+ * Updates the Inode using information from the stats object. Used by file
81
+ * systems at sync time, e.g.:
82
+ * - Program opens file and gets a File object.
83
+ * - Program mutates file. File object is responsible for maintaining
84
+ * metadata changes locally -- typically in a Stats object.
85
+ * - Program closes file. File object's metadata changes are synced with the
86
+ * file system.
87
+ * @return True if any changes have occurred.
88
+ */
89
+ public update(stats: Readonly<Stats>): boolean {
90
+ let hasChanged = false;
91
+ if (this.size !== stats.size) {
92
+ this.size = stats.size;
93
+ hasChanged = true;
94
+ }
95
+
96
+ if (this.mode !== stats.mode) {
97
+ this.mode = stats.mode;
98
+ hasChanged = true;
99
+ }
100
+
101
+ if (this.nlink !== stats.nlink) {
102
+ this.nlink = stats.nlink;
103
+ hasChanged = true;
104
+ }
105
+
106
+ if (this.uid !== stats.uid) {
107
+ this.uid = stats.uid;
108
+ hasChanged = true;
109
+ }
110
+
111
+ if (this.uid !== stats.uid) {
112
+ this.uid = stats.uid;
113
+ hasChanged = true;
114
+ }
115
+
116
+ if (this.atimeMs !== stats.atimeMs) {
117
+ this.atimeMs = stats.atimeMs;
118
+ hasChanged = true;
119
+ }
120
+ if (this.mtimeMs !== stats.mtimeMs) {
121
+ this.mtimeMs = stats.mtimeMs;
122
+ hasChanged = true;
123
+ }
124
+
125
+ if (this.ctimeMs !== stats.ctimeMs) {
126
+ this.ctimeMs = stats.ctimeMs;
127
+ hasChanged = true;
128
+ }
129
+
130
+ return hasChanged;
131
+ }
132
+ }
@@ -0,0 +1,208 @@
1
+ import { join } from '../emulation/path.js';
2
+ import { Errno, ErrnoError } from '../error.js';
3
+ import { parseFlag, PreloadFile, type File } from '../file.js';
4
+ import type { FileSystem } from '../filesystem.js';
5
+ import type { Stats } from '../stats.js';
6
+ import type { AsyncFSMethods, Mixin } from './shared.js';
7
+
8
+ /** @internal */
9
+ export type AsyncOperation = {
10
+ [K in keyof AsyncFSMethods]: [K, ...Parameters<FileSystem[K]>];
11
+ }[keyof AsyncFSMethods];
12
+
13
+ /**
14
+ * Async() implements synchronous methods on an asynchronous file system
15
+ *
16
+ * Implementing classes must define `_sync` for the synchronous file system used as a cache.
17
+ *
18
+ * Synchronous methods on an asynchronous FS are implemented by performing operations over the in-memory copy,
19
+ * while asynchronously pipelining them to the backing store.
20
+ * During loading, the contents of the async file system are preloaded into the synchronous store.
21
+ *
22
+ */
23
+ export function Async<T extends typeof FileSystem>(
24
+ FS: T
25
+ ): Mixin<
26
+ T,
27
+ {
28
+ /**
29
+ * @internal @protected
30
+ */
31
+ _sync?: FileSystem;
32
+ queueDone(): Promise<void>;
33
+ ready(): Promise<void>;
34
+ renameSync(oldPath: string, newPath: string): void;
35
+ statSync(path: string): Stats;
36
+ createFileSync(path: string, flag: string, mode: number): File;
37
+ openFileSync(path: string, flag: string): File;
38
+ unlinkSync(path: string): void;
39
+ rmdirSync(path: string): void;
40
+ mkdirSync(path: string, mode: number): void;
41
+ readdirSync(path: string): string[];
42
+ linkSync(srcpath: string, dstpath: string): void;
43
+ syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
44
+ }
45
+ > {
46
+ abstract class AsyncFS extends FS {
47
+ /**
48
+ * Queue of pending asynchronous operations.
49
+ */
50
+ private _queue: AsyncOperation[] = [];
51
+ private get _queueRunning(): boolean {
52
+ return !!this._queue.length;
53
+ }
54
+
55
+ public queueDone(): Promise<void> {
56
+ return new Promise(resolve => {
57
+ const check = (): unknown => (this._queueRunning ? setTimeout(check) : resolve());
58
+ check();
59
+ });
60
+ }
61
+
62
+ private _isInitialized: boolean = false;
63
+
64
+ abstract _sync?: FileSystem;
65
+
66
+ public async ready(): Promise<void> {
67
+ await super.ready();
68
+ if (this._isInitialized || this._disableSync) {
69
+ return;
70
+ }
71
+ this.checkSync();
72
+
73
+ await this._sync.ready();
74
+
75
+ try {
76
+ await this.crossCopy('/');
77
+ this._isInitialized = true;
78
+ } catch (e) {
79
+ this._isInitialized = false;
80
+ throw e;
81
+ }
82
+ }
83
+
84
+ protected checkSync(path?: string, syscall?: string): asserts this is { _sync: FileSystem } {
85
+ if (this._disableSync) {
86
+ throw new ErrnoError(Errno.ENOTSUP, 'Sync caching has been disabled for this async file system', path, syscall);
87
+ }
88
+ if (!this._sync) {
89
+ throw new ErrnoError(Errno.ENOTSUP, 'No sync cache is attached to this async file system', path, syscall);
90
+ }
91
+ }
92
+
93
+ public renameSync(oldPath: string, newPath: string): void {
94
+ this.checkSync(oldPath, 'rename');
95
+ this._sync.renameSync(oldPath, newPath);
96
+ this.queue('rename', oldPath, newPath);
97
+ }
98
+
99
+ public statSync(path: string): Stats {
100
+ this.checkSync(path, 'stat');
101
+ return this._sync.statSync(path);
102
+ }
103
+
104
+ public createFileSync(path: string, flag: string, mode: number): PreloadFile<this> {
105
+ this.checkSync(path, 'createFile');
106
+ this._sync.createFileSync(path, flag, mode);
107
+ this.queue('createFile', path, flag, mode);
108
+ return this.openFileSync(path, flag);
109
+ }
110
+
111
+ public openFileSync(path: string, flag: string): PreloadFile<this> {
112
+ this.checkSync(path, 'openFile');
113
+ const file = this._sync.openFileSync(path, flag);
114
+ const stats = file.statSync();
115
+ const buffer = new Uint8Array(stats.size);
116
+ file.readSync(buffer);
117
+ return new PreloadFile(this, path, flag, stats, buffer);
118
+ }
119
+
120
+ public unlinkSync(path: string): void {
121
+ this.checkSync(path, 'unlinkSync');
122
+ this._sync.unlinkSync(path);
123
+ this.queue('unlink', path);
124
+ }
125
+
126
+ public rmdirSync(path: string): void {
127
+ this.checkSync(path, 'rmdir');
128
+ this._sync.rmdirSync(path);
129
+ this.queue('rmdir', path);
130
+ }
131
+
132
+ public mkdirSync(path: string, mode: number): void {
133
+ this.checkSync(path, 'mkdir');
134
+ this._sync.mkdirSync(path, mode);
135
+ this.queue('mkdir', path, mode);
136
+ }
137
+
138
+ public readdirSync(path: string): string[] {
139
+ this.checkSync(path, 'readdir');
140
+ return this._sync.readdirSync(path);
141
+ }
142
+
143
+ public linkSync(srcpath: string, dstpath: string): void {
144
+ this.checkSync(srcpath, 'link');
145
+ this._sync.linkSync(srcpath, dstpath);
146
+ this.queue('link', srcpath, dstpath);
147
+ }
148
+
149
+ public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
150
+ this.checkSync(path, 'sync');
151
+ this._sync.syncSync(path, data, stats);
152
+ this.queue('sync', path, data, stats);
153
+ }
154
+
155
+ public existsSync(path: string): boolean {
156
+ this.checkSync(path, 'exists');
157
+ return this._sync.existsSync(path);
158
+ }
159
+
160
+ /**
161
+ * @internal
162
+ */
163
+ protected async crossCopy(path: string): Promise<void> {
164
+ this.checkSync(path, 'crossCopy');
165
+ const stats = await this.stat(path);
166
+ if (!stats.isDirectory()) {
167
+ await using asyncFile = await this.openFile(path, parseFlag('r'));
168
+ using syncFile = this._sync.createFileSync(path, parseFlag('w'), stats.mode);
169
+ const buffer = new Uint8Array(stats.size);
170
+ await asyncFile.read(buffer);
171
+ syncFile.writeSync(buffer, 0, stats.size);
172
+ return;
173
+ }
174
+ if (path !== '/') {
175
+ const stats = await this.stat(path);
176
+ this._sync.mkdirSync(path, stats.mode);
177
+ }
178
+ const files = await this.readdir(path);
179
+ for (const file of files) {
180
+ await this.crossCopy(join(path, file));
181
+ }
182
+ }
183
+
184
+ /**
185
+ * @internal
186
+ */
187
+ private async _next(): Promise<void> {
188
+ if (!this._queueRunning) {
189
+ return;
190
+ }
191
+
192
+ const [method, ...args] = this._queue.shift()!;
193
+ // @ts-expect-error 2556 (since ...args is not correctly picked up as being a tuple)
194
+ await this[method](...args);
195
+ await this._next();
196
+ }
197
+
198
+ /**
199
+ * @internal
200
+ */
201
+ private queue(...op: AsyncOperation) {
202
+ this._queue.push(op);
203
+ void this._next();
204
+ }
205
+ }
206
+
207
+ return AsyncFS;
208
+ }
@@ -0,0 +1,5 @@
1
+ export * from './async.js';
2
+ export * from './mutexed.js';
3
+ export * from './readonly.js';
4
+ export * from './shared.js';
5
+ export * from './sync.js';