UNPKG

@zenfs/core

Version:

A filesystem, anywhere

202 lines (201 loc) 6.51 kB
// 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() { } }