@zenfs/core 0.9.7 → 0.10.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/AsyncStore.js +29 -29
- package/dist/backends/Fetch.d.ts +84 -0
- package/dist/backends/Fetch.js +171 -0
- package/dist/backends/Index.js +19 -19
- package/dist/backends/Locked.d.ts +11 -11
- package/dist/backends/Locked.js +50 -49
- package/dist/backends/Overlay.js +21 -21
- package/dist/backends/SyncStore.js +27 -27
- package/dist/backends/backend.js +4 -4
- package/dist/backends/port/fs.d.ts +124 -0
- package/dist/backends/port/fs.js +241 -0
- package/dist/backends/port/rpc.d.ts +60 -0
- package/dist/backends/port/rpc.js +71 -0
- package/dist/backends/port/store.d.ts +30 -0
- package/dist/backends/port/store.js +142 -0
- package/dist/browser.min.js +4 -4
- package/dist/browser.min.js.map +4 -4
- package/dist/config.d.ts +8 -10
- package/dist/config.js +11 -11
- package/dist/emulation/async.js +6 -6
- package/dist/emulation/dir.js +2 -2
- package/dist/emulation/index.d.ts +1 -1
- package/dist/emulation/index.js +1 -1
- package/dist/emulation/path.d.ts +3 -2
- package/dist/emulation/path.js +19 -45
- package/dist/emulation/promises.d.ts +7 -12
- package/dist/emulation/promises.js +144 -146
- package/dist/emulation/shared.d.ts +5 -10
- package/dist/emulation/shared.js +8 -8
- package/dist/emulation/streams.js +3 -3
- package/dist/emulation/sync.js +25 -25
- package/dist/{ApiError.d.ts → error.d.ts} +13 -14
- package/dist/error.js +292 -0
- package/dist/file.d.ts +2 -0
- package/dist/file.js +10 -4
- package/dist/filesystem.js +15 -15
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mutex.js +2 -1
- package/dist/utils.d.ts +8 -7
- package/dist/utils.js +11 -12
- package/package.json +3 -3
- package/readme.md +17 -9
- package/src/backends/AsyncStore.ts +29 -29
- package/src/backends/Fetch.ts +230 -0
- package/src/backends/Index.ts +19 -19
- package/src/backends/Locked.ts +50 -49
- package/src/backends/Overlay.ts +23 -23
- package/src/backends/SyncStore.ts +27 -27
- package/src/backends/backend.ts +6 -6
- package/src/backends/port/fs.ts +308 -0
- package/src/backends/port/readme.md +59 -0
- package/src/backends/port/rpc.ts +144 -0
- package/src/backends/port/store.ts +187 -0
- package/src/config.ts +20 -24
- package/src/emulation/async.ts +6 -6
- package/src/emulation/dir.ts +2 -2
- package/src/emulation/index.ts +1 -1
- package/src/emulation/path.ts +25 -49
- package/src/emulation/promises.ts +150 -159
- package/src/emulation/shared.ts +12 -14
- package/src/emulation/streams.ts +3 -3
- package/src/emulation/sync.ts +28 -28
- package/src/{ApiError.ts → error.ts} +89 -89
- package/src/file.ts +12 -4
- package/src/filesystem.ts +15 -15
- package/src/index.ts +4 -1
- package/src/mutex.ts +3 -1
- package/src/utils.ts +16 -18
- package/tsconfig.json +2 -2
- package/dist/ApiError.js +0 -292
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, basename, join, resolve } from '../emulation/path.js';
|
|
2
|
-
import {
|
|
2
|
+
import { ErrnoError, Errno } from '../error.js';
|
|
3
3
|
import { W_OK, R_OK } from '../emulation/constants.js';
|
|
4
4
|
import { PreloadFile, flagToMode } from '../file.js';
|
|
5
5
|
import { Async, FileSystem } from '../filesystem.js';
|
|
@@ -53,7 +53,7 @@ class LRUCache {
|
|
|
53
53
|
export class AsyncStoreFS extends Async(FileSystem) {
|
|
54
54
|
get store() {
|
|
55
55
|
if (!this._store) {
|
|
56
|
-
throw new
|
|
56
|
+
throw new ErrnoError(Errno.ENODATA, 'No store attached');
|
|
57
57
|
}
|
|
58
58
|
return this._store;
|
|
59
59
|
}
|
|
@@ -110,10 +110,10 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
110
110
|
// Remove oldPath from parent's directory listing.
|
|
111
111
|
oldDirNode = await this.findINode(tx, oldParent), oldDirList = await this.getDirListing(tx, oldDirNode, oldParent);
|
|
112
112
|
if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
113
|
-
throw
|
|
113
|
+
throw ErrnoError.With('EACCES', oldPath, 'rename');
|
|
114
114
|
}
|
|
115
115
|
if (!oldDirList[oldName]) {
|
|
116
|
-
throw
|
|
116
|
+
throw ErrnoError.With('ENOENT', oldPath, 'rename');
|
|
117
117
|
}
|
|
118
118
|
const nodeId = oldDirList[oldName];
|
|
119
119
|
delete oldDirList[oldName];
|
|
@@ -122,7 +122,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
122
122
|
// is a subpath of newParent. We append '/' to avoid matching folders that
|
|
123
123
|
// are a substring of the bottom-most folder in the path.
|
|
124
124
|
if ((newParent + '/').indexOf(oldPath + '/') === 0) {
|
|
125
|
-
throw new
|
|
125
|
+
throw new ErrnoError(Errno.EBUSY, oldParent);
|
|
126
126
|
}
|
|
127
127
|
// Add newPath to parent's directory listing.
|
|
128
128
|
let newDirNode, newDirList;
|
|
@@ -151,7 +151,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
151
151
|
}
|
|
152
152
|
else {
|
|
153
153
|
// If it's a directory, throw a permissions error.
|
|
154
|
-
throw
|
|
154
|
+
throw ErrnoError.With('EPERM', newPath, 'rename');
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
newDirList[newName] = nodeId;
|
|
@@ -177,11 +177,11 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
177
177
|
const tx = this.store.beginTransaction();
|
|
178
178
|
const inode = await this.findINode(tx, p);
|
|
179
179
|
if (!inode) {
|
|
180
|
-
throw
|
|
180
|
+
throw ErrnoError.With('ENOENT', p, 'stat');
|
|
181
181
|
}
|
|
182
182
|
const stats = inode.toStats();
|
|
183
183
|
if (!stats.hasAccess(R_OK, cred)) {
|
|
184
|
-
throw
|
|
184
|
+
throw ErrnoError.With('EACCES', p, 'stat');
|
|
185
185
|
}
|
|
186
186
|
return stats;
|
|
187
187
|
}
|
|
@@ -195,10 +195,10 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
195
195
|
await this.queueDone();
|
|
196
196
|
const tx = this.store.beginTransaction(), node = await this.findINode(tx, p), data = await tx.get(node.ino);
|
|
197
197
|
if (!node.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
198
|
-
throw
|
|
198
|
+
throw ErrnoError.With('EACCES', p, 'openFile');
|
|
199
199
|
}
|
|
200
200
|
if (!data) {
|
|
201
|
-
throw
|
|
201
|
+
throw ErrnoError.With('ENOENT', p, 'openFile');
|
|
202
202
|
}
|
|
203
203
|
return new PreloadFile(this, p, flag, node.toStats(), data);
|
|
204
204
|
}
|
|
@@ -211,7 +211,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
211
211
|
// Check first if directory is empty.
|
|
212
212
|
const list = await this.readdir(p, cred);
|
|
213
213
|
if (list.length > 0) {
|
|
214
|
-
throw
|
|
214
|
+
throw ErrnoError.With('ENOTEMPTY', p, 'rmdir');
|
|
215
215
|
}
|
|
216
216
|
await this.removeEntry(p, true, cred);
|
|
217
217
|
}
|
|
@@ -225,7 +225,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
225
225
|
const tx = this.store.beginTransaction();
|
|
226
226
|
const node = await this.findINode(tx, p);
|
|
227
227
|
if (!node.toStats().hasAccess(R_OK, cred)) {
|
|
228
|
-
throw
|
|
228
|
+
throw ErrnoError.With('EACCES', p, 'readdur');
|
|
229
229
|
}
|
|
230
230
|
return Object.keys(await this.getDirListing(tx, node, p));
|
|
231
231
|
}
|
|
@@ -256,16 +256,16 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
256
256
|
await this.queueDone();
|
|
257
257
|
const tx = this.store.beginTransaction(), existingDir = dirname(existing), existingDirNode = await this.findINode(tx, existingDir);
|
|
258
258
|
if (!existingDirNode.toStats().hasAccess(R_OK, cred)) {
|
|
259
|
-
throw
|
|
259
|
+
throw ErrnoError.With('EACCES', existingDir, 'link');
|
|
260
260
|
}
|
|
261
261
|
const newDir = dirname(newpath), newDirNode = await this.findINode(tx, newDir), newListing = await this.getDirListing(tx, newDirNode, newDir);
|
|
262
262
|
if (!newDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
263
|
-
throw
|
|
263
|
+
throw ErrnoError.With('EACCES', newDir, 'link');
|
|
264
264
|
}
|
|
265
265
|
const ino = await this._findINode(tx, existingDir, basename(existing));
|
|
266
266
|
const node = await this.getINode(tx, ino, existing);
|
|
267
267
|
if (!node.toStats().hasAccess(W_OK, cred)) {
|
|
268
|
-
throw
|
|
268
|
+
throw ErrnoError.With('EACCES', newpath, 'link');
|
|
269
269
|
}
|
|
270
270
|
node.nlink++;
|
|
271
271
|
newListing[basename(newpath)] = ino;
|
|
@@ -303,7 +303,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
303
303
|
async _findINode(tx, parent, filename, visited = new Set()) {
|
|
304
304
|
const currentPath = join(parent, filename);
|
|
305
305
|
if (visited.has(currentPath)) {
|
|
306
|
-
throw new
|
|
306
|
+
throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
|
|
307
307
|
}
|
|
308
308
|
visited.add(currentPath);
|
|
309
309
|
if (this._cache) {
|
|
@@ -332,7 +332,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
332
332
|
return id;
|
|
333
333
|
}
|
|
334
334
|
else {
|
|
335
|
-
throw
|
|
335
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
}
|
|
@@ -349,7 +349,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
349
349
|
return id;
|
|
350
350
|
}
|
|
351
351
|
else {
|
|
352
|
-
throw
|
|
352
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
}
|
|
@@ -371,7 +371,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
371
371
|
async getINode(tx, id, p) {
|
|
372
372
|
const data = await tx.get(id);
|
|
373
373
|
if (!data) {
|
|
374
|
-
throw
|
|
374
|
+
throw ErrnoError.With('ENOENT', p, 'getINode');
|
|
375
375
|
}
|
|
376
376
|
return new Inode(data.buffer);
|
|
377
377
|
}
|
|
@@ -381,7 +381,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
381
381
|
*/
|
|
382
382
|
async getDirListing(tx, inode, p) {
|
|
383
383
|
if (!inode.toStats().isDirectory()) {
|
|
384
|
-
throw
|
|
384
|
+
throw ErrnoError.With('ENOTDIR', p, 'getDirListing');
|
|
385
385
|
}
|
|
386
386
|
const data = await tx.get(inode.ino);
|
|
387
387
|
if (!data) {
|
|
@@ -390,7 +390,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
390
390
|
than a directory listing. The latter should never occur unless
|
|
391
391
|
the file system is corrupted.
|
|
392
392
|
*/
|
|
393
|
-
throw
|
|
393
|
+
throw ErrnoError.With('ENOENT', p, 'getDirListing');
|
|
394
394
|
}
|
|
395
395
|
return decodeDirListing(data);
|
|
396
396
|
}
|
|
@@ -401,7 +401,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
401
401
|
async addNewNode(tx, data, _maxAttempts = 5) {
|
|
402
402
|
if (_maxAttempts <= 0) {
|
|
403
403
|
// Max retries hit. Return with an error.
|
|
404
|
-
throw new
|
|
404
|
+
throw new ErrnoError(Errno.EIO, 'Unable to commit data to key-value store.');
|
|
405
405
|
}
|
|
406
406
|
// Make an attempt
|
|
407
407
|
const ino = randomIno();
|
|
@@ -425,18 +425,18 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
425
425
|
const parentDir = dirname(p), fname = basename(p), parentNode = await this.findINode(tx, parentDir), dirListing = await this.getDirListing(tx, parentNode, parentDir);
|
|
426
426
|
//Check that the creater has correct access
|
|
427
427
|
if (!parentNode.toStats().hasAccess(W_OK, cred)) {
|
|
428
|
-
throw
|
|
428
|
+
throw ErrnoError.With('EACCES', p, 'commitNewFile');
|
|
429
429
|
}
|
|
430
430
|
// Invariant: The root always exists.
|
|
431
431
|
// If we don't check this prior to taking steps below, we will create a
|
|
432
432
|
// file with name '' in root should p == '/'.
|
|
433
433
|
if (p === '/') {
|
|
434
|
-
throw
|
|
434
|
+
throw ErrnoError.With('EEXIST', p, 'commitNewFile');
|
|
435
435
|
}
|
|
436
436
|
// Check if file already exists.
|
|
437
437
|
if (dirListing[fname]) {
|
|
438
438
|
await tx.abort();
|
|
439
|
-
throw
|
|
439
|
+
throw ErrnoError.With('EEXIST', p, 'commitNewFile');
|
|
440
440
|
}
|
|
441
441
|
try {
|
|
442
442
|
// Commit data.
|
|
@@ -475,21 +475,21 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
475
475
|
}
|
|
476
476
|
const tx = this.store.beginTransaction(), parent = dirname(p), parentNode = await this.findINode(tx, parent), parentListing = await this.getDirListing(tx, parentNode, parent), fileName = basename(p);
|
|
477
477
|
if (!parentListing[fileName]) {
|
|
478
|
-
throw
|
|
478
|
+
throw ErrnoError.With('ENOENT', p, 'removeEntry');
|
|
479
479
|
}
|
|
480
480
|
const fileIno = parentListing[fileName];
|
|
481
481
|
// Get file inode.
|
|
482
482
|
const fileNode = await this.getINode(tx, fileIno, p);
|
|
483
483
|
if (!fileNode.toStats().hasAccess(W_OK, cred)) {
|
|
484
|
-
throw
|
|
484
|
+
throw ErrnoError.With('EACCES', p, 'removeEntry');
|
|
485
485
|
}
|
|
486
486
|
// Remove from directory listing of parent.
|
|
487
487
|
delete parentListing[fileName];
|
|
488
488
|
if (!isDir && fileNode.toStats().isDirectory()) {
|
|
489
|
-
throw
|
|
489
|
+
throw ErrnoError.With('EISDIR', p, 'removeEntry');
|
|
490
490
|
}
|
|
491
491
|
if (isDir && !fileNode.toStats().isDirectory()) {
|
|
492
|
-
throw
|
|
492
|
+
throw ErrnoError.With('ENOTDIR', p, 'removeEntry');
|
|
493
493
|
}
|
|
494
494
|
try {
|
|
495
495
|
await tx.put(parentNode.ino, encodeDirListing(parentListing), true);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NoSyncFile } from '../file.js';
|
|
2
|
+
import type { FileSystemMetadata } from '../filesystem.js';
|
|
3
|
+
import { Stats } from '../stats.js';
|
|
4
|
+
import { type ListingTree, type IndexFileInode, AsyncIndexFS } from './Index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration options for FetchFS.
|
|
7
|
+
*/
|
|
8
|
+
export interface FetchOptions {
|
|
9
|
+
/**
|
|
10
|
+
* URL to a file index as a JSON file or the file index object itself.
|
|
11
|
+
* Defaults to `index.json`.
|
|
12
|
+
*/
|
|
13
|
+
index?: string | ListingTree;
|
|
14
|
+
/** Used as the URL prefix for fetched files.
|
|
15
|
+
* Default: Fetch files relative to the index.
|
|
16
|
+
*/
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A simple filesystem backed by HTTP using the fetch API.
|
|
21
|
+
*
|
|
22
|
+
*
|
|
23
|
+
* Listings objects look like the following:
|
|
24
|
+
*
|
|
25
|
+
* ```json
|
|
26
|
+
* {
|
|
27
|
+
* "home": {
|
|
28
|
+
* "jvilk": {
|
|
29
|
+
* "someFile.txt": null,
|
|
30
|
+
* "someDir": {
|
|
31
|
+
* // Empty directory
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.
|
|
39
|
+
*/
|
|
40
|
+
export declare class FetchFS extends AsyncIndexFS<Stats> {
|
|
41
|
+
readonly prefixUrl: string;
|
|
42
|
+
protected _init: Promise<void>;
|
|
43
|
+
protected _initialize(index: string | ListingTree): Promise<void>;
|
|
44
|
+
ready(): Promise<this>;
|
|
45
|
+
constructor({ index, baseUrl }: FetchOptions);
|
|
46
|
+
metadata(): FileSystemMetadata;
|
|
47
|
+
empty(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Special function: Preload the given file into the index.
|
|
50
|
+
* @param path
|
|
51
|
+
* @param buffer
|
|
52
|
+
*/
|
|
53
|
+
preloadFile(path: string, buffer: Uint8Array): void;
|
|
54
|
+
protected statFileInode(inode: IndexFileInode<Stats>, path: string): Promise<Stats>;
|
|
55
|
+
protected openFileInode(inode: IndexFileInode<Stats>, path: string, flag: string): Promise<NoSyncFile<this>>;
|
|
56
|
+
private _getRemotePath;
|
|
57
|
+
/**
|
|
58
|
+
* Asynchronously download the given file.
|
|
59
|
+
*/
|
|
60
|
+
protected _fetchFile(p: string, type: 'buffer'): Promise<Uint8Array>;
|
|
61
|
+
protected _fetchFile(p: string, type: 'json'): Promise<object>;
|
|
62
|
+
protected _fetchFile(p: string, type: 'buffer' | 'json'): Promise<object>;
|
|
63
|
+
/**
|
|
64
|
+
* Only requests the HEAD content, for the file size.
|
|
65
|
+
*/
|
|
66
|
+
protected _fetchSize(path: string): Promise<number>;
|
|
67
|
+
}
|
|
68
|
+
export declare const Fetch: {
|
|
69
|
+
readonly name: "Fetch";
|
|
70
|
+
readonly options: {
|
|
71
|
+
readonly index: {
|
|
72
|
+
readonly type: readonly ["string", "object"];
|
|
73
|
+
readonly required: false;
|
|
74
|
+
readonly description: "URL to a file index as a JSON file or the file index object itself, generated with the make_http_index script. Defaults to `index.json`.";
|
|
75
|
+
};
|
|
76
|
+
readonly baseUrl: {
|
|
77
|
+
readonly type: "string";
|
|
78
|
+
readonly required: false;
|
|
79
|
+
readonly description: "Used as the URL prefix for fetched files. Default: Fetch files relative to the index.";
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
readonly isAvailable: () => boolean;
|
|
83
|
+
readonly create: (options: FetchOptions) => FetchFS;
|
|
84
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { ErrnoError, Errno } from '../error.js';
|
|
2
|
+
import { NoSyncFile } from '../file.js';
|
|
3
|
+
import { Stats } from '../stats.js';
|
|
4
|
+
import { FileIndex, AsyncIndexFS } from './Index.js';
|
|
5
|
+
/**
|
|
6
|
+
* @hidden
|
|
7
|
+
*/
|
|
8
|
+
function convertError(e) {
|
|
9
|
+
throw new ErrnoError(Errno.EIO, e.message);
|
|
10
|
+
}
|
|
11
|
+
async function fetchFile(p, type) {
|
|
12
|
+
const response = await fetch(p).catch(convertError);
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new ErrnoError(Errno.EIO, 'fetch failed: response returned code ' + response.status);
|
|
15
|
+
}
|
|
16
|
+
switch (type) {
|
|
17
|
+
case 'buffer':
|
|
18
|
+
const arrayBuffer = await response.arrayBuffer().catch(convertError);
|
|
19
|
+
return new Uint8Array(arrayBuffer);
|
|
20
|
+
case 'json':
|
|
21
|
+
return response.json().catch(convertError);
|
|
22
|
+
default:
|
|
23
|
+
throw new ErrnoError(Errno.EINVAL, 'Invalid download type: ' + type);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Asynchronously retrieves the size of the given file in bytes.
|
|
28
|
+
* @hidden
|
|
29
|
+
*/
|
|
30
|
+
async function fetchSize(p) {
|
|
31
|
+
const response = await fetch(p, { method: 'HEAD' }).catch(convertError);
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new ErrnoError(Errno.EIO, 'fetch failed: HEAD response returned code ' + response.status);
|
|
34
|
+
}
|
|
35
|
+
return parseInt(response.headers.get('Content-Length') || '-1', 10);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A simple filesystem backed by HTTP using the fetch API.
|
|
39
|
+
*
|
|
40
|
+
*
|
|
41
|
+
* Listings objects look like the following:
|
|
42
|
+
*
|
|
43
|
+
* ```json
|
|
44
|
+
* {
|
|
45
|
+
* "home": {
|
|
46
|
+
* "jvilk": {
|
|
47
|
+
* "someFile.txt": null,
|
|
48
|
+
* "someDir": {
|
|
49
|
+
* // Empty directory
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.
|
|
57
|
+
*/
|
|
58
|
+
export class FetchFS extends AsyncIndexFS {
|
|
59
|
+
async _initialize(index) {
|
|
60
|
+
if (typeof index != 'string') {
|
|
61
|
+
this._index = FileIndex.FromListing(index);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(index);
|
|
66
|
+
this._index = FileIndex.FromListing((await response.json()));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
throw new ErrnoError(Errno.EINVAL, 'Invalid or unavailable file listing tree');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async ready() {
|
|
73
|
+
await this._init;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
constructor({ index = 'index.json', baseUrl = '' }) {
|
|
77
|
+
super({});
|
|
78
|
+
// prefix url must end in a directory separator.
|
|
79
|
+
if (baseUrl.at(-1) != '/') {
|
|
80
|
+
baseUrl += '/';
|
|
81
|
+
}
|
|
82
|
+
this.prefixUrl = baseUrl;
|
|
83
|
+
this._init = this._initialize(index);
|
|
84
|
+
}
|
|
85
|
+
metadata() {
|
|
86
|
+
return {
|
|
87
|
+
...super.metadata(),
|
|
88
|
+
name: FetchFS.name,
|
|
89
|
+
readonly: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
empty() {
|
|
93
|
+
for (const file of this._index.files()) {
|
|
94
|
+
delete file.data.fileData;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Special function: Preload the given file into the index.
|
|
99
|
+
* @param path
|
|
100
|
+
* @param buffer
|
|
101
|
+
*/
|
|
102
|
+
preloadFile(path, buffer) {
|
|
103
|
+
const inode = this._index.get(path);
|
|
104
|
+
if (!inode) {
|
|
105
|
+
throw ErrnoError.With('ENOENT', path, 'preloadFile');
|
|
106
|
+
}
|
|
107
|
+
if (!inode.isFile()) {
|
|
108
|
+
throw ErrnoError.With('EISDIR', path, 'preloadFile');
|
|
109
|
+
}
|
|
110
|
+
const stats = inode.data;
|
|
111
|
+
stats.size = buffer.length;
|
|
112
|
+
stats.fileData = buffer;
|
|
113
|
+
}
|
|
114
|
+
async statFileInode(inode, path) {
|
|
115
|
+
const stats = inode.data;
|
|
116
|
+
// At this point, a non-opened file will still have default stats from the listing.
|
|
117
|
+
if (stats.size < 0) {
|
|
118
|
+
stats.size = await this._fetchSize(path);
|
|
119
|
+
}
|
|
120
|
+
return stats;
|
|
121
|
+
}
|
|
122
|
+
async openFileInode(inode, path, flag) {
|
|
123
|
+
const stats = inode.data;
|
|
124
|
+
// Use existing file contents. This maintains the previously-used flag.
|
|
125
|
+
if (stats.fileData) {
|
|
126
|
+
return new NoSyncFile(this, path, flag, new Stats(stats), stats.fileData);
|
|
127
|
+
}
|
|
128
|
+
// @todo be lazier about actually requesting the file
|
|
129
|
+
const data = await this._fetchFile(path, 'buffer');
|
|
130
|
+
// we don't initially have file sizes
|
|
131
|
+
stats.size = data.length;
|
|
132
|
+
stats.fileData = data;
|
|
133
|
+
return new NoSyncFile(this, path, flag, new Stats(stats), data);
|
|
134
|
+
}
|
|
135
|
+
_getRemotePath(filePath) {
|
|
136
|
+
if (filePath.charAt(0) === '/') {
|
|
137
|
+
filePath = filePath.slice(1);
|
|
138
|
+
}
|
|
139
|
+
return this.prefixUrl + filePath;
|
|
140
|
+
}
|
|
141
|
+
_fetchFile(p, type) {
|
|
142
|
+
return fetchFile(this._getRemotePath(p), type);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Only requests the HEAD content, for the file size.
|
|
146
|
+
*/
|
|
147
|
+
_fetchSize(path) {
|
|
148
|
+
return fetchSize(this._getRemotePath(path));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export const Fetch = {
|
|
152
|
+
name: 'Fetch',
|
|
153
|
+
options: {
|
|
154
|
+
index: {
|
|
155
|
+
type: ['string', 'object'],
|
|
156
|
+
required: false,
|
|
157
|
+
description: 'URL to a file index as a JSON file or the file index object itself, generated with the make_http_index script. Defaults to `index.json`.',
|
|
158
|
+
},
|
|
159
|
+
baseUrl: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
required: false,
|
|
162
|
+
description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
isAvailable() {
|
|
166
|
+
return typeof globalThis.fetch == 'function';
|
|
167
|
+
},
|
|
168
|
+
create(options) {
|
|
169
|
+
return new FetchFS(options);
|
|
170
|
+
},
|
|
171
|
+
};
|
package/dist/backends/Index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ErrnoError, Errno } from '../error.js';
|
|
2
2
|
import { basename, dirname, join } from '../emulation/path.js';
|
|
3
3
|
import { NoSyncFile, flagToMode, isWriteable } from '../file.js';
|
|
4
4
|
import { FileSystem, Readonly } from '../filesystem.js';
|
|
@@ -91,10 +91,10 @@ export class FileIndex {
|
|
|
91
91
|
*/
|
|
92
92
|
add(path, inode) {
|
|
93
93
|
if (!inode) {
|
|
94
|
-
throw new
|
|
94
|
+
throw new ErrnoError(Errno.EINVAL, 'Inode must be specified', path, 'FileIndex.add');
|
|
95
95
|
}
|
|
96
96
|
if (!path.startsWith('/')) {
|
|
97
|
-
throw new
|
|
97
|
+
throw new ErrnoError(Errno.EINVAL, 'Path not absolute', path, 'FileIndex.add');
|
|
98
98
|
}
|
|
99
99
|
// Check if it already exists.
|
|
100
100
|
if (this._index.has(path)) {
|
|
@@ -308,7 +308,7 @@ export class IndexFS extends Readonly(FileSystem) {
|
|
|
308
308
|
async stat(path) {
|
|
309
309
|
const inode = this._index.get(path);
|
|
310
310
|
if (!inode) {
|
|
311
|
-
throw
|
|
311
|
+
throw ErrnoError.With('ENOENT', path, 'stat');
|
|
312
312
|
}
|
|
313
313
|
if (inode.isDirectory()) {
|
|
314
314
|
return inode.stats;
|
|
@@ -316,12 +316,12 @@ export class IndexFS extends Readonly(FileSystem) {
|
|
|
316
316
|
if (inode.isFile()) {
|
|
317
317
|
return this.statFileInode(inode, path);
|
|
318
318
|
}
|
|
319
|
-
throw new
|
|
319
|
+
throw new ErrnoError(Errno.EINVAL, 'Invalid inode.');
|
|
320
320
|
}
|
|
321
321
|
statSync(path) {
|
|
322
322
|
const inode = this._index.get(path);
|
|
323
323
|
if (!inode) {
|
|
324
|
-
throw
|
|
324
|
+
throw ErrnoError.With('ENOENT', path, 'stat');
|
|
325
325
|
}
|
|
326
326
|
if (inode.isDirectory()) {
|
|
327
327
|
return inode.stats;
|
|
@@ -329,20 +329,20 @@ export class IndexFS extends Readonly(FileSystem) {
|
|
|
329
329
|
if (inode.isFile()) {
|
|
330
330
|
return this.statFileInodeSync(inode, path);
|
|
331
331
|
}
|
|
332
|
-
throw new
|
|
332
|
+
throw new ErrnoError(Errno.EINVAL, 'Invalid inode.');
|
|
333
333
|
}
|
|
334
334
|
async openFile(path, flag, cred) {
|
|
335
335
|
if (isWriteable(flag)) {
|
|
336
336
|
// You can't write to files on this file system.
|
|
337
|
-
throw new
|
|
337
|
+
throw new ErrnoError(Errno.EPERM, path);
|
|
338
338
|
}
|
|
339
339
|
// Check if the path exists, and is a file.
|
|
340
340
|
const inode = this._index.get(path);
|
|
341
341
|
if (!inode) {
|
|
342
|
-
throw
|
|
342
|
+
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
343
343
|
}
|
|
344
344
|
if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
345
|
-
throw
|
|
345
|
+
throw ErrnoError.With('EACCES', path, 'openFile');
|
|
346
346
|
}
|
|
347
347
|
if (inode.isDirectory()) {
|
|
348
348
|
const stats = inode.stats;
|
|
@@ -353,15 +353,15 @@ export class IndexFS extends Readonly(FileSystem) {
|
|
|
353
353
|
openFileSync(path, flag, cred) {
|
|
354
354
|
if (isWriteable(flag)) {
|
|
355
355
|
// You can't write to files on this file system.
|
|
356
|
-
throw new
|
|
356
|
+
throw new ErrnoError(Errno.EPERM, path);
|
|
357
357
|
}
|
|
358
358
|
// Check if the path exists, and is a file.
|
|
359
359
|
const inode = this._index.get(path);
|
|
360
360
|
if (!inode) {
|
|
361
|
-
throw
|
|
361
|
+
throw ErrnoError.With('ENOENT', path, 'openFile');
|
|
362
362
|
}
|
|
363
363
|
if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
364
|
-
throw
|
|
364
|
+
throw ErrnoError.With('EACCES', path, 'openFile');
|
|
365
365
|
}
|
|
366
366
|
if (inode.isDirectory()) {
|
|
367
367
|
const stats = inode.stats;
|
|
@@ -373,23 +373,23 @@ export class IndexFS extends Readonly(FileSystem) {
|
|
|
373
373
|
// Check if it exists.
|
|
374
374
|
const inode = this._index.get(path);
|
|
375
375
|
if (!inode) {
|
|
376
|
-
throw
|
|
376
|
+
throw ErrnoError.With('ENOENT', path, 'readdir');
|
|
377
377
|
}
|
|
378
378
|
if (inode.isDirectory()) {
|
|
379
379
|
return inode.listing;
|
|
380
380
|
}
|
|
381
|
-
throw
|
|
381
|
+
throw ErrnoError.With('ENOTDIR', path, 'readdir');
|
|
382
382
|
}
|
|
383
383
|
readdirSync(path) {
|
|
384
384
|
// Check if it exists.
|
|
385
385
|
const inode = this._index.get(path);
|
|
386
386
|
if (!inode) {
|
|
387
|
-
throw
|
|
387
|
+
throw ErrnoError.With('ENOENT', path, 'readdir');
|
|
388
388
|
}
|
|
389
389
|
if (inode.isDirectory()) {
|
|
390
390
|
return inode.listing;
|
|
391
391
|
}
|
|
392
|
-
throw
|
|
392
|
+
throw ErrnoError.With('ENOTDIR', path, 'readdir');
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
export class SyncIndexFS extends IndexFS {
|
|
@@ -402,9 +402,9 @@ export class SyncIndexFS extends IndexFS {
|
|
|
402
402
|
}
|
|
403
403
|
export class AsyncIndexFS extends IndexFS {
|
|
404
404
|
statFileInodeSync(inode, path) {
|
|
405
|
-
throw
|
|
405
|
+
throw ErrnoError.With('ENOSYS', path, 'AsyncIndexFS.statFileInodeSync');
|
|
406
406
|
}
|
|
407
407
|
openFileInodeSync(inode, path, flag) {
|
|
408
|
-
throw
|
|
408
|
+
throw ErrnoError.With('ENOSYS', path, 'AsyncIndexFS.openFileInodeSync');
|
|
409
409
|
}
|
|
410
410
|
}
|
|
@@ -20,22 +20,22 @@ export declare class LockedFS<FS extends FileSystem> implements FileSystem {
|
|
|
20
20
|
metadata(): FileSystemMetadata;
|
|
21
21
|
rename(oldPath: string, newPath: string, cred: Cred): Promise<void>;
|
|
22
22
|
renameSync(oldPath: string, newPath: string, cred: Cred): void;
|
|
23
|
-
stat(
|
|
24
|
-
statSync(
|
|
23
|
+
stat(path: string, cred: Cred): Promise<Stats>;
|
|
24
|
+
statSync(path: string, cred: Cred): Stats;
|
|
25
25
|
openFile(path: string, flag: string, cred: Cred): Promise<File>;
|
|
26
26
|
openFileSync(path: string, flag: string, cred: Cred): File;
|
|
27
27
|
createFile(path: string, flag: string, mode: number, cred: Cred): Promise<File>;
|
|
28
28
|
createFileSync(path: string, flag: string, mode: number, cred: Cred): File;
|
|
29
29
|
unlink(p: string, cred: Cred): Promise<void>;
|
|
30
|
-
unlinkSync(
|
|
31
|
-
rmdir(
|
|
32
|
-
rmdirSync(
|
|
33
|
-
mkdir(
|
|
34
|
-
mkdirSync(
|
|
35
|
-
readdir(
|
|
36
|
-
readdirSync(
|
|
37
|
-
exists(
|
|
38
|
-
existsSync(
|
|
30
|
+
unlinkSync(path: string, cred: Cred): void;
|
|
31
|
+
rmdir(path: string, cred: Cred): Promise<void>;
|
|
32
|
+
rmdirSync(path: string, cred: Cred): void;
|
|
33
|
+
mkdir(path: string, mode: number, cred: Cred): Promise<void>;
|
|
34
|
+
mkdirSync(path: string, mode: number, cred: Cred): void;
|
|
35
|
+
readdir(path: string, cred: Cred): Promise<string[]>;
|
|
36
|
+
readdirSync(path: string, cred: Cred): string[];
|
|
37
|
+
exists(path: string, cred: Cred): Promise<boolean>;
|
|
38
|
+
existsSync(path: string, cred: Cred): boolean;
|
|
39
39
|
link(srcpath: string, dstpath: string, cred: Cred): Promise<void>;
|
|
40
40
|
linkSync(srcpath: string, dstpath: string, cred: Cred): void;
|
|
41
41
|
sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void>;
|