UNPKG

@zenfs/core

Version:

A filesystem, anywhere

813 lines (812 loc) 32.3 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; } function next() { while (env.stack.length) { var rec = env.stack.pop(); try { var result = rec.dispose && rec.dispose.call(rec.value); if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } catch (e) { fail(e); } } 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; }); import { credentials } from '../../credentials.js'; import { S_IFDIR, S_IFREG } from '../../emulation/constants.js'; import { basename, dirname, join, resolve } from '../../emulation/path.js'; import { Errno, ErrnoError } from '../../error.js'; import { PreloadFile } from '../../file.js'; import { FileSystem } from '../../filesystem.js'; import { Inode, randomIno, rootIno } from '../../inode.js'; import { decodeDirListing, encodeUTF8, encodeDirListing } from '../../utils.js'; const maxInodeAllocTries = 5; /** * A file system which uses a key-value store. * * We use a unique ID for each node in the file system. The root node has a fixed ID. * @todo Introduce Node ID caching. * @todo Check modes. * @internal */ export class StoreFS extends FileSystem { async ready() { if (this._initialized) { return; } await this.checkRoot(); this._initialized = true; } constructor(store) { super(); this.store = store; this._initialized = false; } metadata() { return { ...super.metadata(), name: this.store.name, }; } /** * Delete all contents stored in the file system. * @deprecated */ async empty() { await this.store.clear(); // Root always exists. await this.checkRoot(); } /** * Delete all contents stored in the file system. * @deprecated */ emptySync() { this.store.clearSync(); // Root always exists. this.checkRootSync(); } /** * @todo Make rename compatible with the cache. */ async rename(oldPath, newPath) { const env_1 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_1, this.store.transaction(), true); const oldParent = dirname(oldPath), oldName = basename(oldPath), newParent = dirname(newPath), newName = basename(newPath), // Remove oldPath from parent's directory listing. oldDirNode = await this.findINode(tx, oldParent), oldDirList = await this.getDirListing(tx, oldDirNode, oldParent); if (!oldDirList[oldName]) { throw ErrnoError.With('ENOENT', oldPath, 'rename'); } const nodeId = oldDirList[oldName]; delete oldDirList[oldName]; /* Can't move a folder inside itself. This ensures that the check passes only if `oldPath` is a subpath of `newParent`. We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path. */ if ((newParent + '/').indexOf(oldPath + '/') === 0) { throw new ErrnoError(Errno.EBUSY, oldParent); } // Add newPath to parent's directory listing. const sameParent = newParent === oldParent; // Prevent us from re-grabbing the same directory listing, which still contains `oldName.` const newDirNode = sameParent ? oldDirNode : await this.findINode(tx, newParent); const newDirList = sameParent ? oldDirList : await this.getDirListing(tx, newDirNode, newParent); if (newDirList[newName]) { // If it's a file, delete it, if it's a directory, throw a permissions error. const newNameNode = await this.getINode(tx, newDirList[newName], newPath); if (!newNameNode.toStats().isFile()) { throw ErrnoError.With('EPERM', newPath, 'rename'); } await tx.remove(newNameNode.ino); await tx.remove(newDirList[newName]); } newDirList[newName] = nodeId; // Commit the two changed directory listings. await tx.set(oldDirNode.ino, encodeDirListing(oldDirList)); await tx.set(newDirNode.ino, encodeDirListing(newDirList)); 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; } } renameSync(oldPath, newPath) { const env_2 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_2, this.store.transaction(), false); const oldParent = dirname(oldPath), oldName = basename(oldPath), newParent = dirname(newPath), newName = basename(newPath), // Remove oldPath from parent's directory listing. oldDirNode = this.findINodeSync(tx, oldParent), oldDirList = this.getDirListingSync(tx, oldDirNode, oldParent); if (!oldDirList[oldName]) { throw ErrnoError.With('ENOENT', oldPath, 'rename'); } const ino = oldDirList[oldName]; delete oldDirList[oldName]; /* Can't move a folder inside itself. This ensures that the check passes only if `oldPath` is a subpath of `newParent`. We append '/' to avoid matching folders that are a substring of the bottom-most folder in the path. */ if ((newParent + '/').indexOf(oldPath + '/') == 0) { throw new ErrnoError(Errno.EBUSY, oldParent); } // Add newPath to parent's directory listing. const sameParent = newParent === oldParent; // Prevent us from re-grabbing the same directory listing, which still contains `oldName.` const newDirNode = sameParent ? oldDirNode : this.findINodeSync(tx, newParent); const newDirList = sameParent ? oldDirList : this.getDirListingSync(tx, newDirNode, newParent); if (newDirList[newName]) { // If it's a file, delete it, if it's a directory, throw a permissions error. const newNameNode = this.getINodeSync(tx, newDirList[newName], newPath); if (!newNameNode.toStats().isFile()) { throw ErrnoError.With('EPERM', newPath, 'rename'); } tx.removeSync(newNameNode.ino); tx.removeSync(newDirList[newName]); } newDirList[newName] = ino; // Commit the two changed directory listings. tx.setSync(oldDirNode.ino, encodeDirListing(oldDirList)); tx.setSync(newDirNode.ino, encodeDirListing(newDirList)); tx.commitSync(); } catch (e_2) { env_2.error = e_2; env_2.hasError = true; } finally { __disposeResources(env_2); } } async stat(path) { const env_3 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_3, this.store.transaction(), true); return (await this.findINode(tx, path)).toStats(); } 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; } } statSync(path) { const env_4 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_4, this.store.transaction(), false); return this.findINodeSync(tx, path).toStats(); } catch (e_4) { env_4.error = e_4; env_4.hasError = true; } finally { __disposeResources(env_4); } } async createFile(path, flag, mode) { const node = await this.commitNew(path, S_IFREG, mode, new Uint8Array(0)); return new PreloadFile(this, path, flag, node.toStats(), new Uint8Array(0)); } createFileSync(path, flag, mode) { this.commitNewSync(path, S_IFREG, mode); return this.openFileSync(path, flag); } async openFile(path, flag) { const env_5 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_5, this.store.transaction(), true); const node = await this.findINode(tx, path), data = await tx.get(node.ino); if (!data) { throw ErrnoError.With('ENOENT', path, 'openFile'); } return new PreloadFile(this, path, flag, node.toStats(), data); } 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; } } openFileSync(path, flag) { const env_6 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_6, this.store.transaction(), false); const node = this.findINodeSync(tx, path), data = tx.getSync(node.ino); if (!data) { throw ErrnoError.With('ENOENT', path, 'openFile'); } return new PreloadFile(this, path, flag, node.toStats(), data); } catch (e_6) { env_6.error = e_6; env_6.hasError = true; } finally { __disposeResources(env_6); } } async unlink(path) { return this.remove(path, false); } unlinkSync(path) { this.removeSync(path, false); } async rmdir(path) { if ((await this.readdir(path)).length) { throw ErrnoError.With('ENOTEMPTY', path, 'rmdir'); } await this.remove(path, true); } rmdirSync(path) { if (this.readdirSync(path).length) { throw ErrnoError.With('ENOTEMPTY', path, 'rmdir'); } this.removeSync(path, true); } async mkdir(path, mode) { await this.commitNew(path, S_IFDIR, mode, encodeUTF8('{}')); } mkdirSync(path, mode) { this.commitNewSync(path, S_IFDIR, mode, encodeUTF8('{}')); } async readdir(path) { const env_7 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_7, this.store.transaction(), true); const node = await this.findINode(tx, path); return Object.keys(await this.getDirListing(tx, node, 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; } } readdirSync(path) { const env_8 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_8, this.store.transaction(), false); const node = this.findINodeSync(tx, path); return Object.keys(this.getDirListingSync(tx, node, path)); } catch (e_8) { env_8.error = e_8; env_8.hasError = true; } finally { __disposeResources(env_8); } } /** * Updated the inode and data node at `path` * @todo Ensure mtime updates properly, and use that to determine if a data update is required. */ async sync(path, data, stats) { const env_9 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_9, this.store.transaction(), true); // We use _findInode because we actually need the INode id. const fileInodeId = await this._findINode(tx, dirname(path), basename(path)), fileInode = await this.getINode(tx, fileInodeId, path), inodeChanged = fileInode.update(stats); // Sync data. await tx.set(fileInode.ino, data); // Sync metadata. if (inodeChanged) { await tx.set(fileInodeId, fileInode.data); } 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; } } /** * Updated the inode and data node at `path` * @todo Ensure mtime updates properly, and use that to determine if a data update is required. */ syncSync(path, data, stats) { const env_10 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_10, this.store.transaction(), false); // We use _findInode because we actually need the INode id. const fileInodeId = this._findINodeSync(tx, dirname(path), basename(path)), fileInode = this.getINodeSync(tx, fileInodeId, path), inodeChanged = fileInode.update(stats); // Sync data. tx.setSync(fileInode.ino, data); // Sync metadata. if (inodeChanged) { tx.setSync(fileInodeId, fileInode.data); } tx.commitSync(); } catch (e_10) { env_10.error = e_10; env_10.hasError = true; } finally { __disposeResources(env_10); } } async link(target, link) { const env_11 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_11, this.store.transaction(), true); const newDir = dirname(link), newDirNode = await this.findINode(tx, newDir), listing = await this.getDirListing(tx, newDirNode, newDir); const ino = await this._findINode(tx, dirname(target), basename(target)); const node = await this.getINode(tx, ino, target); node.nlink++; listing[basename(link)] = ino; tx.setSync(ino, node.data); tx.setSync(newDirNode.ino, encodeDirListing(listing)); tx.commitSync(); } 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; } } linkSync(target, link) { const env_12 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_12, this.store.transaction(), false); const newDir = dirname(link), newDirNode = this.findINodeSync(tx, newDir), listing = this.getDirListingSync(tx, newDirNode, newDir); const ino = this._findINodeSync(tx, dirname(target), basename(target)); const node = this.getINodeSync(tx, ino, target); node.nlink++; listing[basename(link)] = ino; tx.setSync(ino, node.data); tx.setSync(newDirNode.ino, encodeDirListing(listing)); tx.commitSync(); } catch (e_12) { env_12.error = e_12; env_12.hasError = true; } finally { __disposeResources(env_12); } } /** * Checks if the root directory exists. Creates it if it doesn't. */ async checkRoot() { const env_13 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_13, this.store.transaction(), true); if (await tx.get(rootIno)) { return; } // Create new inode. o777, owned by root:root const inode = new Inode(); inode.mode = 0o777 | S_IFDIR; // If the root doesn't exist, the first random ID shouldn't exist either. await tx.set(inode.ino, encodeUTF8('{}')); await tx.set(rootIno, inode.data); 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; } } /** * Checks if the root directory exists. Creates it if it doesn't. */ checkRootSync() { const env_14 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_14, this.store.transaction(), false); if (tx.getSync(rootIno)) { return; } // Create new inode, mode o777, owned by root:root const inode = new Inode(); inode.mode = 0o777 | S_IFDIR; // If the root doesn't exist, the first random ID shouldn't exist either. tx.setSync(inode.ino, encodeUTF8('{}')); tx.setSync(rootIno, inode.data); tx.commitSync(); } catch (e_14) { env_14.error = e_14; env_14.hasError = true; } finally { __disposeResources(env_14); } } /** * Helper function for findINode. * @param parent The parent directory of the file we are attempting to find. * @param filename The filename of the inode we are attempting to find, minus * the parent. */ async _findINode(tx, parent, filename, visited = new Set()) { const currentPath = join(parent, filename); if (visited.has(currentPath)) { throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath); } visited.add(currentPath); if (parent == '/' && filename === '') { return rootIno; } const inode = parent == '/' ? await this.getINode(tx, rootIno, parent) : await this.findINode(tx, parent, visited); const dirList = await this.getDirListing(tx, inode, parent); if (!(filename in dirList)) { throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode'); } return dirList[filename]; } /** * Helper function for findINode. * @param parent The parent directory of the file we are attempting to find. * @param filename The filename of the inode we are attempting to find, minus * the parent. * @return string The ID of the file's inode in the file system. */ _findINodeSync(tx, parent, filename, visited = new Set()) { const currentPath = join(parent, filename); if (visited.has(currentPath)) { throw new ErrnoError(Errno.EIO, 'Infinite loop detected while finding inode', currentPath); } visited.add(currentPath); if (parent == '/' && filename === '') { return rootIno; } const inode = parent == '/' ? this.getINodeSync(tx, rootIno, parent) : this.findINodeSync(tx, parent, visited); const dir = this.getDirListingSync(tx, inode, parent); if (!(filename in dir)) { throw ErrnoError.With('ENOENT', resolve(parent, filename), '_findINode'); } return dir[filename]; } /** * Finds the Inode of `path`. * @param path The path to look up. * @todo memoize/cache */ async findINode(tx, path, visited = new Set()) { const id = await this._findINode(tx, dirname(path), basename(path), visited); return this.getINode(tx, id, path); } /** * 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, visited = new Set()) { const ino = this._findINodeSync(tx, dirname(path), basename(path), visited); return this.getINodeSync(tx, ino, path); } /** * Given the ID of a node, retrieves the corresponding Inode. * @param tx The transaction to use. * @param path The corresponding path to the file (used for error messages). * @param id The ID to look up. */ async getINode(tx, id, path) { const data = await tx.get(id); if (!data) { throw ErrnoError.With('ENOENT', path, 'getINode'); } return new Inode(data.buffer); } /** * Given the ID of a node, retrieves the corresponding Inode. * @param tx The transaction to use. * @param path The corresponding path to the file (used for error messages). * @param id The ID to look up. */ getINodeSync(tx, id, path) { const data = tx.getSync(id); if (!data) { throw ErrnoError.With('ENOENT', path, 'getINode'); } const inode = new Inode(data.buffer); return inode; } /** * Given the Inode of a directory, retrieves the corresponding directory * listing. */ async getDirListing(tx, inode, path) { const data = await tx.get(inode.ino); /* Occurs when data is undefined,or corresponds to something other than a directory listing. The latter should never occur unless the file system is corrupted. */ if (!data) { throw ErrnoError.With('ENOENT', path, 'getDirListing'); } return decodeDirListing(data); } /** * Given the Inode of a directory, retrieves the corresponding directory listing. */ getDirListingSync(tx, inode, p) { const data = tx.getSync(inode.ino); if (!data) { throw ErrnoError.With('ENOENT', p, 'getDirListing'); } return decodeDirListing(data); } /** * Adds a new node under a random ID. Retries before giving up in * the exceedingly unlikely chance that we try to reuse a random ino. */ async addNew(tx, data, path) { for (let i = 0; i < maxInodeAllocTries; i++) { const ino = randomIno(); if (await tx.get(ino)) { continue; } await tx.set(ino, data); return ino; } throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode'); } /** * Creates a new node under a random ID. Retries before giving up in * the exceedingly unlikely chance that we try to reuse a random ino. * @return The ino that the data was stored under. */ addNewSync(tx, data, path) { for (let i = 0; i < maxInodeAllocTries; i++) { const ino = randomIno(); if (tx.getSync(ino)) { continue; } tx.setSync(ino, data); return ino; } throw new ErrnoError(Errno.ENOSPC, 'No inode IDs available', path, 'addNewNode'); } /** * 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 type The type of the new file. * @param mode The mode to create the new file with. * @param data The data to store at the file's data node. */ async commitNew(path, type, mode, data) { const env_15 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_15, this.store.transaction(), true); const parentPath = dirname(path), parent = await this.findINode(tx, parentPath); const fname = basename(path), listing = await this.getDirListing(tx, parent, parentPath); /* The root always exists. If we don't check this prior to taking steps below, we will create a file with name '' in root should path == '/'. */ if (path === '/') { throw ErrnoError.With('EEXIST', path, 'commitNew'); } // Check if file already exists. if (listing[fname]) { await tx.abort(); throw ErrnoError.With('EEXIST', path, 'commitNew'); } // Commit data. const inode = new Inode(); inode.ino = await this.addNew(tx, data, path); inode.mode = mode | type; inode.uid = credentials.uid; inode.gid = credentials.gid; inode.size = data.length; // Update and commit parent directory listing. listing[fname] = await this.addNew(tx, inode.data, path); await tx.set(parent.ino, encodeDirListing(listing)); await tx.commit(); return inode; } 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; } } /** * 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 type The type of the new file. * @param mode The mode 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, type, mode, data = new Uint8Array()) { const env_16 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_16, this.store.transaction(), false); const parentPath = dirname(path), parent = this.findINodeSync(tx, parentPath); const fname = basename(path), listing = this.getDirListingSync(tx, parent, parentPath); /* The root always exists. If we don't check this prior to taking steps below, we will create a file with name '' in root should p == '/'. */ if (path === '/') { throw ErrnoError.With('EEXIST', path, 'commitNew'); } // Check if file already exists. if (listing[fname]) { throw ErrnoError.With('EEXIST', path, 'commitNew'); } // Commit data. const node = new Inode(); node.ino = this.addNewSync(tx, data, path); node.size = data.length; node.mode = mode | type; node.uid = credentials.uid; node.gid = credentials.gid; // Update and commit parent directory listing. listing[fname] = this.addNewSync(tx, node.data, path); tx.setSync(parent.ino, encodeDirListing(listing)); tx.commitSync(); return node; } catch (e_16) { env_16.error = e_16; env_16.hasError = true; } finally { __disposeResources(env_16); } } /** * 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? * @todo Update mtime. */ async remove(path, isDir) { const env_17 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_17, this.store.transaction(), true); const parent = dirname(path), parentNode = await this.findINode(tx, parent), listing = await this.getDirListing(tx, parentNode, parent), fileName = basename(path); if (!listing[fileName]) { throw ErrnoError.With('ENOENT', path, 'remove'); } const fileIno = listing[fileName]; // Get file inode. const fileNode = await this.getINode(tx, fileIno, path); // Remove from directory listing of parent. delete listing[fileName]; if (!isDir && fileNode.toStats().isDirectory()) { throw ErrnoError.With('EISDIR', path, 'remove'); } await tx.set(parentNode.ino, encodeDirListing(listing)); if (--fileNode.nlink < 1) { // remove file await tx.remove(fileNode.ino); await tx.remove(fileIno); } // Success. 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; } } /** * 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? * @todo Update mtime. */ removeSync(path, isDir) { const env_18 = { stack: [], error: void 0, hasError: false }; try { const tx = __addDisposableResource(env_18, this.store.transaction(), false); const parent = dirname(path), parentNode = this.findINodeSync(tx, parent), listing = this.getDirListingSync(tx, parentNode, parent), fileName = basename(path), fileIno = listing[fileName]; if (!fileIno) { throw ErrnoError.With('ENOENT', path, 'remove'); } // Get file inode. const fileNode = this.getINodeSync(tx, fileIno, path); // Remove from directory listing of parent. delete listing[fileName]; if (!isDir && fileNode.toStats().isDirectory()) { throw ErrnoError.With('EISDIR', path, 'remove'); } // Update directory listing. tx.setSync(parentNode.ino, encodeDirListing(listing)); if (--fileNode.nlink < 1) { // remove file tx.removeSync(fileNode.ino); tx.removeSync(fileIno); } // Success. tx.commitSync(); } catch (e_18) { env_18.error = e_18; env_18.hasError = true; } finally { __disposeResources(env_18); } } }