@zenfs/core
Version:
A filesystem, anywhere
202 lines (201 loc) • 6.51 kB
JavaScript
// SPDX-License-Identifier: LGPL-3.0-or-later
/* eslint-disable @typescript-eslint/require-await */
import { withErrno } from 'kerium';
import { _throw } from 'utilium';
import { dirname, join, relative } from '../path.js';
import { S_IFDIR, S_IFMT, S_IFREG, S_ISGID, S_ISUID } from '../constants.js';
import { Index } from './file_index.js';
import { FileSystem } from './filesystem.js';
import { Inode } from './inode.js';
/**
* A file system that uses an `Index` for metadata.
* @category Internals
* @internal
*/
export class IndexFS extends FileSystem {
index;
constructor(id, name, index = new Index()) {
super(id, name);
this.index = index;
}
usage() {
return this.index.usage();
}
/**
* Finds all the paths in the index that need to be moved for a rename
*/
pathsForRename(oldPath, newPath) {
if (!this.index.has(oldPath))
throw withErrno('ENOENT');
if ((dirname(newPath) + '/').startsWith(oldPath + '/'))
throw withErrno('EBUSY');
const toRename = [];
for (const [from, inode] of this.index.entries()) {
const rel = relative(oldPath, from);
if (rel.startsWith('..'))
continue;
let to = join(newPath, rel);
if (to.endsWith('/'))
to = to.slice(0, -1);
toRename.push({ from, to, inode });
}
toRename.sort((a, b) => b.from.length - a.from.length);
return toRename;
}
async rename(oldPath, newPath) {
if (oldPath == newPath)
return;
const toRename = this.pathsForRename(oldPath, newPath);
const contents = new Map();
for (const { from, to, inode } of toRename) {
const data = new Uint8Array(inode.size);
await this.read(from, data, 0, inode.size);
contents.set(to, data);
this.index.delete(from);
await this.remove(from);
if (this.index.has(to))
await this.remove(to);
}
toRename.reverse();
for (const { to, inode } of toRename) {
const data = contents.get(to);
this.index.set(to, inode);
if ((inode.mode & S_IFMT) == S_IFDIR)
await this._mkdir?.(to, inode);
else
await this.write(to, data, 0);
}
}
renameSync(oldPath, newPath) {
if (oldPath == newPath)
return;
const toRename = this.pathsForRename(oldPath, newPath);
const contents = new Map();
for (const { from, to, inode } of toRename) {
const data = new Uint8Array(inode.size);
this.readSync(from, data, 0, inode.size);
contents.set(to, data);
this.index.delete(from);
this.removeSync(from);
if (this.index.has(to))
this.removeSync(to);
}
toRename.reverse();
for (const { to, inode } of toRename) {
const data = contents.get(to);
this.index.set(to, inode);
if ((inode.mode & S_IFMT) == S_IFDIR)
this._mkdirSync?.(to, inode);
else
this.writeSync(to, data, 0);
}
}
async stat(path) {
const inode = this.index.get(path);
if (!inode)
throw withErrno('ENOENT');
return inode;
}
statSync(path) {
const inode = this.index.get(path);
if (!inode)
throw withErrno('ENOENT');
return inode;
}
async touch(path, metadata) {
const inode = this.index.get(path) ?? _throw(withErrno('ENOENT'));
inode.update(metadata);
}
touchSync(path, metadata) {
const inode = this.index.get(path) ?? _throw(withErrno('ENOENT'));
inode.update(metadata);
}
_remove(path, isUnlink) {
const inode = this.index.get(path);
if (!inode)
throw withErrno('ENOENT');
const isDir = (inode.mode & S_IFMT) == S_IFDIR;
if (!isDir && !isUnlink)
throw withErrno('ENOTDIR');
if (isDir && isUnlink)
throw withErrno('EISDIR');
if (!isDir)
this.index.delete(path);
}
async unlink(path) {
this._remove(path, true);
await this.remove(path);
}
unlinkSync(path) {
this._remove(path, true);
this.removeSync(path);
}
async rmdir(path) {
this._remove(path, false);
const entries = await this.readdir(path);
if (entries.length)
throw withErrno('ENOTEMPTY');
this.index.delete(path);
await this.remove(path);
}
rmdirSync(path) {
this._remove(path, false);
if (this.readdirSync(path).length)
throw withErrno('ENOTEMPTY');
this.index.delete(path);
this.removeSync(path);
}
create(path, options) {
if (this.index.has(path))
throw withErrno('EEXIST');
const parent = this.index.get(dirname(path));
if (!parent)
throw withErrno('ENOENT');
const id = this.index._alloc();
const inode = new Inode({
ino: id,
data: id + 1,
mode: options.mode,
size: 0,
uid: parent.mode & S_ISUID ? parent.uid : options.uid,
gid: parent.mode & S_ISGID ? parent.gid : options.gid,
nlink: 1,
});
this.index.set(path, inode);
return inode;
}
async createFile(path, options) {
options.mode |= S_IFREG;
return this.create(path, options);
}
createFileSync(path, options) {
options.mode |= S_IFREG;
return this.create(path, options);
}
async mkdir(path, options) {
options.mode |= S_IFDIR;
const inode = this.create(path, options);
await this._mkdir?.(path, options);
return inode;
}
mkdirSync(path, options) {
options.mode |= S_IFDIR;
const inode = this.create(path, options);
this._mkdirSync?.(path, options);
return inode;
}
link(target, link) {
throw withErrno('ENOSYS');
}
linkSync(target, link) {
throw withErrno('ENOSYS');
}
async readdir(path) {
return Object.keys(this.index.directoryEntries(path));
}
readdirSync(path) {
return Object.keys(this.index.directoryEntries(path));
}
async sync() { }
syncSync() { }
}