@zenfs/core
Version:
A filesystem, anywhere
129 lines (128 loc) • 3.75 kB
JavaScript
import { Errno, ErrnoError } from '../error.js';
import { IndexFS } from './file_index.js';
async function fetchFile(path, type) {
const response = await fetch(path).catch((e) => {
throw new ErrnoError(Errno.EIO, e.message, path);
});
if (!response.ok) {
throw new ErrnoError(Errno.EIO, 'fetch failed: response returned code ' + response.status, path);
}
switch (type) {
case 'buffer': {
const arrayBuffer = await response.arrayBuffer().catch((e) => {
throw new ErrnoError(Errno.EIO, e.message, path);
});
return new Uint8Array(arrayBuffer);
}
case 'json':
return response.json().catch((e) => {
throw new ErrnoError(Errno.EIO, e.message, path);
});
default:
throw new ErrnoError(Errno.EINVAL, 'Invalid download type: ' + type);
}
}
/**
* A simple filesystem backed by HTTP using the `fetch` API.
*
*
* Index objects look like the following:
*
* ```json
* {
* "version": 1,
* "entries": {
* "/home": { ... },
* "/home/jvilk": { ... },
* "/home/james": { ... }
* }
* }
* ```
*
* Each entry contains the stats associated with the file.
*/
export class FetchFS extends IndexFS {
async ready() {
if (this._isInitialized) {
return;
}
await super.ready();
if (this._disableSync) {
return;
}
/**
* Iterate over all of the files and cache their contents
*/
for (const [path, stats] of this.index.files()) {
await this.getData(path, stats);
}
}
constructor({ index = 'index.json', baseUrl = '' }) {
// prefix url must end in a directory separator.
if (baseUrl.at(-1) != '/') {
baseUrl += '/';
}
super(typeof index != 'string' ? index : fetchFile(baseUrl + index, 'json'));
this.baseUrl = baseUrl;
}
metadata() {
return {
...super.metadata(),
name: FetchFS.name,
readonly: true,
};
}
/**
* Preload the `path` into the index.
*/
preload(path, buffer) {
const stats = this.index.get(path);
if (!stats) {
throw ErrnoError.With('ENOENT', path, 'preload');
}
if (!stats.isFile()) {
throw ErrnoError.With('EISDIR', path, 'preload');
}
stats.size = buffer.length;
stats.fileData = buffer;
}
/**
* @todo Be lazier about actually requesting the data?
*/
async getData(path, stats) {
if (stats.fileData) {
return stats.fileData;
}
const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
stats.fileData = data;
return data;
}
getDataSync(path, stats) {
if (stats.fileData) {
return stats.fileData;
}
throw new ErrnoError(Errno.ENODATA, '', path, 'getData');
}
}
const _Fetch = {
name: 'Fetch',
options: {
index: {
type: ['string', 'object'],
required: false,
description: 'URL to a file index as a JSON file or the file index object itself, generated with the make-index script. Defaults to `index.json`.',
},
baseUrl: {
type: 'string',
required: false,
description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
},
},
isAvailable() {
return typeof globalThis.fetch == 'function';
},
create(options) {
return new FetchFS(options);
},
};
export const Fetch = _Fetch;