@zenfs/core 0.9.2 → 0.9.3
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/package.json +2 -9
- package/src/ApiError.ts +310 -0
- package/src/backends/AsyncStore.ts +635 -0
- package/src/backends/InMemory.ts +56 -0
- package/src/backends/Index.ts +500 -0
- package/src/backends/Locked.ts +181 -0
- package/src/backends/Overlay.ts +591 -0
- package/src/backends/SyncStore.ts +589 -0
- package/src/backends/backend.ts +152 -0
- package/src/config.ts +101 -0
- package/src/cred.ts +21 -0
- package/src/emulation/async.ts +910 -0
- package/src/emulation/constants.ts +176 -0
- package/src/emulation/dir.ts +139 -0
- package/src/emulation/index.ts +8 -0
- package/src/emulation/path.ts +468 -0
- package/src/emulation/promises.ts +1071 -0
- package/src/emulation/shared.ts +128 -0
- package/src/emulation/streams.ts +33 -0
- package/src/emulation/sync.ts +898 -0
- package/src/file.ts +721 -0
- package/src/filesystem.ts +546 -0
- package/src/index.ts +21 -0
- package/src/inode.ts +229 -0
- package/src/mutex.ts +52 -0
- package/src/stats.ts +385 -0
- package/src/utils.ts +287 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { ApiError, ErrorCode } from '../ApiError.js';
|
|
2
|
+
import type { Cred } from '../cred.js';
|
|
3
|
+
import { basename, dirname, join } from '../emulation/path.js';
|
|
4
|
+
import { NoSyncFile, flagToMode, isWriteable } from '../file.js';
|
|
5
|
+
import { FileSystem, Readonly } from '../filesystem.js';
|
|
6
|
+
import { FileType, Stats } from '../stats.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export type ListingTree = { [key: string]: ListingTree | null };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export interface ListingQueueNode<TData> {
|
|
17
|
+
pwd: string;
|
|
18
|
+
tree: ListingTree;
|
|
19
|
+
parent: IndexDirInode<TData>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A simple class for storing a filesystem index. Assumes that all paths passed
|
|
24
|
+
* to it are *absolute* paths.
|
|
25
|
+
*
|
|
26
|
+
* Can be used as a partial or a full index, although care must be taken if used
|
|
27
|
+
* for the former purpose, especially when directories are concerned.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export class FileIndex<TData> {
|
|
32
|
+
/**
|
|
33
|
+
* Static method for constructing indices from a JSON listing.
|
|
34
|
+
* @param listing Directory listing generated by tools
|
|
35
|
+
* @return A new FileIndex object.
|
|
36
|
+
*/
|
|
37
|
+
public static FromListing<T>(listing: ListingTree): FileIndex<T> {
|
|
38
|
+
const index = new FileIndex<T>();
|
|
39
|
+
// Add a root DirNode.
|
|
40
|
+
const rootInode = new IndexDirInode<T>();
|
|
41
|
+
index._index.set('/', rootInode);
|
|
42
|
+
const queue: ListingQueueNode<T | Stats>[] = [{ pwd: '', tree: listing, parent: rootInode }];
|
|
43
|
+
while (queue.length > 0) {
|
|
44
|
+
let inode: IndexFileInode<Stats> | IndexDirInode<T>;
|
|
45
|
+
const { tree, pwd, parent } = queue.pop()!;
|
|
46
|
+
for (const node in tree) {
|
|
47
|
+
if (!Object.hasOwn(tree, node)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const children = tree[node];
|
|
51
|
+
|
|
52
|
+
if (children) {
|
|
53
|
+
const path = pwd + '/' + node;
|
|
54
|
+
inode = new IndexDirInode<T>();
|
|
55
|
+
index._index.set(path, inode);
|
|
56
|
+
queue.push({ pwd: path, tree: children, parent: inode });
|
|
57
|
+
} else {
|
|
58
|
+
// This inode doesn't have correct size information, noted with -1.
|
|
59
|
+
inode = new IndexFileInode<Stats>(new Stats({ mode: FileType.FILE | 0o555 }));
|
|
60
|
+
}
|
|
61
|
+
if (!parent) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
parent._listing.set(node, inode);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return index;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Maps directory paths to directory inodes, which contain files.
|
|
71
|
+
protected _index: Map<string, IndexDirInode<TData>> = new Map();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Constructs a new FileIndex.
|
|
75
|
+
*/
|
|
76
|
+
constructor() {
|
|
77
|
+
// _index is a single-level key,value store that maps *directory* paths to
|
|
78
|
+
// DirInodes. File information is only contained in DirInodes themselves.
|
|
79
|
+
// Create the root directory.
|
|
80
|
+
this.add('/', new IndexDirInode());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public files(): IndexFileInode<TData>[] {
|
|
84
|
+
const files: IndexFileInode<TData>[] = [];
|
|
85
|
+
|
|
86
|
+
for (const dir of this._index.values()) {
|
|
87
|
+
for (const file of dir.listing) {
|
|
88
|
+
const item = dir.get(file);
|
|
89
|
+
if (!item?.isFile()) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
files.push(item);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Adds the given absolute path to the index if it is not already in the index.
|
|
101
|
+
* Creates any needed parent directories.
|
|
102
|
+
* @param path The path to add to the index.
|
|
103
|
+
* @param inode The inode for the
|
|
104
|
+
* path to add.
|
|
105
|
+
* @return 'True' if it was added or already exists, 'false' if there
|
|
106
|
+
* was an issue adding it (e.g. item in path is a file, item exists but is
|
|
107
|
+
* different).
|
|
108
|
+
* @todo If adding fails and implicitly creates directories, we do not clean up
|
|
109
|
+
* the new empty directories.
|
|
110
|
+
*/
|
|
111
|
+
public add(path: string, inode: IndexInode<TData>): boolean {
|
|
112
|
+
if (!inode) {
|
|
113
|
+
throw new Error('Inode must be specified');
|
|
114
|
+
}
|
|
115
|
+
if (!path.startsWith('/')) {
|
|
116
|
+
throw new Error('Path must be absolute, got: ' + path);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if it already exists.
|
|
120
|
+
if (this._index.has(path)) {
|
|
121
|
+
return this._index.get(path) === inode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dirpath = dirname(path);
|
|
125
|
+
|
|
126
|
+
// Try to add to its parent directory first.
|
|
127
|
+
let parent = this._index.get(dirpath);
|
|
128
|
+
if (!parent && path != '/') {
|
|
129
|
+
// Create parent.
|
|
130
|
+
parent = new IndexDirInode<TData>();
|
|
131
|
+
if (!this.add(dirpath, parent)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add to parent.
|
|
137
|
+
if (path != '/' && !parent.add(basename(path), inode)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If a directory, add to the index.
|
|
142
|
+
if (inode.isDirectory()) {
|
|
143
|
+
this._index.set(path, inode);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Adds the given absolute path to the index if it is not already in the index.
|
|
150
|
+
* The path is added without special treatment (no joining of adjacent separators, etc).
|
|
151
|
+
* Creates any needed parent directories.
|
|
152
|
+
* @param path The path to add to the index.
|
|
153
|
+
* @param inode The inode for the path to add.
|
|
154
|
+
* @return 'True' if it was added or already exists, 'false' if there
|
|
155
|
+
* was an issue adding it (e.g. item in path is a file, item exists but is
|
|
156
|
+
* different).
|
|
157
|
+
* @todo If adding fails and implicitly creates directories, we do not clean up the new empty directories.
|
|
158
|
+
*/
|
|
159
|
+
public addFast(path: string, inode: IndexInode<TData>): boolean {
|
|
160
|
+
const parentPath = dirname(path);
|
|
161
|
+
const itemName = basename(path);
|
|
162
|
+
|
|
163
|
+
// Try to add to its parent directory first.
|
|
164
|
+
let parent = this._index.get(parentPath);
|
|
165
|
+
if (!parent) {
|
|
166
|
+
// Create parent.
|
|
167
|
+
parent = new IndexDirInode<TData>();
|
|
168
|
+
this.addFast(parentPath, parent);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!parent.add(itemName, inode)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// If adding a directory, add to the index as well.
|
|
176
|
+
if (inode.isDirectory()) {
|
|
177
|
+
this._index.set(path, <IndexDirInode<TData>>inode);
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Removes the given path. Can be a file or a directory.
|
|
184
|
+
* @return The removed item,
|
|
185
|
+
* or null if it did not exist.
|
|
186
|
+
*/
|
|
187
|
+
public remove(path: string): IndexInode<TData> | null {
|
|
188
|
+
const dirpath = dirname(path);
|
|
189
|
+
|
|
190
|
+
// Try to remove it from its parent directory first.
|
|
191
|
+
const parent = this._index.get(dirpath);
|
|
192
|
+
if (!parent) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Remove from parent.
|
|
196
|
+
const inode = parent.remove(basename(path));
|
|
197
|
+
if (!inode) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!inode.isDirectory()) {
|
|
202
|
+
return inode;
|
|
203
|
+
}
|
|
204
|
+
// If a directory, remove from the index, and remove children.
|
|
205
|
+
const children = inode.listing;
|
|
206
|
+
for (const child of children) {
|
|
207
|
+
this.remove(join(path, child));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Remove the directory from the index, unless it's the root.
|
|
211
|
+
if (path != '/') {
|
|
212
|
+
this._index.delete(path);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Retrieves the directory listing of the given path.
|
|
218
|
+
* @return An array of files in the given path, or 'null' if it does not exist.
|
|
219
|
+
*/
|
|
220
|
+
public ls(path: string): string[] | null {
|
|
221
|
+
return this._index.get(path)?.listing;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Returns the inode of the given item.
|
|
226
|
+
* @return Returns null if the item does not exist.
|
|
227
|
+
*/
|
|
228
|
+
public get(path: string): IndexInode<TData> | null {
|
|
229
|
+
const dirpath = dirname(path);
|
|
230
|
+
|
|
231
|
+
// Retrieve from its parent directory.
|
|
232
|
+
const parent = this._index.get(dirpath);
|
|
233
|
+
// Root case
|
|
234
|
+
if (dirpath == path) {
|
|
235
|
+
return parent;
|
|
236
|
+
}
|
|
237
|
+
return parent?.get(basename(path));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generic interface for file/directory inodes.
|
|
243
|
+
* Note that Stats objects are what we use for file inodes.
|
|
244
|
+
*/
|
|
245
|
+
export abstract class IndexInode<TData> {
|
|
246
|
+
constructor(public data?: TData) {}
|
|
247
|
+
/**
|
|
248
|
+
* Whether this inode is for a file
|
|
249
|
+
*/
|
|
250
|
+
abstract isFile(): this is IndexFileInode<TData>;
|
|
251
|
+
/**
|
|
252
|
+
* Whether this inode is for a directory
|
|
253
|
+
*/
|
|
254
|
+
abstract isDirectory(): this is IndexDirInode<TData>;
|
|
255
|
+
|
|
256
|
+
abstract toStats(): Stats;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Inode for a file. Stores an arbitrary (filesystem-specific) data payload.
|
|
261
|
+
*/
|
|
262
|
+
export class IndexFileInode<TData> extends IndexInode<TData> {
|
|
263
|
+
public isFile() {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
public isDirectory() {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public toStats(): Stats {
|
|
271
|
+
return new Stats({ mode: FileType.FILE | 0o666, size: 4096 });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Inode for a directory. Currently only contains the directory listing.
|
|
277
|
+
*/
|
|
278
|
+
export class IndexDirInode<TData> extends IndexInode<TData> {
|
|
279
|
+
/**
|
|
280
|
+
* @internal
|
|
281
|
+
*/
|
|
282
|
+
_listing: Map<string, IndexInode<TData>> = new Map();
|
|
283
|
+
|
|
284
|
+
public isFile(): boolean {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
public isDirectory(): boolean {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Return a Stats object for this inode.
|
|
294
|
+
* @todo Should probably remove this at some point. This isn't the responsibility of the FileIndex.
|
|
295
|
+
*/
|
|
296
|
+
public get stats(): Stats {
|
|
297
|
+
return new Stats({ mode: FileType.DIRECTORY | 0o555, size: 4096 });
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Alias of getStats()
|
|
301
|
+
* @todo Remove this at some point. This isn't the responsibility of the FileIndex.
|
|
302
|
+
*/
|
|
303
|
+
public toStats(): Stats {
|
|
304
|
+
return this.stats;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Returns the directory listing for this directory. Paths in the directory are
|
|
308
|
+
* relative to the directory's path.
|
|
309
|
+
* @return The directory listing for this directory.
|
|
310
|
+
*/
|
|
311
|
+
public get listing(): string[] {
|
|
312
|
+
return [...this._listing.keys()];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Returns the inode for the indicated item, or null if it does not exist.
|
|
317
|
+
* @param path Name of item in this directory.
|
|
318
|
+
*/
|
|
319
|
+
public get(path: string): IndexInode<TData> | null {
|
|
320
|
+
return this._listing.get(path);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Add the given item to the directory listing. Note that the given inode is
|
|
324
|
+
* not copied, and will be mutated by the DirInode if it is a DirInode.
|
|
325
|
+
* @param path Item name to add to the directory listing.
|
|
326
|
+
* @param inode The inode for the
|
|
327
|
+
* item to add to the directory inode.
|
|
328
|
+
* @return True if it was added, false if it already existed.
|
|
329
|
+
*/
|
|
330
|
+
public add(path: string, inode: IndexInode<TData>): boolean {
|
|
331
|
+
if (this._listing.has(path)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
this._listing.set(path, inode);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Removes the given item from the directory listing.
|
|
339
|
+
* @param p Name of item to remove from the directory listing.
|
|
340
|
+
* @return Returns the item
|
|
341
|
+
* removed, or null if the item did not exist.
|
|
342
|
+
*/
|
|
343
|
+
public remove(p: string): IndexInode<TData> | null {
|
|
344
|
+
const item = this._listing.get(p);
|
|
345
|
+
if (!item) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this._listing.delete(p);
|
|
349
|
+
return item;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export abstract class IndexFS<TData> extends Readonly(FileSystem) {
|
|
354
|
+
protected _index: FileIndex<TData>;
|
|
355
|
+
|
|
356
|
+
constructor(index: ListingTree) {
|
|
357
|
+
super();
|
|
358
|
+
this._index = FileIndex.FromListing<TData>(index);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public async stat(path: string): Promise<Stats> {
|
|
362
|
+
const inode = this._index.get(path);
|
|
363
|
+
if (!inode) {
|
|
364
|
+
throw ApiError.With('ENOENT', path, 'stat');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (inode.isDirectory()) {
|
|
368
|
+
return inode.stats;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (inode.isFile()) {
|
|
372
|
+
return this.statFileInode(inode, path);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
throw new ApiError(ErrorCode.EINVAL, 'Invalid inode.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
public statSync(path: string): Stats {
|
|
379
|
+
const inode = this._index.get(path);
|
|
380
|
+
if (!inode) {
|
|
381
|
+
throw ApiError.With('ENOENT', path, 'stat');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (inode.isDirectory()) {
|
|
385
|
+
return inode.stats;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (inode.isFile()) {
|
|
389
|
+
return this.statFileInodeSync(inode, path);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
throw new ApiError(ErrorCode.EINVAL, 'Invalid inode.');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
public async openFile(path: string, flag: string, cred: Cred): Promise<NoSyncFile<this>> {
|
|
396
|
+
if (isWriteable(flag)) {
|
|
397
|
+
// You can't write to files on this file system.
|
|
398
|
+
throw new ApiError(ErrorCode.EPERM, path);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check if the path exists, and is a file.
|
|
402
|
+
const inode = this._index.get(path);
|
|
403
|
+
|
|
404
|
+
if (!inode) {
|
|
405
|
+
throw ApiError.With('ENOENT', path, 'openFile');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
409
|
+
throw ApiError.With('EACCES', path, 'openFile');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (inode.isDirectory()) {
|
|
413
|
+
const stats = inode.stats;
|
|
414
|
+
return new NoSyncFile(this, path, flag, stats, stats.fileData);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return this.openFileInode(inode, path, flag);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public openFileSync(path: string, flag: string, cred: Cred): NoSyncFile<this> {
|
|
421
|
+
if (isWriteable(flag)) {
|
|
422
|
+
// You can't write to files on this file system.
|
|
423
|
+
throw new ApiError(ErrorCode.EPERM, path);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if the path exists, and is a file.
|
|
427
|
+
const inode = this._index.get(path);
|
|
428
|
+
|
|
429
|
+
if (!inode) {
|
|
430
|
+
throw ApiError.With('ENOENT', path, 'openFile');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
434
|
+
throw ApiError.With('EACCES', path, 'openFile');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (inode.isDirectory()) {
|
|
438
|
+
const stats = inode.stats;
|
|
439
|
+
return new NoSyncFile(this, path, flag, stats, stats.fileData);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return this.openFileInodeSync(inode, path, flag);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public async readdir(path: string): Promise<string[]> {
|
|
446
|
+
// Check if it exists.
|
|
447
|
+
const inode = this._index.get(path);
|
|
448
|
+
if (!inode) {
|
|
449
|
+
throw ApiError.With('ENOENT', path, 'readdir');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (inode.isDirectory()) {
|
|
453
|
+
return inode.listing;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
throw ApiError.With('ENOTDIR', path, 'readdir');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public readdirSync(path: string): string[] {
|
|
460
|
+
// Check if it exists.
|
|
461
|
+
const inode = this._index.get(path);
|
|
462
|
+
if (!inode) {
|
|
463
|
+
throw ApiError.With('ENOENT', path, 'readdir');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (inode.isDirectory()) {
|
|
467
|
+
return inode.listing;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
throw ApiError.With('ENOTDIR', path, 'readdir');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
protected abstract statFileInode(inode: IndexFileInode<TData>, path: string): Promise<Stats>;
|
|
474
|
+
|
|
475
|
+
protected abstract statFileInodeSync(inode: IndexFileInode<TData>, path: string): Stats;
|
|
476
|
+
|
|
477
|
+
protected abstract openFileInode(inode: IndexFileInode<TData>, path: string, flag: string): Promise<NoSyncFile<this>>;
|
|
478
|
+
|
|
479
|
+
protected abstract openFileInodeSync(inode: IndexFileInode<TData>, path: string, flag: string): NoSyncFile<this>;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export abstract class SyncIndexFS<TData> extends IndexFS<TData> {
|
|
483
|
+
protected async statFileInode(inode: IndexFileInode<TData>, path: string): Promise<Stats> {
|
|
484
|
+
return this.statFileInodeSync(inode, path);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
protected async openFileInode(inode: IndexFileInode<TData>, path: string, flag: string): Promise<NoSyncFile<this>> {
|
|
488
|
+
return this.openFileInodeSync(inode, path, flag);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export abstract class AsyncIndexFS<TData> extends IndexFS<TData> {
|
|
493
|
+
protected statFileInodeSync(inode: IndexFileInode<TData>, path: string): Stats {
|
|
494
|
+
throw ApiError.With('ENOSYS', path, 'AsyncIndexFS.statFileInodeSync');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
protected openFileInodeSync(inode: IndexFileInode<TData>, path: string, flag: string): NoSyncFile<this> {
|
|
498
|
+
throw ApiError.With('ENOSYS', path, 'AsyncIndexFS.openFileInodeSync');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Cred } from '../cred.js';
|
|
2
|
+
import type { File } from '../file.js';
|
|
3
|
+
import type { FileSystem, FileSystemMetadata } from '../filesystem.js';
|
|
4
|
+
import { Mutex } from '../mutex.js';
|
|
5
|
+
import type { Stats } from '../stats.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This class serializes access to an underlying async filesystem.
|
|
9
|
+
* For example, on an OverlayFS instance with an async lower
|
|
10
|
+
* directory operations like rename and rmdir may involve multiple
|
|
11
|
+
* requests involving both the upper and lower filesystems -- they
|
|
12
|
+
* are not executed in a single atomic step. OverlayFS uses this
|
|
13
|
+
* LockedFS to avoid having to reason about the correctness of
|
|
14
|
+
* multiple requests interleaving.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export class LockedFS<FS extends FileSystem> implements FileSystem {
|
|
18
|
+
private _mu: Mutex = new Mutex();
|
|
19
|
+
|
|
20
|
+
constructor(public readonly fs: FS) {}
|
|
21
|
+
|
|
22
|
+
public async ready(): Promise<this> {
|
|
23
|
+
await this.fs.ready();
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public metadata(): FileSystemMetadata {
|
|
28
|
+
return {
|
|
29
|
+
...this.fs.metadata(),
|
|
30
|
+
name: 'Locked<' + this.fs.metadata().name + '>',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async rename(oldPath: string, newPath: string, cred: Cred): Promise<void> {
|
|
35
|
+
await this._mu.lock(oldPath);
|
|
36
|
+
await this.fs.rename(oldPath, newPath, cred);
|
|
37
|
+
this._mu.unlock(oldPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public renameSync(oldPath: string, newPath: string, cred: Cred): void {
|
|
41
|
+
if (this._mu.isLocked(oldPath)) {
|
|
42
|
+
throw new Error('invalid sync call');
|
|
43
|
+
}
|
|
44
|
+
return this.fs.renameSync(oldPath, newPath, cred);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async stat(p: string, cred: Cred): Promise<Stats> {
|
|
48
|
+
await this._mu.lock(p);
|
|
49
|
+
const stats = await this.fs.stat(p, cred);
|
|
50
|
+
this._mu.unlock(p);
|
|
51
|
+
return stats;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public statSync(p: string, cred: Cred): Stats {
|
|
55
|
+
if (this._mu.isLocked(p)) {
|
|
56
|
+
throw new Error('invalid sync call');
|
|
57
|
+
}
|
|
58
|
+
return this.fs.statSync(p, cred);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async openFile(path: string, flag: string, cred: Cred): Promise<File> {
|
|
62
|
+
await this._mu.lock(path);
|
|
63
|
+
const fd = await this.fs.openFile(path, flag, cred);
|
|
64
|
+
this._mu.unlock(path);
|
|
65
|
+
return fd;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public openFileSync(path: string, flag: string, cred: Cred): File {
|
|
69
|
+
if (this._mu.isLocked(path)) {
|
|
70
|
+
throw new Error('invalid sync call');
|
|
71
|
+
}
|
|
72
|
+
return this.fs.openFileSync(path, flag, cred);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async createFile(path: string, flag: string, mode: number, cred: Cred): Promise<File> {
|
|
76
|
+
await this._mu.lock(path);
|
|
77
|
+
const fd = await this.fs.createFile(path, flag, mode, cred);
|
|
78
|
+
this._mu.unlock(path);
|
|
79
|
+
return fd;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public createFileSync(path: string, flag: string, mode: number, cred: Cred): File {
|
|
83
|
+
if (this._mu.isLocked(path)) {
|
|
84
|
+
throw new Error('invalid sync call');
|
|
85
|
+
}
|
|
86
|
+
return this.fs.createFileSync(path, flag, mode, cred);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async unlink(p: string, cred: Cred): Promise<void> {
|
|
90
|
+
await this._mu.lock(p);
|
|
91
|
+
await this.fs.unlink(p, cred);
|
|
92
|
+
this._mu.unlock(p);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public unlinkSync(p: string, cred: Cred): void {
|
|
96
|
+
if (this._mu.isLocked(p)) {
|
|
97
|
+
throw new Error('invalid sync call');
|
|
98
|
+
}
|
|
99
|
+
return this.fs.unlinkSync(p, cred);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async rmdir(p: string, cred: Cred): Promise<void> {
|
|
103
|
+
await this._mu.lock(p);
|
|
104
|
+
await this.fs.rmdir(p, cred);
|
|
105
|
+
this._mu.unlock(p);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public rmdirSync(p: string, cred: Cred): void {
|
|
109
|
+
if (this._mu.isLocked(p)) {
|
|
110
|
+
throw new Error('invalid sync call');
|
|
111
|
+
}
|
|
112
|
+
return this.fs.rmdirSync(p, cred);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public async mkdir(p: string, mode: number, cred: Cred): Promise<void> {
|
|
116
|
+
await this._mu.lock(p);
|
|
117
|
+
await this.fs.mkdir(p, mode, cred);
|
|
118
|
+
this._mu.unlock(p);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public mkdirSync(p: string, mode: number, cred: Cred): void {
|
|
122
|
+
if (this._mu.isLocked(p)) {
|
|
123
|
+
throw new Error('invalid sync call');
|
|
124
|
+
}
|
|
125
|
+
return this.fs.mkdirSync(p, mode, cred);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async readdir(p: string, cred: Cred): Promise<string[]> {
|
|
129
|
+
await this._mu.lock(p);
|
|
130
|
+
const files = await this.fs.readdir(p, cred);
|
|
131
|
+
this._mu.unlock(p);
|
|
132
|
+
return files;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public readdirSync(p: string, cred: Cred): string[] {
|
|
136
|
+
if (this._mu.isLocked(p)) {
|
|
137
|
+
throw new Error('invalid sync call');
|
|
138
|
+
}
|
|
139
|
+
return this.fs.readdirSync(p, cred);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async exists(p: string, cred: Cred): Promise<boolean> {
|
|
143
|
+
await this._mu.lock(p);
|
|
144
|
+
const exists = await this.fs.exists(p, cred);
|
|
145
|
+
this._mu.unlock(p);
|
|
146
|
+
return exists;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public existsSync(p: string, cred: Cred): boolean {
|
|
150
|
+
if (this._mu.isLocked(p)) {
|
|
151
|
+
throw new Error('invalid sync call');
|
|
152
|
+
}
|
|
153
|
+
return this.fs.existsSync(p, cred);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public async link(srcpath: string, dstpath: string, cred: Cred): Promise<void> {
|
|
157
|
+
await this._mu.lock(srcpath);
|
|
158
|
+
await this.fs.link(srcpath, dstpath, cred);
|
|
159
|
+
this._mu.unlock(srcpath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public linkSync(srcpath: string, dstpath: string, cred: Cred): void {
|
|
163
|
+
if (this._mu.isLocked(srcpath)) {
|
|
164
|
+
throw new Error('invalid sync call');
|
|
165
|
+
}
|
|
166
|
+
return this.fs.linkSync(srcpath, dstpath, cred);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
|
|
170
|
+
await this._mu.lock(path);
|
|
171
|
+
await this.fs.sync(path, data, stats);
|
|
172
|
+
this._mu.unlock(path);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
|
|
176
|
+
if (this._mu.isLocked(path)) {
|
|
177
|
+
throw new Error('invalid sync call');
|
|
178
|
+
}
|
|
179
|
+
return this.fs.syncSync(path, data, stats);
|
|
180
|
+
}
|
|
181
|
+
}
|