UNPKG

@zenfs/core

Version:

A filesystem, anywhere

1,103 lines (1,102 loc) 42.4 kB
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); // SPDX-License-Identifier: LGPL-3.0-or-later import { withErrno } from 'kerium'; import { crit, debug, err, notice, warn } from 'kerium/log'; import { sizeof } from 'memium'; import { _throw, canary, encodeUTF8 } from 'utilium'; import { extendBuffer } from 'utilium/buffer'; import { Index } from '../../internal/file_index.js'; import { FileSystem } from '../../internal/filesystem.js'; import { Inode, isDirectory, isFile, rootIno } from '../../internal/inode.js'; import { basename, dirname, join, parse, relative } from '../../path.js'; import { decodeDirListing, encodeDirListing } from '../../utils.js'; import { S_IFDIR, S_IFREG, size_max } from '../../constants.js'; import { WrappedTransaction } from './store.js'; /** * A file system which uses a `Store` * * @todo Check modes? * @category Stores and Transactions * @internal */ export class StoreFS extends FileSystem { store; /** * A map of paths to inode IDs * @internal @hidden */ _ids = new Map([['/', 0]]); /** * A map of inode IDs to paths * @internal @hidden */ _paths = new Map([[0, new Set('/')]]); /** * Gets the first path associated with an inode */ _path(id) { const [path] = this._paths.get(id) ?? []; return path; } /** * Add a inode/path pair */ _add(ino, path) { if (!this._paths.has(ino)) this._paths.set(ino, new Set()); this._paths.get(ino).add(path); this._ids.set(path, ino); } /** * Remove a inode/path pair */ _remove(ino) { for (const path of this._paths.get(ino) ?? []) { this._ids.delete(path); } this._paths.delete(ino); } /** * Move paths in the tables */ _move(from, to) { const toMove = []; for (const [path, ino] of this._ids) { const rel = relative(from, path); if (rel.startsWith('..')) continue; let newKey = join(to, rel); if (newKey.endsWith('/')) newKey = newKey.slice(0, -1); toMove.push({ oldKey: path, newKey, ino }); } for (const { oldKey, newKey, ino } of toMove) { this._ids.delete(oldKey); this._ids.set(newKey, ino); const p = this._paths.get(ino); if (!p) { warn('Missing paths in table for ino ' + ino); continue; } p.delete(oldKey); p.add(newKey); } } _initialized = false; async ready() { if (this._initialized) return; if (!this.attributes.has('no_async_preload')) { this.checkRootSync(); } await this.checkRoot(); await this._populate(); this._initialized = true; } readySync() { if (this._initialized) return; if (!this.attributes.has('no_async_preload')) { this.checkRootSync(); } this.checkRootSync(); this._populateSync(); this._initialized = true; } constructor(store) { super(store.type ?? 0x6b766673, store.name); this.store = store; store.fs = this; this._uuid = store.uuid ?? this.uuid; this.label = store.label; debug(this.name + ': supports features: ' + this.store.flags?.join(', ')); } /** * @experimental */ usage() { return (this.store.usage?.() || { totalSpace: 0, freeSpace: 0, }); } /** * Load an index into the StoreFS. * You *must* manually add non-directory files */ async loadIndex(index) { const env_1 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_1, this.transaction(), true); const dirs = index.directories(); for (const [path, inode] of index) { this._add(inode.ino, path); await tx.set(inode.ino, inode); if (dirs.has(path)) await tx.set(inode.data, encodeDirListing(dirs.get(path))); } await tx.commit(); } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { const result_1 = __disposeResources(env_1); if (result_1) await result_1; } } /** * Load an index into the StoreFS. * You *must* manually add non-directory files */ loadIndexSync(index) { const env_2 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_2, this.transaction(), false); const dirs = index.directories(); for (const [path, inode] of index) { this._add(inode.ino, path); tx.setSync(inode.ino, inode); if (dirs.has(path)) tx.setSync(inode.data, encodeDirListing(dirs.get(path))); } tx.commitSync(); } catch (e_2) { env_2.error = e_2; env_2.hasError = true; } finally { __disposeResources(env_2); } } async createIndex() { const env_3 = { stack: [], error: void 0, hasError: false }; try { const index = new Index(); const tx = __addDisposableResource(env_3, this.transaction(), true); const queue = [['/', 0]]; const silence = canary(withErrno('EDEADLK')); while (queue.length) { const [path, ino] = queue.shift(); const inode = new Inode(await tx.get(ino)); index.set(path, inode); if (inode.mode & S_IFDIR) { const dir = decodeDirListing((await tx.get(inode.data)) ?? _throw(withErrno('ENODATA'))); for (const [name, id] of Object.entries(dir)) { queue.push([join(path, name), id]); } } } silence(); return index; } catch (e_3) { env_3.error = e_3; env_3.hasError = true; } finally { const result_2 = __disposeResources(env_3); if (result_2) await result_2; } } createIndexSync() { const env_4 = { stack: [], error: void 0, hasError: false }; try { const index = new Index(); const tx = __addDisposableResource(env_4, this.transaction(), false); const queue = [['/', 0]]; const silence = canary(withErrno('EDEADLK')); while (queue.length) { const [path, ino] = queue.shift(); const inode = new Inode(tx.getSync(ino)); index.set(path, inode); if (inode.mode & S_IFDIR) { const dir = decodeDirListing(tx.getSync(inode.data) ?? _throw(withErrno('ENODATA'))); for (const [name, id] of Object.entries(dir)) { queue.push([join(path, name), id]); } } } silence(); return index; } catch (e_4) { env_4.error = e_4; env_4.hasError = true; } finally { __disposeResources(env_4); } } /** * @todo Make rename compatible with the cache. */ async rename(oldPath, newPath) { const env_5 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_5, this.transaction(), true); const _old = parse(oldPath), _new = parse(newPath), // Remove oldPath from parent's directory listing. oldDirNode = await this.findInode(tx, _old.dir), oldDirList = decodeDirListing((await tx.get(oldDirNode.data)) ?? _throw(withErrno('ENODATA'))); if (!oldDirList[_old.base]) throw withErrno('ENOENT'); const ino = oldDirList[_old.base]; if (ino != this._ids.get(oldPath)) err(`Ino mismatch while renaming ${oldPath} to ${newPath}`); delete oldDirList[_old.base]; /* Can't move a folder inside itself. This ensures that the check passes only if `oldPath` is a subpath of `_new.dir`. We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path. */ if ((_new.dir + '/').startsWith(oldPath + '/')) throw withErrno('EBUSY'); const sameParent = _new.dir == _old.dir; // Prevent us from re-grabbing the same directory listing, which still contains `old_path.base.` const newDirNode = sameParent ? oldDirNode : await this.findInode(tx, _new.dir); const newDirList = sameParent ? oldDirList : decodeDirListing((await tx.get(newDirNode.data)) ?? _throw(withErrno('ENODATA'))); if (newDirList[_new.base]) { const existing = new Inode((await tx.get(newDirList[_new.base])) ?? _throw(withErrno('ENOENT'))); if (!isFile(existing)) throw withErrno('EISDIR'); await tx.remove(existing.data); await tx.remove(newDirList[_new.base]); } newDirList[_new.base] = ino; // Commit the two changed directory listings. await tx.set(oldDirNode.data, encodeDirListing(oldDirList)); await tx.set(newDirNode.data, encodeDirListing(newDirList)); await tx.commit(); this._move(oldPath, newPath); } catch (e_5) { env_5.error = e_5; env_5.hasError = true; } finally { const result_3 = __disposeResources(env_5); if (result_3) await result_3; } } renameSync(oldPath, newPath) { const env_6 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_6, this.transaction(), false); const _old = parse(oldPath), _new = parse(newPath), // Remove oldPath from parent's directory listing. oldDirNode = this.findInodeSync(tx, _old.dir), oldDirList = decodeDirListing(tx.getSync(oldDirNode.data) ?? _throw(withErrno('ENODATA'))); if (!oldDirList[_old.base]) throw withErrno('ENOENT'); const ino = oldDirList[_old.base]; if (ino != this._ids.get(oldPath)) err(`Ino mismatch while renaming ${oldPath} to ${newPath}`); delete oldDirList[_old.base]; /* Can't move a folder inside itself. This ensures that the check passes only if `oldPath` is a subpath of `_new.dir`. We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path. */ if ((_new.dir + '/').startsWith(oldPath + '/')) throw withErrno('EBUSY'); // Add newPath to parent's directory listing. const sameParent = _new.dir === _old.dir; // Prevent us from re-grabbing the same directory listing, which still contains `old_path.base.` const newDirNode = sameParent ? oldDirNode : this.findInodeSync(tx, _new.dir); const newDirList = sameParent ? oldDirList : decodeDirListing(tx.getSync(newDirNode.data) ?? _throw(withErrno('ENODATA'))); if (newDirList[_new.base]) { const existing = new Inode(tx.getSync(newDirList[_new.base]) ?? _throw(withErrno('ENOENT'))); if (!isFile(existing)) throw withErrno('EISDIR'); tx.removeSync(existing.data); tx.removeSync(newDirList[_new.base]); } newDirList[_new.base] = ino; // Commit the two changed directory listings. tx.setSync(oldDirNode.data, encodeDirListing(oldDirList)); tx.setSync(newDirNode.data, encodeDirListing(newDirList)); tx.commitSync(); this._move(oldPath, newPath); } catch (e_6) { env_6.error = e_6; env_6.hasError = true; } finally { __disposeResources(env_6); } } async stat(path) { const env_7 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_7, this.transaction(), true); return await this.findInode(tx, path); } catch (e_7) { env_7.error = e_7; env_7.hasError = true; } finally { const result_4 = __disposeResources(env_7); if (result_4) await result_4; } } statSync(path) { const env_8 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_8, this.transaction(), false); return this.findInodeSync(tx, path); } catch (e_8) { env_8.error = e_8; env_8.hasError = true; } finally { __disposeResources(env_8); } } async touch(path, metadata) { const env_9 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_9, this.transaction(), true); const inode = await this.findInode(tx, path); if (inode.update(metadata)) { this._add(inode.ino, path); tx.setSync(inode.ino, inode); } await tx.commit(); } catch (e_9) { env_9.error = e_9; env_9.hasError = true; } finally { const result_5 = __disposeResources(env_9); if (result_5) await result_5; } } touchSync(path, metadata) { const env_10 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_10, this.transaction(), false); const inode = this.findInodeSync(tx, path); if (inode.update(metadata)) { this._add(inode.ino, path); tx.setSync(inode.ino, inode); } tx.commitSync(); } catch (e_10) { env_10.error = e_10; env_10.hasError = true; } finally { __disposeResources(env_10); } } async createFile(path, options) { options.mode |= S_IFREG; return await this.commitNew(path, options, new Uint8Array()); } createFileSync(path, options) { options.mode |= S_IFREG; return this.commitNewSync(path, options, new Uint8Array()); } async unlink(path) { return this.remove(path, false); } unlinkSync(path) { this.removeSync(path, false); } async rmdir(path) { if ((await this.readdir(path)).length) throw withErrno('ENOTEMPTY'); await this.remove(path, true); } rmdirSync(path) { if (this.readdirSync(path).length) throw withErrno('ENOTEMPTY'); this.removeSync(path, true); } async mkdir(path, options) { options.mode |= S_IFDIR; return await this.commitNew(path, options, encodeUTF8('{}')); } mkdirSync(path, options) { options.mode |= S_IFDIR; return this.commitNewSync(path, options, encodeUTF8('{}')); } async readdir(path) { const env_11 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_11, this.transaction(), true); const node = await this.findInode(tx, path); return Object.keys(decodeDirListing((await tx.get(node.data)) ?? _throw(withErrno('ENOENT')))); } catch (e_11) { env_11.error = e_11; env_11.hasError = true; } finally { const result_6 = __disposeResources(env_11); if (result_6) await result_6; } } readdirSync(path) { const env_12 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_12, this.transaction(), false); const node = this.findInodeSync(tx, path); return Object.keys(decodeDirListing(tx.getSync(node.data) ?? _throw(withErrno('ENOENT')))); } catch (e_12) { env_12.error = e_12; env_12.hasError = true; } finally { __disposeResources(env_12); } } /** * Updated the inode and data node at `path` */ async sync() { } /** * Updated the inode and data node at `path` */ syncSync() { } async link(target, link) { const env_13 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_13, this.transaction(), true); const newDir = dirname(link), newDirNode = await this.findInode(tx, newDir), listing = decodeDirListing((await tx.get(newDirNode.data)) ?? _throw(withErrno('ENOENT'))); const inode = await this.findInode(tx, target); inode.nlink++; listing[basename(link)] = inode.ino; this._add(inode.ino, link); await tx.set(inode.ino, inode); await tx.set(newDirNode.data, encodeDirListing(listing)); await tx.commit(); } catch (e_13) { env_13.error = e_13; env_13.hasError = true; } finally { const result_7 = __disposeResources(env_13); if (result_7) await result_7; } } linkSync(target, link) { const env_14 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_14, this.transaction(), false); const newDir = dirname(link), newDirNode = this.findInodeSync(tx, newDir), listing = decodeDirListing(tx.getSync(newDirNode.data) ?? _throw(withErrno('ENOENT'))); const inode = this.findInodeSync(tx, target); inode.nlink++; listing[basename(link)] = inode.ino; this._add(inode.ino, link); tx.setSync(inode.ino, inode); tx.setSync(newDirNode.data, encodeDirListing(listing)); tx.commitSync(); } catch (e_14) { env_14.error = e_14; env_14.hasError = true; } finally { __disposeResources(env_14); } } async read(path, buffer, offset, end) { const env_15 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_15, this.transaction(), true); const inode = await this.findInode(tx, path); if (inode.size == 0) return; const data = (await tx.get(inode.data, offset, end)) ?? _throw(withErrno('ENODATA')); const _ = tx.flag('partial') ? data : data.subarray(offset, end); if (_.byteLength > buffer.byteLength) err(`Trying to place ${_.byteLength} bytes into a ${buffer.byteLength} byte buffer on read`); buffer.set(_); } catch (e_15) { env_15.error = e_15; env_15.hasError = true; } finally { const result_8 = __disposeResources(env_15); if (result_8) await result_8; } } readSync(path, buffer, offset, end) { const env_16 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_16, this.transaction(), false); const inode = this.findInodeSync(tx, path); if (inode.size == 0) return; const data = tx.getSync(inode.data, offset, end) ?? _throw(withErrno('ENODATA')); const _ = tx.flag('partial') ? data : data.subarray(offset, end); if (_.byteLength > buffer.byteLength) err(`Trying to place ${_.byteLength} bytes into a ${buffer.byteLength} byte buffer on read`); buffer.set(_); } catch (e_16) { env_16.error = e_16; env_16.hasError = true; } finally { __disposeResources(env_16); } } async write(path, data, offset) { const env_17 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_17, this.transaction(), true); const inode = await this.findInode(tx, path); let buffer = data; if (!tx.flag('partial')) { buffer = extendBuffer((await tx.get(inode.data)) ?? new Uint8Array(), offset + data.byteLength); buffer.set(data, offset); offset = 0; } await tx.set(inode.data, buffer, offset); this._add(inode.ino, path); await tx.commit(); } catch (e_17) { env_17.error = e_17; env_17.hasError = true; } finally { const result_9 = __disposeResources(env_17); if (result_9) await result_9; } } writeSync(path, data, offset) { const env_18 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_18, this.transaction(), false); const inode = this.findInodeSync(tx, path); let buffer = data; if (!tx.flag('partial')) { buffer = extendBuffer(tx.getSync(inode.data) ?? new Uint8Array(), offset + data.byteLength); buffer.set(data, offset); offset = 0; } tx.setSync(inode.data, buffer, offset); this._add(inode.ino, path); tx.commitSync(); } catch (e_18) { env_18.error = e_18; env_18.hasError = true; } finally { __disposeResources(env_18); } } /** * Wraps a transaction * @internal @hidden */ transaction() { return new WrappedTransaction(this.store.transaction(), this); } /** * Checks if the root directory exists. Creates it if it doesn't. */ async checkRoot() { const env_19 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_19, this.transaction(), true); if (await tx.get(rootIno)) return; const inode = new Inode({ ino: rootIno, data: 1, mode: 0o777 | S_IFDIR }); await tx.set(inode.data, encodeUTF8('{}')); this._add(rootIno, '/'); await tx.set(rootIno, inode); await tx.commit(); } catch (e_19) { env_19.error = e_19; env_19.hasError = true; } finally { const result_10 = __disposeResources(env_19); if (result_10) await result_10; } } /** * Checks if the root directory exists. Creates it if it doesn't. */ checkRootSync() { const env_20 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_20, this.transaction(), false); if (tx.getSync(rootIno)) return; const inode = new Inode({ ino: rootIno, data: 1, mode: 0o777 | S_IFDIR }); tx.setSync(inode.data, encodeUTF8('{}')); this._add(rootIno, '/'); tx.setSync(rootIno, inode); tx.commitSync(); } catch (e_20) { env_20.error = e_20; env_20.hasError = true; } finally { __disposeResources(env_20); } } /** * Populates the `_ids` and `_paths` maps with all existing files stored in the underlying `Store`. */ async _populate() { const env_21 = { stack: [], error: void 0, hasError: false }; try { if (this._initialized) { warn('Attempted to populate tables after initialization'); return; } debug('Populating tables with existing store metadata'); const tx = __addDisposableResource(env_21, this.transaction(), true); const rootData = await tx.get(rootIno); if (!rootData) { notice('Store does not have a root inode'); const inode = new Inode({ ino: rootIno, data: 1, mode: 0o777 | S_IFDIR }); await tx.set(inode.data, encodeUTF8('{}')); this._add(rootIno, '/'); await tx.set(rootIno, inode); await tx.commit(); return; } if (rootData.length < sizeof(Inode)) { crit('Store contains an invalid root inode. Refusing to populate tables'); return; } // Keep track of directories we have already traversed to avoid loops const visitedDirectories = new Set(); let i = 0; // Start BFS from root const queue = [['/', rootIno]]; while (queue.length > 0) { i++; const [path, ino] = queue.shift(); this._add(ino, path); // Get the inode data from the store const inodeData = await tx.get(ino); if (!inodeData) { warn('Store is missing data for inode: ' + ino); continue; } if (inodeData.length < sizeof(Inode)) { warn(`Invalid inode size for ino ${ino}: ${inodeData.length}`); continue; } // Parse the raw data into our Inode object const inode = new Inode(inodeData); // If it is a directory and not yet visited, read its directory listing if ((inode.mode & S_IFDIR) != S_IFDIR || visitedDirectories.has(ino)) { continue; } visitedDirectories.add(ino); // Grab the directory listing from the store const dirData = await tx.get(inode.data); if (!dirData) { warn('Store is missing directory data: ' + inode.data); continue; } const dirListing = decodeDirListing(dirData); for (const [entryName, childIno] of Object.entries(dirListing)) { queue.push([join(path, entryName), childIno]); } } debug(`Added ${i} existing inode(s) from store`); } catch (e_21) { env_21.error = e_21; env_21.hasError = true; } finally { const result_11 = __disposeResources(env_21); if (result_11) await result_11; } } _populateSync() { const env_22 = { stack: [], error: void 0, hasError: false }; try { if (this._initialized) { warn('Attempted to populate tables after initialization'); return; } debug('Populating tables with existing store metadata'); const tx = __addDisposableResource(env_22, this.transaction(), false); const rootData = tx.getSync(rootIno); if (!rootData) { notice('Store does not have a root inode'); const inode = new Inode({ ino: rootIno, data: 1, mode: 0o777 | S_IFDIR }); tx.setSync(inode.data, encodeUTF8('{}')); this._add(rootIno, '/'); tx.setSync(rootIno, inode); tx.commitSync(); return; } if (rootData.length < sizeof(Inode)) { crit('Store contains an invalid root inode. Refusing to populate tables'); return; } const visitedDirectories = new Set(); let i = 0; const queue = [['/', rootIno]]; while (queue.length > 0) { i++; const [path, ino] = queue.shift(); this._add(ino, path); const inodeData = tx.getSync(ino); if (!inodeData) { warn('Store is missing data for inode: ' + ino); continue; } if (inodeData.length < sizeof(Inode)) { warn(`Invalid inode size for ino ${ino}: ${inodeData.length}`); continue; } const inode = new Inode(inodeData); if ((inode.mode & S_IFDIR) != S_IFDIR || visitedDirectories.has(ino)) { continue; } visitedDirectories.add(ino); const dirData = tx.getSync(inode.data); if (!dirData) { warn('Store is missing directory data: ' + inode.data); continue; } const dirListing = decodeDirListing(dirData); for (const [entryName, childIno] of Object.entries(dirListing)) { queue.push([join(path, entryName), childIno]); } } debug(`Added ${i} existing inode(s) from store`); } catch (e_22) { env_22.error = e_22; env_22.hasError = true; } finally { __disposeResources(env_22); } } /** * Find an inode without using the ID tables */ async _findInode(tx, path, visited = new Set()) { if (visited.has(path)) throw crit(withErrno('EIO', 'Infinite loop detected while finding inode')); visited.add(path); if (path == '/') return new Inode((await tx.get(rootIno)) ?? _throw(withErrno('ENODATA'))); const { dir: parent, base: filename } = parse(path); const inode = await this._findInode(tx, parent, visited); const dirList = decodeDirListing((await tx.get(inode.data)) ?? _throw(withErrno('ENODATA'))); if (!(filename in dirList)) throw withErrno('ENOENT'); return new Inode((await tx.get(dirList[filename])) ?? _throw(withErrno('ENODATA'))); } /** * Find an inode without using the ID tables */ _findInodeSync(tx, path, visited = new Set()) { if (visited.has(path)) throw crit(withErrno('EIO', 'Infinite loop detected while finding inode')); visited.add(path); if (path == '/') return new Inode(tx.getSync(rootIno) ?? _throw(withErrno('ENOENT'))); const { dir: parent, base: filename } = parse(path); const inode = this._findInodeSync(tx, parent, visited); const dir = decodeDirListing(tx.getSync(inode.data) ?? _throw(withErrno('ENODATA'))); if (!(filename in dir)) throw withErrno('ENOENT'); return new Inode(tx.getSync(dir[filename]) ?? _throw(withErrno('ENODATA'))); } /** * Finds the Inode of `path`. * @param path The path to look up. * @todo memoize/cache */ async findInode(tx, path) { if (this.attributes.has('no_id_tables')) return await this._findInode(tx, path); const ino = this._ids.get(path); if (ino === undefined) throw withErrno('ENOENT'); return new Inode((await tx.get(ino)) ?? _throw(withErrno('ENOENT'))); } /** * Finds the Inode of `path`. * @param path The path to look up. * @return The Inode of the path p. * @todo memoize/cache */ findInodeSync(tx, path) { if (this.attributes.has('no_id_tables')) return this._findInodeSync(tx, path); const ino = this._ids.get(path); if (ino === undefined) throw withErrno('ENOENT'); return new Inode(tx.getSync(ino) ?? _throw(withErrno('ENOENT'))); } _lastID; /** Allocates a new ID and adds the ID/path */ allocNew(path) { this._lastID ??= Math.max(...this._paths.keys()); this._lastID += 2; const id = this._lastID; if (id > size_max) throw err(withErrno('ENOSPC', 'No IDs available')); this._add(id, path); return id; } /** * Commits a new file (well, a FILE or a DIRECTORY) to the file system with `mode`. * Note: This will commit the transaction. * @param path The path to the new file. * @param options The options to create the new file with. * @param data The data to store at the file's data node. */ async commitNew(path, options, data) { const env_23 = { stack: [], error: void 0, hasError: false }; try { /* The root always exists. If we don't check this prior to taking steps below, we will create a file with name '' in root if path is '/'. */ if (path == '/') throw withErrno('EEXIST'); const tx = __addDisposableResource(env_23, this.transaction(), true); const { dir: parentPath, base: fname } = parse(path); const parent = await this.findInode(tx, parentPath); const listing = decodeDirListing((await tx.get(parent.data)) ?? _throw(withErrno('ENOENT'))); // Check if file already exists. if (listing[fname]) throw withErrno('EEXIST'); const id = this.allocNew(path); // Commit data. const inode = new Inode({ ...options, ino: id, data: id + 1, size: data.byteLength, nlink: 1, }); await tx.set(inode.ino, inode); await tx.set(inode.data, data); // Update and commit parent directory listing. listing[fname] = inode.ino; await tx.set(parent.data, encodeDirListing(listing)); await tx.commit(); return inode; } catch (e_23) { env_23.error = e_23; env_23.hasError = true; } finally { const result_12 = __disposeResources(env_23); if (result_12) await result_12; } } /** * Commits a new file (well, a FILE or a DIRECTORY) to the file system with `mode`. * Note: This will commit the transaction. * @param path The path to the new file. * @param options The options to create the new file with. * @param data The data to store at the file's data node. * @return The Inode for the new file. */ commitNewSync(path, options, data) { const env_24 = { stack: [], error: void 0, hasError: false }; try { /* The root always exists. If we don't check this prior to taking steps below, we will create a file with name '' in root if path is '/'. */ if (path == '/') throw withErrno('EEXIST'); const tx = __addDisposableResource(env_24, this.transaction(), false); const { dir: parentPath, base: fname } = parse(path); const parent = this.findInodeSync(tx, parentPath); const listing = decodeDirListing(tx.getSync(parent.data) ?? _throw(withErrno('ENOENT'))); if (listing[fname]) throw withErrno('EEXIST'); const id = this.allocNew(path); const inode = new Inode({ ...options, ino: id, data: id + 1, size: data.byteLength, nlink: 1, }); // Update and commit parent directory listing. tx.setSync(inode.ino, inode); tx.setSync(inode.data, data); listing[fname] = inode.ino; tx.setSync(parent.data, encodeDirListing(listing)); tx.commitSync(); return inode; } catch (e_24) { env_24.error = e_24; env_24.hasError = true; } finally { __disposeResources(env_24); } } /** * Remove all traces of `path` from the file system. * @param path The path to remove from the file system. * @param isDir Does the path belong to a directory, or a file? */ async remove(path, isDir) { const env_25 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_25, this.transaction(), true); const { dir: parent, base: fileName } = parse(path), parentNode = await this.findInode(tx, parent), listing = decodeDirListing((await tx.get(parentNode.data)) ?? _throw(withErrno('ENOENT'))); if (!listing[fileName]) throw withErrno('ENOENT'); const ino = listing[fileName]; const inode = new Inode((await tx.get(ino)) ?? _throw(withErrno('ENOENT'))); delete listing[fileName]; if (!isDir && isDirectory(inode)) throw withErrno('EISDIR'); await tx.set(parentNode.data, encodeDirListing(listing)); if (inode.nlink > 1) { inode.update({ nlink: inode.nlink - 1 }); await tx.set(inode.ino, inode); } else { await tx.remove(inode.data); await tx.remove(ino); this._remove(ino); } await tx.commit(); } catch (e_25) { env_25.error = e_25; env_25.hasError = true; } finally { const result_13 = __disposeResources(env_25); if (result_13) await result_13; } } /** * Remove all traces of `path` from the file system. * @param path The path to remove from the file system. * @param isDir Does the path belong to a directory, or a file? */ removeSync(path, isDir) { const env_26 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_26, this.transaction(), false); const { dir: parent, base: fileName } = parse(path), parentNode = this.findInodeSync(tx, parent), listing = decodeDirListing(tx.getSync(parentNode.data) ?? _throw(withErrno('ENOENT'))), ino = listing[fileName]; if (!ino) throw withErrno('ENOENT'); const inode = new Inode(tx.getSync(ino) ?? _throw(withErrno('ENOENT'))); delete listing[fileName]; if (!isDir && isDirectory(inode)) throw withErrno('EISDIR'); tx.setSync(parentNode.data, encodeDirListing(listing)); if (inode.nlink > 1) { inode.update({ nlink: inode.nlink - 1 }); tx.setSync(inode.ino, inode); } else { tx.removeSync(inode.data); tx.removeSync(ino); this._remove(ino); } tx.commitSync(); } catch (e_26) { env_26.error = e_26; env_26.hasError = true; } finally { __disposeResources(env_26); } } }