@zenfs/core 0.18.0 → 1.0.0-rc.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.
@@ -1,9 +1,9 @@
1
1
  import { ErrnoError } from '../error.js';
2
2
  import type { File } from '../file.js';
3
- import type { FileSystem } from '../filesystem.js';
3
+ import type { FileSystem, FileSystemMetadata } from '../filesystem.js';
4
4
  import '../polyfills.js';
5
5
  import type { Stats } from '../stats.js';
6
- import type { Mixin } from './shared.js';
6
+ import type { Concrete } from '../utils.js';
7
7
 
8
8
  export class MutexLock {
9
9
  protected current = Promise.withResolvers<void>();
@@ -13,10 +13,7 @@ export class MutexLock {
13
13
  return this._isLocked;
14
14
  }
15
15
 
16
- public constructor(
17
- public readonly path: string,
18
- protected readonly previous?: MutexLock
19
- ) {}
16
+ public constructor(protected readonly previous?: MutexLock) {}
20
17
 
21
18
  public async done(): Promise<void> {
22
19
  await this.previous?.done();
@@ -34,219 +31,227 @@ export class MutexLock {
34
31
  }
35
32
 
36
33
  /**
37
- * This serializes access to an underlying async filesystem.
38
- * For example, on an OverlayFS instance with an async lower
39
- * directory operations like rename and rmdir may involve multiple
40
- * requests involving both the upper and lower filesystems -- they
41
- * are not executed in a single atomic step. OverlayFS uses this
42
- * to avoid having to reason about the correctness of
43
- * multiple requests interleaving.
44
- *
45
- * Note: `@ts-expect-error 2513` is needed because `FS` is not properly detected as being concrete
46
- *
47
- * @todo Change `using _` to `using void` pending https://github.com/tc39/proposal-discard-binding
48
- * @internal
34
+ * @hidden
49
35
  */
50
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
- export function Mutexed<T extends new (...args: any[]) => FileSystem>(
52
- FS: T
53
- ): Mixin<
54
- T,
55
- {
56
- lock(path: string, syscall: string): Promise<MutexLock>;
57
- lockSync(path: string): MutexLock;
58
- isLocked(path: string): boolean;
59
- }
60
- > {
61
- class MutexedFS extends FS {
62
- /**
63
- * The current locks
64
- */
65
- private locks: Map<string, MutexLock> = new Map();
66
-
67
- /**
68
- * Adds a lock for a path
69
- */
70
- protected addLock(path: string): MutexLock {
71
- const previous = this.locks.get(path);
72
- const lock = new MutexLock(path, previous?.isLocked ? previous : undefined);
73
- this.locks.set(path, lock);
74
- return lock;
75
- }
36
+ export class __MutexedFS<T extends FileSystem> implements FileSystem {
37
+ /**
38
+ * @internal
39
+ */
40
+ public _fs!: T;
41
+
42
+ public async ready(): Promise<void> {
43
+ return await this._fs.ready();
44
+ }
76
45
 
77
- /**
78
- * Locks `path` asynchronously.
79
- * If the path is currently locked, waits for it to be unlocked.
80
- * @internal
81
- */
82
- public async lock(path: string, syscall: string): Promise<MutexLock> {
83
- const previous = this.locks.get(path);
84
- const lock = this.addLock(path);
85
- const stack = new Error().stack;
86
- setTimeout(() => {
87
- if (lock.isLocked) {
88
- const error = ErrnoError.With('EDEADLK', path, syscall);
89
- error.stack += stack?.slice('Error'.length);
90
- throw error;
91
- }
92
- }, 5000);
93
- await previous?.done();
94
- return lock;
95
- }
46
+ public metadata(): FileSystemMetadata {
47
+ return this._fs.metadata();
48
+ }
96
49
 
97
- /**
98
- * Locks `path` asynchronously.
99
- * If the path is currently locked, an error will be thrown
100
- * @internal
101
- */
102
- public lockSync(path: string): MutexLock {
103
- if (this.locks.has(path)) {
104
- // Non-null assertion: we already checked locks has path
105
- throw ErrnoError.With('EBUSY', path, 'lock');
50
+ /**
51
+ * The current locks
52
+ */
53
+ private currentLock?: MutexLock;
54
+
55
+ /**
56
+ * Adds a lock for a path
57
+ */
58
+ protected addLock(): MutexLock {
59
+ const lock = new MutexLock(this.currentLock);
60
+ this.currentLock = lock;
61
+ return lock;
62
+ }
63
+
64
+ /**
65
+ * Locks `path` asynchronously.
66
+ * If the path is currently locked, waits for it to be unlocked.
67
+ * @internal
68
+ */
69
+ public async lock(path: string, syscall: string): Promise<MutexLock> {
70
+ const previous = this.currentLock;
71
+ const lock = this.addLock();
72
+ const stack = new Error().stack;
73
+ setTimeout(() => {
74
+ if (lock.isLocked) {
75
+ const error = ErrnoError.With('EDEADLK', path, syscall);
76
+ error.stack += stack?.slice('Error'.length);
77
+ throw error;
106
78
  }
79
+ }, 5000);
80
+ await previous?.done();
81
+ return lock;
82
+ }
107
83
 
108
- return this.addLock(path);
84
+ /**
85
+ * Locks `path` asynchronously.
86
+ * If the path is currently locked, an error will be thrown
87
+ * @internal
88
+ */
89
+ public lockSync(path: string, syscall: string): MutexLock {
90
+ if (this.currentLock) {
91
+ throw ErrnoError.With('EBUSY', path, syscall);
109
92
  }
110
93
 
111
- /**
112
- * Whether `path` is locked
113
- * @internal
114
- */
115
- public isLocked(path: string): boolean {
116
- return !!this.locks.get(path)?.isLocked;
117
- }
94
+ return this.addLock();
95
+ }
118
96
 
119
- /* eslint-disable @typescript-eslint/no-unused-vars */
120
- public async rename(oldPath: string, newPath: string): Promise<void> {
121
- using _ = await this.lock(oldPath, 'rename');
122
- // @ts-expect-error 2513
123
- await super.rename(oldPath, newPath);
124
- }
97
+ /**
98
+ * Whether `path` is locked
99
+ * @internal
100
+ */
101
+ public get isLocked(): boolean {
102
+ return !!this.currentLock?.isLocked;
103
+ }
125
104
 
126
- public renameSync(oldPath: string, newPath: string): void {
127
- using _ = this.lockSync(oldPath);
128
- // @ts-expect-error 2513
129
- return super.renameSync(oldPath, newPath);
130
- }
105
+ /* eslint-disable @typescript-eslint/no-unused-vars */
106
+ public async rename(oldPath: string, newPath: string): Promise<void> {
107
+ using _ = await this.lock(oldPath, 'rename');
108
+ await this._fs.rename(oldPath, newPath);
109
+ }
131
110
 
132
- public async stat(path: string): Promise<Stats> {
133
- using _ = await this.lock(path, 'stat');
134
- // @ts-expect-error 2513
135
- return await super.stat(path);
136
- }
111
+ public renameSync(oldPath: string, newPath: string): void {
112
+ using _ = this.lockSync(oldPath, 'rename');
113
+ return this._fs.renameSync(oldPath, newPath);
114
+ }
137
115
 
138
- public statSync(path: string): Stats {
139
- using _ = this.lockSync(path);
140
- // @ts-expect-error 2513
141
- return super.statSync(path);
142
- }
116
+ public async stat(path: string): Promise<Stats> {
117
+ using _ = await this.lock(path, 'stat');
118
+ return await this._fs.stat(path);
119
+ }
143
120
 
144
- public async openFile(path: string, flag: string): Promise<File> {
145
- using _ = await this.lock(path, 'openFile');
146
- // @ts-expect-error 2513
147
- return await super.openFile(path, flag);
148
- }
121
+ public statSync(path: string): Stats {
122
+ using _ = this.lockSync(path, 'stat');
123
+ return this._fs.statSync(path);
124
+ }
149
125
 
150
- public openFileSync(path: string, flag: string): File {
151
- using _ = this.lockSync(path);
152
- // @ts-expect-error 2513
153
- return super.openFileSync(path, flag);
154
- }
126
+ public async openFile(path: string, flag: string): Promise<File> {
127
+ using _ = await this.lock(path, 'openFile');
128
+ const file = await this._fs.openFile(path, flag);
129
+ file.fs = this;
130
+ return file;
131
+ }
155
132
 
156
- public async createFile(path: string, flag: string, mode: number): Promise<File> {
157
- using _ = await this.lock(path, 'createFile');
158
- // @ts-expect-error 2513
159
- return await super.createFile(path, flag, mode);
160
- }
133
+ public openFileSync(path: string, flag: string): File {
134
+ using _ = this.lockSync(path, 'openFile');
135
+ const file = this._fs.openFileSync(path, flag);
136
+ file.fs = this;
137
+ return file;
138
+ }
161
139
 
162
- public createFileSync(path: string, flag: string, mode: number): File {
163
- using _ = this.lockSync(path);
164
- // @ts-expect-error 2513
165
- return super.createFileSync(path, flag, mode);
166
- }
140
+ public async createFile(path: string, flag: string, mode: number): Promise<File> {
141
+ using _ = await this.lock(path, 'createFile');
142
+ const file = await this._fs.createFile(path, flag, mode);
143
+ file.fs = this;
144
+ return file;
145
+ }
167
146
 
168
- public async unlink(path: string): Promise<void> {
169
- using _ = await this.lock(path, 'unlink');
170
- // @ts-expect-error 2513
171
- await super.unlink(path);
172
- }
147
+ public createFileSync(path: string, flag: string, mode: number): File {
148
+ using _ = this.lockSync(path, 'createFile');
149
+ const file = this._fs.createFileSync(path, flag, mode);
150
+ file.fs = this;
151
+ return file;
152
+ }
173
153
 
174
- public unlinkSync(path: string): void {
175
- using _ = this.lockSync(path);
176
- // @ts-expect-error 2513
177
- return super.unlinkSync(path);
178
- }
154
+ public async unlink(path: string): Promise<void> {
155
+ using _ = await this.lock(path, 'unlink');
156
+ await this._fs.unlink(path);
157
+ }
179
158
 
180
- public async rmdir(path: string): Promise<void> {
181
- using _ = await this.lock(path, 'rmdir');
182
- // @ts-expect-error 2513
183
- await super.rmdir(path);
184
- }
159
+ public unlinkSync(path: string): void {
160
+ using _ = this.lockSync(path, 'unlink');
161
+ return this._fs.unlinkSync(path);
162
+ }
185
163
 
186
- public rmdirSync(path: string): void {
187
- using _ = this.lockSync(path);
188
- // @ts-expect-error 2513
189
- return super.rmdirSync(path);
190
- }
164
+ public async rmdir(path: string): Promise<void> {
165
+ using _ = await this.lock(path, 'rmdir');
166
+ await this._fs.rmdir(path);
167
+ }
191
168
 
192
- public async mkdir(path: string, mode: number): Promise<void> {
193
- using _ = await this.lock(path, 'mkdir');
194
- // @ts-expect-error 2513
195
- await super.mkdir(path, mode);
196
- }
169
+ public rmdirSync(path: string): void {
170
+ using _ = this.lockSync(path, 'rmdir');
171
+ return this._fs.rmdirSync(path);
172
+ }
197
173
 
198
- public mkdirSync(path: string, mode: number): void {
199
- using _ = this.lockSync(path);
200
- // @ts-expect-error 2513
201
- return super.mkdirSync(path, mode);
202
- }
174
+ public async mkdir(path: string, mode: number): Promise<void> {
175
+ using _ = await this.lock(path, 'mkdir');
176
+ await this._fs.mkdir(path, mode);
177
+ }
203
178
 
204
- public async readdir(path: string): Promise<string[]> {
205
- using _ = await this.lock(path, 'readdir');
206
- // @ts-expect-error 2513
207
- return await super.readdir(path);
208
- }
179
+ public mkdirSync(path: string, mode: number): void {
180
+ using _ = this.lockSync(path, 'mkdir');
181
+ return this._fs.mkdirSync(path, mode);
182
+ }
209
183
 
210
- public readdirSync(path: string): string[] {
211
- using _ = this.lockSync(path);
212
- // @ts-expect-error 2513
213
- return super.readdirSync(path);
214
- }
184
+ public async readdir(path: string): Promise<string[]> {
185
+ using _ = await this.lock(path, 'readdir');
186
+ return await this._fs.readdir(path);
187
+ }
215
188
 
216
- public async exists(path: string): Promise<boolean> {
217
- using _ = await this.lock(path, 'exists');
218
- return await super.exists(path);
219
- }
189
+ public readdirSync(path: string): string[] {
190
+ using _ = this.lockSync(path, 'readdir');
191
+ return this._fs.readdirSync(path);
192
+ }
220
193
 
221
- public existsSync(path: string): boolean {
222
- using _ = this.lockSync(path);
223
- return super.existsSync(path);
224
- }
194
+ public async exists(path: string): Promise<boolean> {
195
+ using _ = await this.lock(path, 'exists');
196
+ return await this._fs.exists(path);
197
+ }
225
198
 
226
- public async link(srcpath: string, dstpath: string): Promise<void> {
227
- using _ = await this.lock(srcpath, 'link');
228
- // @ts-expect-error 2513
229
- await super.link(srcpath, dstpath);
230
- }
199
+ public existsSync(path: string): boolean {
200
+ using _ = this.lockSync(path, 'exists');
201
+ return this._fs.existsSync(path);
202
+ }
231
203
 
232
- public linkSync(srcpath: string, dstpath: string): void {
233
- using _ = this.lockSync(srcpath);
234
- // @ts-expect-error 2513
235
- return super.linkSync(srcpath, dstpath);
236
- }
204
+ public async link(srcpath: string, dstpath: string): Promise<void> {
205
+ using _ = await this.lock(srcpath, 'link');
206
+ await this._fs.link(srcpath, dstpath);
207
+ }
237
208
 
238
- public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
239
- using _ = await this.lock(path, 'sync');
240
- // @ts-expect-error 2513
241
- await super.sync(path, data, stats);
242
- }
209
+ public linkSync(srcpath: string, dstpath: string): void {
210
+ using _ = this.lockSync(srcpath, 'link');
211
+ return this._fs.linkSync(srcpath, dstpath);
212
+ }
213
+
214
+ public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
215
+ using _ = await this.lock(path, 'sync');
216
+ await this._fs.sync(path, data, stats);
217
+ }
218
+
219
+ public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
220
+ using _ = this.lockSync(path, 'sync');
221
+ return this._fs.syncSync(path, data, stats);
222
+ }
223
+ /* eslint-enable @typescript-eslint/no-unused-vars */
224
+ }
243
225
 
244
- public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
245
- using _ = this.lockSync(path);
246
- // @ts-expect-error 2513
247
- return super.syncSync(path, data, stats);
226
+ /**
227
+ * This serializes access to an underlying async filesystem.
228
+ * For example, on an OverlayFS instance with an async lower
229
+ * directory operations like rename and rmdir may involve multiple
230
+ * requests involving both the upper and lower filesystems -- they
231
+ * are not executed in a single atomic step. OverlayFS uses this
232
+ * to avoid having to reason about the correctness of
233
+ * multiple requests interleaving.
234
+ *
235
+ * Note:
236
+ * Instead of extending the passed class, `MutexedFS` stores it internally.
237
+ * This is to avoid a deadlock caused when a mathod calls another one
238
+ * The problem is discussed extensivly in [#78](https://github.com/zen-fs/core/issues/78)
239
+ * Instead of extending `FileSystem`,
240
+ * `MutexedFS` implements it in order to make sure all of the methods are passed through
241
+ *
242
+ * @todo Change `using _` to `using void` pending https://github.com/tc39/proposal-discard-binding
243
+ * @internal
244
+ */
245
+ export function Mutexed<const T extends Concrete<typeof FileSystem>>(
246
+ FS: T
247
+ ): typeof __MutexedFS<InstanceType<T>> & {
248
+ new (...args: ConstructorParameters<T>): __MutexedFS<InstanceType<T>>;
249
+ } {
250
+ class MutexedFS extends __MutexedFS<InstanceType<T>> {
251
+ public constructor(...args: ConstructorParameters<T>) {
252
+ super();
253
+ this._fs = new FS(...args) as InstanceType<T>;
248
254
  }
249
- /* eslint-enable @typescript-eslint/no-unused-vars */
250
255
  }
251
256
  return MutexedFS;
252
257
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  /*
2
3
  Code shared by various mixins
3
4
  */
@@ -9,12 +10,12 @@ import type { FileSystem } from '../filesystem.js';
9
10
  * `TBase` with `TMixin` mixed-in.
10
11
  * @internal @experimental
11
12
  */
12
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
13
  export type Mixin<TBase extends typeof FileSystem, TMixin> = (abstract new (...args: any[]) => TMixin) & TBase;
14
14
 
15
15
  /**
16
16
  * Asynchronous `FileSystem` methods. This is a convience type.
17
17
  * @internal
18
18
  */
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
19
  export type _AsyncFSMethods = ExtractProperties<FileSystem, (...args: any[]) => Promise<unknown>>;
20
+
21
+ export type ConcreteFS = ExtractProperties<FileSystem, any>;
package/src/stats.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type * as Node from 'fs';
2
- import type { Credentials } from './credentials.js';
2
+ import { credentials, type Credentials } from './credentials.js';
3
3
  import { S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRWXG, S_IRWXO, S_IRWXU } from './emulation/constants.js';
4
4
  import { size_max } from './inode.js';
5
5
 
@@ -233,18 +233,17 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
233
233
  /**
234
234
  * Checks if a given user/group has access to this item
235
235
  * @param mode The requested access, combination of W_OK, R_OK, and X_OK
236
- * @param cred The requesting credentials
237
236
  * @returns True if the request has access, false if the request does not
238
237
  * @internal
239
238
  */
240
- public hasAccess(mode: number, cred: Credentials): boolean {
241
- if (cred.euid === 0 || cred.egid === 0) {
239
+ public hasAccess(mode: number): boolean {
240
+ if (credentials.euid === 0 || credentials.egid === 0) {
242
241
  //Running as root
243
242
  return true;
244
243
  }
245
244
 
246
245
  // Mask for
247
- const adjusted = (cred.uid == this.uid ? S_IRWXU : 0) | (cred.gid == this.gid ? S_IRWXG : 0) | S_IRWXO;
246
+ const adjusted = (credentials.uid == this.uid ? S_IRWXU : 0) | (credentials.gid == this.gid ? S_IRWXG : 0) | S_IRWXO;
248
247
  return (mode & this.mode & adjusted) == mode;
249
248
  }
250
249
 
package/src/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return */
2
2
  import type * as fs from 'node:fs';
3
- import type { OptionalTuple } from 'utilium';
3
+ import type { ClassLike, OptionalTuple } from 'utilium';
4
4
  import { dirname, resolve, type AbsolutePath } from './emulation/path.js';
5
5
  import { Errno, ErrnoError } from './error.js';
6
6
  import type { FileSystem } from './filesystem.js';
@@ -262,3 +262,5 @@ export function normalizeOptions(
262
262
  mode: normalizeMode('mode' in options ? options?.mode : null, mode),
263
263
  };
264
264
  }
265
+
266
+ export type Concrete<T extends ClassLike> = Pick<T, keyof T> & (new (...args: any[]) => InstanceType<T>);