@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.
- package/dist/backends/overlay.d.ts +3 -5
- package/dist/backends/overlay.js +9 -9
- package/dist/backends/port/fs.d.ts +1 -2
- package/dist/backends/port/fs.js +2 -3
- package/dist/browser.min.js +4 -4
- package/dist/browser.min.js.map +3 -3
- package/dist/emulation/promises.js +14 -15
- package/dist/emulation/sync.js +13 -14
- package/dist/file.d.ts +22 -13
- package/dist/file.js +16 -7
- package/dist/mixins/mutexed.d.ts +72 -10
- package/dist/mixins/mutexed.js +375 -379
- package/dist/mixins/shared.d.ts +1 -0
- package/dist/mixins/shared.js +1 -0
- package/dist/stats.d.ts +2 -3
- package/dist/stats.js +4 -4
- package/dist/utils.d.ts +2 -1
- package/package.json +1 -1
- package/readme.md +1 -1
- package/src/backends/overlay.ts +9 -11
- package/src/backends/port/fs.ts +4 -4
- package/src/emulation/promises.ts +14 -15
- package/src/emulation/sync.ts +13 -14
- package/src/file.ts +16 -11
- package/src/mixins/mutexed.ts +194 -189
- package/src/mixins/shared.ts +3 -2
- package/src/stats.ts +4 -5
- package/src/utils.ts +3 -1
package/src/mixins/mutexed.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
T
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Whether `path` is locked
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
public get isLocked(): boolean {
|
|
102
|
+
return !!this.currentLock?.isLocked;
|
|
103
|
+
}
|
|
125
104
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
121
|
+
public statSync(path: string): Stats {
|
|
122
|
+
using _ = this.lockSync(path, 'stat');
|
|
123
|
+
return this._fs.statSync(path);
|
|
124
|
+
}
|
|
149
125
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
159
|
+
public unlinkSync(path: string): void {
|
|
160
|
+
using _ = this.lockSync(path, 'unlink');
|
|
161
|
+
return this._fs.unlinkSync(path);
|
|
162
|
+
}
|
|
185
163
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
169
|
+
public rmdirSync(path: string): void {
|
|
170
|
+
using _ = this.lockSync(path, 'rmdir');
|
|
171
|
+
return this._fs.rmdirSync(path);
|
|
172
|
+
}
|
|
197
173
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
189
|
+
public readdirSync(path: string): string[] {
|
|
190
|
+
using _ = this.lockSync(path, 'readdir');
|
|
191
|
+
return this._fs.readdirSync(path);
|
|
192
|
+
}
|
|
220
193
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
199
|
+
public existsSync(path: string): boolean {
|
|
200
|
+
using _ = this.lockSync(path, 'exists');
|
|
201
|
+
return this._fs.existsSync(path);
|
|
202
|
+
}
|
|
231
203
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
}
|
package/src/mixins/shared.ts
CHANGED
|
@@ -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
|
|
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
|
|
241
|
-
if (
|
|
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 = (
|
|
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>);
|