@zenfs/core 0.9.6 → 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.d.ts +3 -2
- package/dist/backends/AsyncStore.js +40 -33
- package/dist/backends/Fetch.d.ts +84 -0
- package/dist/backends/Fetch.js +171 -0
- package/dist/backends/InMemory.d.ts +1 -1
- package/dist/backends/Index.d.ts +7 -10
- package/dist/backends/Index.js +26 -24
- package/dist/backends/Locked.d.ts +11 -11
- package/dist/backends/Locked.js +50 -49
- package/dist/backends/Overlay.js +22 -22
- package/dist/backends/SyncStore.d.ts +6 -6
- package/dist/backends/SyncStore.js +30 -30
- package/dist/backends/backend.d.ts +5 -4
- package/dist/backends/backend.js +6 -6
- 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 +9 -11
- package/dist/config.js +13 -13
- package/dist/emulation/async.d.ts +76 -77
- package/dist/emulation/async.js +45 -45
- package/dist/emulation/dir.js +8 -7
- 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 +112 -113
- package/dist/emulation/promises.js +167 -173
- package/dist/emulation/shared.d.ts +6 -17
- package/dist/emulation/shared.js +9 -9
- package/dist/emulation/streams.js +3 -2
- package/dist/emulation/sync.d.ts +71 -64
- package/dist/emulation/sync.js +62 -63
- package/dist/{ApiError.d.ts → error.d.ts} +15 -15
- package/dist/error.js +292 -0
- package/dist/file.d.ts +6 -4
- package/dist/file.js +17 -9
- package/dist/filesystem.d.ts +1 -1
- package/dist/filesystem.js +18 -15
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mutex.js +4 -3
- package/dist/stats.d.ts +7 -7
- package/dist/stats.js +50 -10
- package/dist/utils.d.ts +10 -9
- package/dist/utils.js +15 -15
- package/package.json +5 -5
- package/readme.md +19 -11
- package/src/backends/AsyncStore.ts +42 -36
- package/src/backends/Fetch.ts +230 -0
- package/src/backends/Index.ts +33 -29
- package/src/backends/Locked.ts +50 -49
- package/src/backends/Overlay.ts +24 -24
- package/src/backends/SyncStore.ts +34 -34
- package/src/backends/backend.ts +13 -11
- 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 +25 -29
- package/src/emulation/async.ts +191 -199
- package/src/emulation/dir.ts +8 -8
- package/src/emulation/index.ts +1 -1
- package/src/emulation/path.ts +25 -49
- package/src/emulation/promises.ts +286 -287
- package/src/emulation/shared.ts +14 -23
- package/src/emulation/streams.ts +9 -8
- package/src/emulation/sync.ts +182 -182
- package/src/{ApiError.ts → error.ts} +91 -89
- package/src/file.ts +23 -13
- package/src/filesystem.ts +26 -22
- package/src/index.ts +4 -1
- package/src/mutex.ts +6 -4
- package/src/stats.ts +32 -23
- package/src/utils.ts +23 -24
- package/tsconfig.json +4 -3
- package/dist/ApiError.js +0 -292
|
@@ -11,7 +11,7 @@ declare class LRUCache<K, V> {
|
|
|
11
11
|
private cache;
|
|
12
12
|
constructor(limit: number);
|
|
13
13
|
set(key: K, value: V): void;
|
|
14
|
-
get(key: K): V |
|
|
14
|
+
get(key: K): V | void;
|
|
15
15
|
remove(key: K): void;
|
|
16
16
|
reset(): void;
|
|
17
17
|
}
|
|
@@ -113,7 +113,8 @@ declare const AsyncStoreFS_base: (abstract new (...args: any[]) => {
|
|
|
113
113
|
*/
|
|
114
114
|
export declare class AsyncStoreFS extends AsyncStoreFS_base {
|
|
115
115
|
protected _options: AsyncStoreOptions;
|
|
116
|
-
protected
|
|
116
|
+
protected _store?: AsyncStore;
|
|
117
|
+
protected get store(): AsyncStore;
|
|
117
118
|
protected _cache?: LRUCache<string, Ino>;
|
|
118
119
|
private _initialized;
|
|
119
120
|
_sync: FileSystem;
|
|
@@ -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';
|
|
@@ -51,16 +51,22 @@ class LRUCache {
|
|
|
51
51
|
* @internal
|
|
52
52
|
*/
|
|
53
53
|
export class AsyncStoreFS extends Async(FileSystem) {
|
|
54
|
+
get store() {
|
|
55
|
+
if (!this._store) {
|
|
56
|
+
throw new ErrnoError(Errno.ENODATA, 'No store attached');
|
|
57
|
+
}
|
|
58
|
+
return this._store;
|
|
59
|
+
}
|
|
54
60
|
async ready() {
|
|
55
61
|
if (this._initialized) {
|
|
56
62
|
return this;
|
|
57
63
|
}
|
|
58
64
|
this._initialized = true;
|
|
59
|
-
if (this._options.lruCacheSize
|
|
65
|
+
if (this._options.lruCacheSize) {
|
|
60
66
|
this._cache = new LRUCache(this._options.lruCacheSize);
|
|
61
67
|
}
|
|
62
|
-
this.
|
|
63
|
-
this._sync = this._options.sync ||
|
|
68
|
+
this._store = await this._options.store;
|
|
69
|
+
this._sync = this._options.sync || this._sync;
|
|
64
70
|
await this.makeRootDirectory();
|
|
65
71
|
await super.ready();
|
|
66
72
|
return this;
|
|
@@ -75,6 +81,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
75
81
|
super();
|
|
76
82
|
this._options = _options;
|
|
77
83
|
this._initialized = false;
|
|
84
|
+
this._sync = InMemory.create({ name: 'test' });
|
|
78
85
|
}
|
|
79
86
|
/**
|
|
80
87
|
* Delete all contents stored in the file system.
|
|
@@ -95,18 +102,18 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
95
102
|
const c = this._cache;
|
|
96
103
|
if (this._cache) {
|
|
97
104
|
// Clear and disable cache during renaming process.
|
|
98
|
-
this._cache
|
|
99
|
-
c
|
|
105
|
+
delete this._cache;
|
|
106
|
+
c?.reset();
|
|
100
107
|
}
|
|
101
108
|
try {
|
|
102
109
|
const tx = this.store.beginTransaction(), oldParent = dirname(oldPath), oldName = basename(oldPath), newParent = dirname(newPath), newName = basename(newPath),
|
|
103
110
|
// Remove oldPath from parent's directory listing.
|
|
104
111
|
oldDirNode = await this.findINode(tx, oldParent), oldDirList = await this.getDirListing(tx, oldDirNode, oldParent);
|
|
105
112
|
if (!oldDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
106
|
-
throw
|
|
113
|
+
throw ErrnoError.With('EACCES', oldPath, 'rename');
|
|
107
114
|
}
|
|
108
115
|
if (!oldDirList[oldName]) {
|
|
109
|
-
throw
|
|
116
|
+
throw ErrnoError.With('ENOENT', oldPath, 'rename');
|
|
110
117
|
}
|
|
111
118
|
const nodeId = oldDirList[oldName];
|
|
112
119
|
delete oldDirList[oldName];
|
|
@@ -115,7 +122,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
115
122
|
// is a subpath of newParent. We append '/' to avoid matching folders that
|
|
116
123
|
// are a substring of the bottom-most folder in the path.
|
|
117
124
|
if ((newParent + '/').indexOf(oldPath + '/') === 0) {
|
|
118
|
-
throw new
|
|
125
|
+
throw new ErrnoError(Errno.EBUSY, oldParent);
|
|
119
126
|
}
|
|
120
127
|
// Add newPath to parent's directory listing.
|
|
121
128
|
let newDirNode, newDirList;
|
|
@@ -144,7 +151,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
144
151
|
}
|
|
145
152
|
else {
|
|
146
153
|
// If it's a directory, throw a permissions error.
|
|
147
|
-
throw
|
|
154
|
+
throw ErrnoError.With('EPERM', newPath, 'rename');
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
157
|
newDirList[newName] = nodeId;
|
|
@@ -170,11 +177,11 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
170
177
|
const tx = this.store.beginTransaction();
|
|
171
178
|
const inode = await this.findINode(tx, p);
|
|
172
179
|
if (!inode) {
|
|
173
|
-
throw
|
|
180
|
+
throw ErrnoError.With('ENOENT', p, 'stat');
|
|
174
181
|
}
|
|
175
182
|
const stats = inode.toStats();
|
|
176
183
|
if (!stats.hasAccess(R_OK, cred)) {
|
|
177
|
-
throw
|
|
184
|
+
throw ErrnoError.With('EACCES', p, 'stat');
|
|
178
185
|
}
|
|
179
186
|
return stats;
|
|
180
187
|
}
|
|
@@ -188,10 +195,10 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
188
195
|
await this.queueDone();
|
|
189
196
|
const tx = this.store.beginTransaction(), node = await this.findINode(tx, p), data = await tx.get(node.ino);
|
|
190
197
|
if (!node.toStats().hasAccess(flagToMode(flag), cred)) {
|
|
191
|
-
throw
|
|
198
|
+
throw ErrnoError.With('EACCES', p, 'openFile');
|
|
192
199
|
}
|
|
193
200
|
if (!data) {
|
|
194
|
-
throw
|
|
201
|
+
throw ErrnoError.With('ENOENT', p, 'openFile');
|
|
195
202
|
}
|
|
196
203
|
return new PreloadFile(this, p, flag, node.toStats(), data);
|
|
197
204
|
}
|
|
@@ -204,7 +211,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
204
211
|
// Check first if directory is empty.
|
|
205
212
|
const list = await this.readdir(p, cred);
|
|
206
213
|
if (list.length > 0) {
|
|
207
|
-
throw
|
|
214
|
+
throw ErrnoError.With('ENOTEMPTY', p, 'rmdir');
|
|
208
215
|
}
|
|
209
216
|
await this.removeEntry(p, true, cred);
|
|
210
217
|
}
|
|
@@ -218,7 +225,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
218
225
|
const tx = this.store.beginTransaction();
|
|
219
226
|
const node = await this.findINode(tx, p);
|
|
220
227
|
if (!node.toStats().hasAccess(R_OK, cred)) {
|
|
221
|
-
throw
|
|
228
|
+
throw ErrnoError.With('EACCES', p, 'readdur');
|
|
222
229
|
}
|
|
223
230
|
return Object.keys(await this.getDirListing(tx, node, p));
|
|
224
231
|
}
|
|
@@ -249,16 +256,16 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
249
256
|
await this.queueDone();
|
|
250
257
|
const tx = this.store.beginTransaction(), existingDir = dirname(existing), existingDirNode = await this.findINode(tx, existingDir);
|
|
251
258
|
if (!existingDirNode.toStats().hasAccess(R_OK, cred)) {
|
|
252
|
-
throw
|
|
259
|
+
throw ErrnoError.With('EACCES', existingDir, 'link');
|
|
253
260
|
}
|
|
254
261
|
const newDir = dirname(newpath), newDirNode = await this.findINode(tx, newDir), newListing = await this.getDirListing(tx, newDirNode, newDir);
|
|
255
262
|
if (!newDirNode.toStats().hasAccess(W_OK, cred)) {
|
|
256
|
-
throw
|
|
263
|
+
throw ErrnoError.With('EACCES', newDir, 'link');
|
|
257
264
|
}
|
|
258
265
|
const ino = await this._findINode(tx, existingDir, basename(existing));
|
|
259
266
|
const node = await this.getINode(tx, ino, existing);
|
|
260
267
|
if (!node.toStats().hasAccess(W_OK, cred)) {
|
|
261
|
-
throw
|
|
268
|
+
throw ErrnoError.With('EACCES', newpath, 'link');
|
|
262
269
|
}
|
|
263
270
|
node.nlink++;
|
|
264
271
|
newListing[basename(newpath)] = ino;
|
|
@@ -296,7 +303,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
296
303
|
async _findINode(tx, parent, filename, visited = new Set()) {
|
|
297
304
|
const currentPath = join(parent, filename);
|
|
298
305
|
if (visited.has(currentPath)) {
|
|
299
|
-
throw new
|
|
306
|
+
throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath);
|
|
300
307
|
}
|
|
301
308
|
visited.add(currentPath);
|
|
302
309
|
if (this._cache) {
|
|
@@ -325,7 +332,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
325
332
|
return id;
|
|
326
333
|
}
|
|
327
334
|
else {
|
|
328
|
-
throw
|
|
335
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
329
336
|
}
|
|
330
337
|
}
|
|
331
338
|
}
|
|
@@ -342,7 +349,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
342
349
|
return id;
|
|
343
350
|
}
|
|
344
351
|
else {
|
|
345
|
-
throw
|
|
352
|
+
throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode');
|
|
346
353
|
}
|
|
347
354
|
}
|
|
348
355
|
}
|
|
@@ -364,7 +371,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
364
371
|
async getINode(tx, id, p) {
|
|
365
372
|
const data = await tx.get(id);
|
|
366
373
|
if (!data) {
|
|
367
|
-
throw
|
|
374
|
+
throw ErrnoError.With('ENOENT', p, 'getINode');
|
|
368
375
|
}
|
|
369
376
|
return new Inode(data.buffer);
|
|
370
377
|
}
|
|
@@ -374,7 +381,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
374
381
|
*/
|
|
375
382
|
async getDirListing(tx, inode, p) {
|
|
376
383
|
if (!inode.toStats().isDirectory()) {
|
|
377
|
-
throw
|
|
384
|
+
throw ErrnoError.With('ENOTDIR', p, 'getDirListing');
|
|
378
385
|
}
|
|
379
386
|
const data = await tx.get(inode.ino);
|
|
380
387
|
if (!data) {
|
|
@@ -383,7 +390,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
383
390
|
than a directory listing. The latter should never occur unless
|
|
384
391
|
the file system is corrupted.
|
|
385
392
|
*/
|
|
386
|
-
throw
|
|
393
|
+
throw ErrnoError.With('ENOENT', p, 'getDirListing');
|
|
387
394
|
}
|
|
388
395
|
return decodeDirListing(data);
|
|
389
396
|
}
|
|
@@ -394,7 +401,7 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
394
401
|
async addNewNode(tx, data, _maxAttempts = 5) {
|
|
395
402
|
if (_maxAttempts <= 0) {
|
|
396
403
|
// Max retries hit. Return with an error.
|
|
397
|
-
throw new
|
|
404
|
+
throw new ErrnoError(Errno.EIO, 'Unable to commit data to key-value store.');
|
|
398
405
|
}
|
|
399
406
|
// Make an attempt
|
|
400
407
|
const ino = randomIno();
|
|
@@ -418,18 +425,18 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
418
425
|
const parentDir = dirname(p), fname = basename(p), parentNode = await this.findINode(tx, parentDir), dirListing = await this.getDirListing(tx, parentNode, parentDir);
|
|
419
426
|
//Check that the creater has correct access
|
|
420
427
|
if (!parentNode.toStats().hasAccess(W_OK, cred)) {
|
|
421
|
-
throw
|
|
428
|
+
throw ErrnoError.With('EACCES', p, 'commitNewFile');
|
|
422
429
|
}
|
|
423
430
|
// Invariant: The root always exists.
|
|
424
431
|
// If we don't check this prior to taking steps below, we will create a
|
|
425
432
|
// file with name '' in root should p == '/'.
|
|
426
433
|
if (p === '/') {
|
|
427
|
-
throw
|
|
434
|
+
throw ErrnoError.With('EEXIST', p, 'commitNewFile');
|
|
428
435
|
}
|
|
429
436
|
// Check if file already exists.
|
|
430
437
|
if (dirListing[fname]) {
|
|
431
438
|
await tx.abort();
|
|
432
|
-
throw
|
|
439
|
+
throw ErrnoError.With('EEXIST', p, 'commitNewFile');
|
|
433
440
|
}
|
|
434
441
|
try {
|
|
435
442
|
// Commit data.
|
|
@@ -468,21 +475,21 @@ export class AsyncStoreFS extends Async(FileSystem) {
|
|
|
468
475
|
}
|
|
469
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);
|
|
470
477
|
if (!parentListing[fileName]) {
|
|
471
|
-
throw
|
|
478
|
+
throw ErrnoError.With('ENOENT', p, 'removeEntry');
|
|
472
479
|
}
|
|
473
480
|
const fileIno = parentListing[fileName];
|
|
474
481
|
// Get file inode.
|
|
475
482
|
const fileNode = await this.getINode(tx, fileIno, p);
|
|
476
483
|
if (!fileNode.toStats().hasAccess(W_OK, cred)) {
|
|
477
|
-
throw
|
|
484
|
+
throw ErrnoError.With('EACCES', p, 'removeEntry');
|
|
478
485
|
}
|
|
479
486
|
// Remove from directory listing of parent.
|
|
480
487
|
delete parentListing[fileName];
|
|
481
488
|
if (!isDir && fileNode.toStats().isDirectory()) {
|
|
482
|
-
throw
|
|
489
|
+
throw ErrnoError.With('EISDIR', p, 'removeEntry');
|
|
483
490
|
}
|
|
484
491
|
if (isDir && !fileNode.toStats().isDirectory()) {
|
|
485
|
-
throw
|
|
492
|
+
throw ErrnoError.With('ENOTDIR', p, 'removeEntry');
|
|
486
493
|
}
|
|
487
494
|
try {
|
|
488
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
|
+
};
|
|
@@ -9,7 +9,7 @@ export declare class InMemoryStore implements SyncStore, SimpleSyncStore {
|
|
|
9
9
|
constructor(name?: string);
|
|
10
10
|
clear(): void;
|
|
11
11
|
beginTransaction(): SyncTransaction;
|
|
12
|
-
get(key: Ino): Uint8Array;
|
|
12
|
+
get(key: Ino): Uint8Array | undefined;
|
|
13
13
|
put(key: Ino, data: Uint8Array, overwrite: boolean): boolean;
|
|
14
14
|
remove(key: Ino): void;
|
|
15
15
|
}
|
package/dist/backends/Index.d.ts
CHANGED
|
@@ -68,25 +68,25 @@ export declare class FileIndex<TData> {
|
|
|
68
68
|
* @return The removed item,
|
|
69
69
|
* or null if it did not exist.
|
|
70
70
|
*/
|
|
71
|
-
remove(path: string): IndexInode<TData> |
|
|
71
|
+
remove(path: string): IndexInode<TData> | void;
|
|
72
72
|
/**
|
|
73
73
|
* Retrieves the directory listing of the given path.
|
|
74
74
|
* @return An array of files in the given path, or 'null' if it does not exist.
|
|
75
75
|
*/
|
|
76
|
-
ls(path: string): string[] |
|
|
76
|
+
ls(path: string): string[] | void;
|
|
77
77
|
/**
|
|
78
78
|
* Returns the inode of the given item.
|
|
79
79
|
* @return Returns null if the item does not exist.
|
|
80
80
|
*/
|
|
81
|
-
get(path: string): IndexInode<TData> | null;
|
|
81
|
+
get(path: string): IndexInode<TData> | void | null;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Generic interface for file/directory inodes.
|
|
85
85
|
* Note that Stats objects are what we use for file inodes.
|
|
86
86
|
*/
|
|
87
87
|
export declare abstract class IndexInode<TData> {
|
|
88
|
-
data?: TData;
|
|
89
|
-
constructor(data?: TData);
|
|
88
|
+
data?: TData | undefined;
|
|
89
|
+
constructor(data?: TData | undefined);
|
|
90
90
|
/**
|
|
91
91
|
* Whether this inode is for a file
|
|
92
92
|
*/
|
|
@@ -135,7 +135,7 @@ export declare class IndexDirInode<TData> extends IndexInode<TData> {
|
|
|
135
135
|
* Returns the inode for the indicated item, or null if it does not exist.
|
|
136
136
|
* @param path Name of item in this directory.
|
|
137
137
|
*/
|
|
138
|
-
get(path: string): IndexInode<TData> |
|
|
138
|
+
get(path: string): IndexInode<TData> | void;
|
|
139
139
|
/**
|
|
140
140
|
* Add the given item to the directory listing. Note that the given inode is
|
|
141
141
|
* not copied, and will be mutated by the DirInode if it is a DirInode.
|
|
@@ -151,7 +151,7 @@ export declare class IndexDirInode<TData> extends IndexInode<TData> {
|
|
|
151
151
|
* @return Returns the item
|
|
152
152
|
* removed, or null if the item did not exist.
|
|
153
153
|
*/
|
|
154
|
-
remove(p: string): IndexInode<TData> |
|
|
154
|
+
remove(p: string): IndexInode<TData> | void;
|
|
155
155
|
}
|
|
156
156
|
declare const IndexFS_base: (abstract new (...args: any[]) => {
|
|
157
157
|
metadata(): import("../filesystem.js").FileSystemMetadata;
|
|
@@ -173,9 +173,6 @@ declare const IndexFS_base: (abstract new (...args: any[]) => {
|
|
|
173
173
|
stat(path: string, cred: Cred): Promise<Stats>;
|
|
174
174
|
statSync(path: string, cred: Cred): Stats;
|
|
175
175
|
openFile(path: string, flag: string, cred: Cred): Promise<import("../file.js").File>;
|
|
176
|
-
/**
|
|
177
|
-
* Constructs a new FileIndex.
|
|
178
|
-
*/
|
|
179
176
|
openFileSync(path: string, flag: string, cred: Cred): import("../file.js").File;
|
|
180
177
|
readdir(path: string, cred: Cred): Promise<string[]>;
|
|
181
178
|
readdirSync(path: string, cred: Cred): string[];
|