UNPKG

@zenfs/core

Version:

A filesystem, anywhere

139 lines (138 loc) 5.24 kB
// SPDX-License-Identifier: LGPL-3.0-or-later import { withErrno } from 'kerium'; import { err, warn } from 'kerium/log'; import { decodeUTF8 } from 'utilium'; import * as requests from 'utilium/requests'; import { Index } from '../internal/file_index.js'; import { IndexFS } from '../internal/index_fs.js'; import { normalizePath } from '../utils.js'; import { S_IFREG } from '../constants.js'; /** Parse and throw */ function parseError(error) { if (!('tag' in error)) throw err(withErrno('EIO', error.stack)); switch (error.tag) { case 'fetch': throw err(withErrno('EREMOTEIO', error.message)); case 'status': throw err(withErrno(error.response.status > 500 ? 'EREMOTEIO' : 'EIO', 'Response status code is ' + error.response.status)); case 'size': throw err(withErrno('EBADE', error.message)); case 'buffer': throw err(withErrno('EIO', 'Failed to decode buffer')); } } /** * A simple filesystem backed by HTTP using the `fetch` API. * @category Internals * @internal */ export class FetchFS extends IndexFS { baseUrl; requestInit; remoteWrite; /** * @internal @hidden */ _asyncDone = Promise.resolve(); _async(p) { this._asyncDone = this._asyncDone.then(() => p); } constructor(index, baseUrl, requestInit = {}, remoteWrite) { super(0x206e6673, 'nfs', index); this.baseUrl = baseUrl; this.requestInit = requestInit; this.remoteWrite = remoteWrite; } async remove(path) { await requests.remove(this.baseUrl + path, { warn, cacheOnly: !this.remoteWrite }, this.requestInit); } removeSync(path) { this._async(requests.remove(this.baseUrl + path, { warn, cacheOnly: !this.remoteWrite }, this.requestInit)); } async read(path, buffer, offset = 0, end) { const inode = this.index.get(path); if (!inode) throw withErrno('ENOENT'); if (end - offset == 0) return; const data = await requests .get(this.baseUrl + path, { start: offset, end, size: inode.size, warn }, this.requestInit) .catch(parseError) .catch(() => undefined); if (!data) throw withErrno('ENODATA'); buffer.set(data); } readSync(path, buffer, offset = 0, end) { const inode = this.index.get(path); if (!inode) throw withErrno('ENOENT'); if (end - offset == 0) return; const { data, missing } = requests.getCached(this.baseUrl + path, { start: offset, end, size: inode.size, warn }); if (!data) throw withErrno('ENODATA'); if (missing.length) { this._async(requests.get(this.baseUrl + path, { start: offset, end, size: inode.size, warn })); throw withErrno('EAGAIN'); } buffer.set(data); } async write(path, data, offset) { const inode = this.index.get(path); if (!inode) throw withErrno('ENOENT'); inode.update({ mtimeMs: Date.now(), size: Math.max(inode.size, data.byteLength + offset) }); await requests.set(this.baseUrl + path, data, { offset, warn, cacheOnly: !this.remoteWrite }, this.requestInit).catch(parseError); } writeSync(path, data, offset) { const inode = this.index.get(path); if (!inode) throw withErrno('ENOENT'); inode.update({ mtimeMs: Date.now(), size: Math.max(inode.size, data.byteLength + offset) }); this._async(requests.set(this.baseUrl + path, data, { offset, warn, cacheOnly: !this.remoteWrite }, this.requestInit).catch(parseError)); } } const _Fetch = { name: 'Fetch', options: { index: { type: ['string', 'object'], required: false }, baseUrl: { type: 'string', required: true }, requestInit: { type: 'object', required: false }, remoteWrite: { type: 'boolean', required: false }, }, isAvailable() { return typeof globalThis.fetch == 'function'; }, async create(options) { const url = new URL(options.baseUrl); url.pathname = normalizePath(url.pathname); let baseUrl = url.toString(); if (baseUrl.at(-1) == '/') baseUrl = baseUrl.slice(0, -1); options.index ??= 'index.json'; const index = new Index(); if (typeof options.index != 'string') { index.fromJSON(options.index); } else { const data = await requests.get(options.index, { warn }, options.requestInit).catch(parseError); index.fromJSON(JSON.parse(decodeUTF8(data))); } const fs = new FetchFS(index, baseUrl, options.requestInit, options.remoteWrite); if (options.disableAsyncCache) return fs; // Iterate over all of the files and cache their contents for (const [path, node] of index) { if (!(node.mode & S_IFREG)) continue; await requests.get(baseUrl + path, { warn }, options.requestInit).catch(parseError); } return fs; }, }; /** * @category Backends and Configuration */ export const Fetch = _Fetch;