@zenfs/core
Version:
A filesystem, anywhere
271 lines (270 loc) • 10.2 kB
JavaScript
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;
}