@zenfs/core
Version:
A filesystem, anywhere
1,103 lines (1,102 loc) • 42.4 kB
JavaScript
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);
}
}
}