UNPKG

@zenfs/core

Version:

A filesystem, anywhere

271 lines (270 loc) 10.2 kB
import { setUVMessage, UV } from 'kerium'; import { decodeUTF8 } from 'utilium'; import * as constants from '../constants.js'; import { contextOf } from '../internal/contexts.js'; import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js'; import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js'; import { normalizeMode, normalizePath } from '../utils.js'; import { checkAccess } from './config.js'; import { Dirent, ifToDt } from './dir.js'; import { Handle } from './file.js'; import * as flags from './flags.js'; import { resolveMount } from './shared.js'; import { emitChange } from './watchers.js'; /** * Resolves the mount and real path for a path. * Additionally, any stats fetched will be returned for de-duplication * @category VFS * @internal @hidden */ export function resolve($, path, preserveSymlinks, extra) { path = resolvePath.call($, path); /* Try to resolve it directly. If this works, that means we don't need to perform any resolution for parent directories. */ try { const resolved = resolveMount(path, $); // Stat it to make sure it exists const stats = resolved.fs.statSync(resolved.path); if (!isSymbolicLink(stats) || preserveSymlinks) { return { ...resolved, fullPath: path, stats }; } const target = resolvePath.call($, dirname(path), readlink.call($, path)); return resolve($, target, preserveSymlinks, extra); } catch (e) { setUVMessage(Object.assign(e, { syscall: 'stat', path, ...extra })); if (preserveSymlinks) throw e; } const { base, dir } = parse(path); const realDir = dir == '/' ? '/' : resolve($, dir, false, extra).fullPath; const maybePath = join(realDir, base); const resolved = resolveMount(maybePath, $); let stats; try { stats = resolved.fs.statSync(resolved.path); } catch (e) { if (e.code === 'ENOENT') return { ...resolved, fullPath: path }; throw setUVMessage(Object.assign(e, { syscall: 'stat', path: maybePath, ...extra })); } if (!isSymbolicLink(stats)) { return { ...resolved, fullPath: maybePath, stats }; } const target = resolvePath.call($, realDir, readlink.call($, maybePath)); return resolve($, target, false, extra); } /** * @category VFS * @internal */ export function open(path, opt) { path = normalizePath(path); const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag); path = opt.preserveSymlinks ? path : resolve(this, path).fullPath; const { fs, path: resolved } = resolveMount(path, this); let stats; try { stats = fs.statSync(resolved); } catch { // nothing } if (!stats) { if (!(flag & constants.O_CREAT)) { throw UV('ENOENT', 'open', path); } // Create the file const parentStats = fs.statSync(dirname(resolved)); if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) { throw UV('EACCES', 'open', path); } if (!isDirectory(parentStats)) { throw UV('ENOTDIR', 'open', path); } if (!opt.allowDirectory && mode & constants.S_IFDIR) throw UV('EISDIR', 'open', path); if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) { throw UV('EACCES', 'open', path); } const { euid: uid, egid: gid } = contextOf(this).credentials; const inode = fs.createFileSync(resolved, { mode, uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid, gid: parentStats.mode & constants.S_ISGID ? parentStats.gid : gid, }); return new Handle(this, path, fs, resolved, flag, inode); } if (checkAccess && (!hasAccess(this, stats, mode) || !hasAccess(this, stats, flags.toMode(flag)))) { throw UV('EACCES', 'open', path); } if (flag & constants.O_EXCL) throw UV('EEXIST', 'open', path); const file = new Handle(this, path, fs, resolved, flag, stats); if (!opt.allowDirectory && stats.mode & constants.S_IFDIR) throw UV('EISDIR', 'open', path); if (flag & constants.O_TRUNC) file.truncateSync(0); return file; } export function readlink(path) { path = normalizePath(path); const { fs, stats, path: resolved } = resolve(this, path, true); if (!stats) throw UV('ENOENT', 'readlink', path); if (checkAccess && !hasAccess(this, stats, constants.R_OK)) throw UV('EACCES', 'readlink', path); if (!isSymbolicLink(stats)) throw UV('EINVAL', 'readlink', path); const size = stats.size; const data = new Uint8Array(size); fs.readSync(resolved, data, 0, size); return decodeUTF8(data); } export function mkdir(path, options = {}) { path = normalizePath(path); const { fs, path: resolved } = resolve(this, path); const { euid: uid, egid: gid } = contextOf(this).credentials; const { mode = 0o777, recursive } = options; const __create = (path, resolved, parent) => { if (checkAccess && !hasAccess(this, parent, constants.W_OK)) throw UV('EACCES', 'mkdir', dirname(path)); const inode = fs.mkdirSync(resolved, { mode, uid: parent.mode & constants.S_ISUID ? parent.uid : uid, gid: parent.mode & constants.S_ISGID ? parent.gid : gid, }); emitChange(this, 'rename', path); return inode; }; if (!recursive) { __create(path, resolved, fs.statSync(dirname(resolved))); return; } const dirs = []; for (let dir = resolved, original = path; !fs.existsSync(dir); dir = dirname(dir), original = dirname(original)) { dirs.unshift({ resolved: dir, original }); } if (!dirs.length) return; const stats = [fs.statSync(dirname(dirs[0].resolved))]; for (const [i, dir] of dirs.entries()) { stats.push(__create(dir.original, dir.resolved, stats[i])); } return dirs[0].original; } export function readdir(path, options = {}) { path = normalizePath(path); const { fs, path: resolved } = resolve(this, path); const stats = fs.statSync(resolved); if (checkAccess && !hasAccess(this, stats, constants.R_OK)) throw UV('EACCES', 'readdir', path); if (!isDirectory(stats)) throw UV('ENOTDIR', 'readdir', path); const entries = fs.readdirSync(resolved); // Iterate over entries and handle recursive case if needed const values = []; const addEntry = (entry) => { let entryStat; try { entryStat = fs.statSync(join(resolved, entry)); } catch (e) { if (e.code == 'ENOENT') return; throw e; } const ent = new Dirent(); ent.ino = entryStat.ino; ent.type = ifToDt(entryStat.mode); ent.path = entry; ent.name = basename(entry); values.push(ent); if (!isDirectory(entryStat) || !options?.recursive) return; const children = fs.readdirSync(join(resolved, entry)); for (const child of children) addEntry(join(entry, child)); }; for (const entry of entries) addEntry(entry); return values; } export function rename(oldPath, newPath) { oldPath = normalizePath(oldPath); newPath = normalizePath(newPath); const $ex = { syscall: 'rename', path: oldPath, dest: newPath }; const src = resolve(this, oldPath, true, $ex); const dst = resolveMount(newPath, this, $ex); if (src.fs.uuid !== dst.fs.uuid) throw UV('EXDEV', $ex); if (dst.path.startsWith(src.path + '/')) throw UV('EBUSY', $ex); if (!src.stats) throw UV('ENOENT', $ex); const fs = src.fs; const oldParent = fs.statSync(dirname(src.path)); const newParent = fs.statSync(dirname(dst.path)); let newStats; try { newStats = fs.statSync(dst.path); } catch (e) { if (e.code != 'ENOENT') throw e; } if (checkAccess && (!hasAccess(this, oldParent, constants.R_OK) || !hasAccess(this, newParent, constants.W_OK))) throw UV('EACCES', $ex); if (newStats && !isDirectory(src.stats) && isDirectory(newStats)) throw UV('EISDIR', $ex); if (newStats && isDirectory(src.stats) && !isDirectory(newStats)) throw UV('ENOTDIR', $ex); src.fs.renameSync(src.path, dst.path); emitChange(this, 'rename', oldPath); emitChange(this, 'change', newPath); } export function link(target, link) { target = normalizePath(target); link = normalizePath(link); const $ex = { syscall: 'link', path: link, dest: target }; const { fs, path: resolved } = resolveMount(target, this, $ex); const dst = resolveMount(link, this, $ex); if (fs.uuid !== dst.fs.uuid) throw UV('EXDEV', $ex); const stats = fs.statSync(resolved); if (checkAccess) { if (!hasAccess(this, stats, constants.R_OK)) throw UV('EACCES', $ex); const dirStats = fs.statSync(dirname(resolved)); if (!hasAccess(this, dirStats, constants.R_OK)) throw UV('EACCES', $ex); const destStats = fs.statSync(dirname(dst.path)); if (!hasAccess(this, destStats, constants.W_OK)) throw UV('EACCES', $ex); } return fs.linkSync(resolved, dst.path); } export function stat(path, lstat) { path = normalizePath.call(this, path); const extra = { syscall: lstat ? 'lstat' : 'stat', path }; let stats; if (!lstat) stats = resolve(this, path, false, extra).stats; else { const { base, dir } = parse(path); const { fs, path: parent } = resolve(this, dir, false, extra); try { stats = fs.statSync(base ? join(parent, base) : parent); } catch (e) { setUVMessage(Object.assign(e, extra)); throw e; } } if (!stats) throw UV('ENOENT', extra); if (checkAccess && !hasAccess(this, stats, constants.R_OK)) throw UV('EACCES', extra); return stats; }