UNPKG

fs-zoo

Version:

File system abstractions and implementations

339 lines (338 loc) 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeCrud = void 0; const util_1 = require("memfs/lib/node-to-fsa/util"); const util_2 = require("../crud/util"); const util_3 = require("../fsa-to-crud/util"); class NodeCrud { constructor(options) { this.options = options; this.put = async (path, data, options) => { const [collection, id] = (0, util_2.parseId)(path); (0, util_2.assertType)(collection, 'put', 'crudfs'); (0, util_1.assertName)(id, 'put', 'crudfs'); const dir = this.dir + (collection.length ? collection.join(this.separator) + this.separator : ''); const fs = this.fs; if (dir.length > 1) await fs.mkdir(dir, { recursive: true }); const filename = dir + id; if (typeof data === 'undefined') { const throwIf = options?.throwIf; // Determine existence and type let exists = false; try { const st = await fs.stat(filename); exists = st.isDirectory() ? 'dir' : st.isFile() ? 'file' : false; } catch (e) { if (!(e && typeof e === 'object' && e.code === 'ENOENT')) throw e; } switch (throwIf) { case 'exists': { if (exists) throw (0, util_3.newExistsError)(); await fs.mkdir(filename, { recursive: true }); return; } case 'missing': { if (exists !== 'dir') throw (0, util_3.newMissingError)(); return; // directory already there } default: { if (!exists) { await fs.mkdir(filename, { recursive: true }); } else if (exists === 'file') { // cannot replace file with directory under default behavior throw (0, util_3.newExistsError)(); } return; } } } const throwIf = options?.throwIf; let pos = options?.pos; if (pos === void 0) { if (throwIf) { try { const stats = await fs.stat(filename); if (throwIf === 'exists') throw (0, util_3.newExistsError)(); if (!stats.isFile()) throw (0, util_1.newNotAllowedError)(); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') { if (throwIf === 'missing') throw (0, util_3.newMissingError)(); } else throw error; } } await fs.writeFile(filename, data); return; } let handle; try { if (typeof pos !== 'number') throw new Error(`Invalid position: ${pos}`); // Use string flags compatible with both native fs and memfs. if (throwIf === 'exists') { // Create new file exclusively; fail if it already exists. try { handle = await fs.open(filename, 'wx'); } catch (error) { if (error && typeof error === 'object' && error.code === 'EEXIST') throw (0, util_3.newExistsError)(); throw error; } } else if (throwIf === 'missing') { // Open existing file for read/write; fail if missing. try { handle = await fs.open(filename, 'r+'); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') throw (0, util_3.newMissingError)(); throw error; } } else { // Default: open for read/write; if missing, create then reopen without truncation. try { handle = await fs.open(filename, 'r+'); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') { await fs.writeFile(filename, new Uint8Array()); handle = await fs.open(filename, 'r+'); } else { throw error; } } } if (pos === -1) { // Append: compute size without changing file content or position semantics const stats = await handle.stat(); pos = stats.size; } await handle.write(data, 0, data.byteLength, pos); } finally { await handle?.close(); } }; this.getStream = async (path) => { const [collection, id] = (0, util_2.parseId)(path); try { const handle = await this._file(collection, id, 0 /* FLAG.O_RDONLY */); const stream = handle.readableWebStream(); // const reader = await stream.getReader(); // console.log(await reader.read()); // console.log(stream); return stream; } catch (error) { if (error && typeof error === 'object') { switch (error.code) { case 'ENOENT': throw (0, util_3.newFile404Error)(collection, id); } } throw error; } }; this.del = async (path, silent) => { const [collection, id] = (0, util_2.parseId)(path); (0, util_2.assertType)(collection, 'del', 'crudfs'); (0, util_1.assertName)(id, 'del', 'crudfs'); try { const dir = await this._dir(collection); const filename = dir + id; await this.fs.unlink(filename); } catch (error) { if (!!silent) return; if (error && typeof error === 'object') { switch (error.code) { case 'ENOENT': throw (0, util_3.newFile404Error)(collection, id); } } throw error; } }; this.info = async (path) => { const [collection, id] = (0, util_2.parseId)(path); const isRootPath = !collection.length && !id; if (!isRootPath) { (0, util_2.assertType)(collection, 'info', 'crudfs'); (0, util_1.assertName)(id, 'info', 'crudfs'); } await this._dir(collection); if (isRootPath) { return { type: 'collection', id: '', readable: true, }; } try { // Build base dir path without introducing a double slash when collection is empty const base = this.dir + (collection.length ? collection.join(this.separator) + this.separator : ''); const fullPath = base + id; const stats = await this.fs.stat(fullPath); // Access mode constants (Node: F_OK=0, X_OK=1, W_OK=2, R_OK=4) const R_OK = 4; const check = async (mode) => { try { await this.fs.access(fullPath, mode); return true; } catch { return false; } }; if (stats.isFile()) { // Only perform a non-destructive readability check; some adapters may // implement write-access checks in a destructive way. const readable = await check(R_OK); return { type: 'resource', id, size: stats.size, modified: stats.mtimeMs, readable, }; } else if (stats.isDirectory()) { const readable = await check(R_OK); return { type: 'collection', id: '', readable, }; } else { throw (0, util_3.newMissingError)(); } } catch (error) { if (error && typeof error === 'object') { switch (error.code) { case 'ENOENT': throw (0, util_3.newFile404Error)(collection, id); } } throw error; } }; this.drop = async (path, silent) => { const collection = (0, util_2.parseParts)(path); (0, util_2.assertType)(collection, 'drop', 'crudfs'); try { const dir = await this._dir(collection); const isRoot = dir === this.dir; if (isRoot) { const list = (await this.fs.readdir(dir)); for (const entry of list) await this.fs.rmdir(dir + entry, { recursive: true }); } else { await this.fs.rmdir(dir, { recursive: true }); } } catch (error) { if (!silent) throw error; } }; this.scan = async function* (path) { const collection = (0, util_2.parseParts)(path); (0, util_2.assertType)(collection, 'scan', 'crudfs'); const dir = await this._dir(collection); const dirents = (await this.fs.readdir(dir, { withFileTypes: true })); for (const entry of dirents) { if (entry.isFile()) { yield { type: 'resource', id: '' + entry.name, }; } else if (entry.isDirectory()) { yield { type: 'collection', id: '' + entry.name, }; } } }; this.list = async (path) => { const entries = []; for await (const entry of this.scan(path)) entries.push(entry); return entries; }; this.from = async (path) => { const collection = (0, util_2.parseParts)(path); (0, util_2.assertType)(collection, 'from', 'crudfs'); const dir = this.dir + (collection.length ? collection.join(this.separator) + this.separator : ''); const fs = this.fs; if (dir.length > 1) await fs.mkdir(dir, { recursive: true }); await this._dir(collection); return new NodeCrud({ dir, fs: this.fs, separator: this.separator, }); }; this.separator = options.separator ?? '/'; let dir = options.dir; const last = dir[dir.length - 1]; if (last !== this.separator) dir = dir + this.separator; this.dir = dir; this.fs = options.fs; } async _dir(collection) { const dir = this.dir + (collection.length ? collection.join(this.separator) + this.separator : ''); // Avoid statting the root directory when collection is empty; some adapters // (e.g., FSA-backed) don't accept '/' if (!collection.length) return dir; const fs = this.fs; try { const stats = await fs.stat(dir); if (!stats.isDirectory()) throw (0, util_3.newFolder404Error)(collection); return dir; } catch (error) { if (error && typeof error === 'object') { switch (error.code) { case 'ENOENT': case 'ENOTDIR': throw (0, util_3.newFolder404Error)(collection); } } throw error; } } async _file(collection, id, flags) { (0, util_2.assertType)(collection, 'get', 'crudfs'); (0, util_1.assertName)(id, 'get', 'crudfs'); const dir = await this._dir(collection); const filename = dir + id; const fs = this.fs; return await fs.open(filename, flags); } } exports.NodeCrud = NodeCrud;