@zenfs/core
Version:
A filesystem, anywhere
248 lines (247 loc) • 10.1 kB
JavaScript
import { rethrow, 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
* @internal @hidden
*/
export async function resolve($, path, preserveSymlinks, extra) {
path = resolvePath.call($, path);
if (preserveSymlinks) {
const resolved = resolveMount(path, $, extra);
const stats = await resolved.fs.stat(resolved.path).catch(() => undefined);
return { ...resolved, fullPath: path, stats };
}
/* 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 = await resolved.fs.stat(resolved.path);
if (!isSymbolicLink(stats)) {
return { ...resolved, fullPath: path, stats };
}
const target = resolvePath.call($, dirname(path), await readlink.call($, path));
return await resolve($, target, preserveSymlinks, extra);
}
catch {
// Go the long way
}
const { base, dir } = parse(path);
const realDir = dir == '/' ? '/' : (await resolve($, dir, false, extra)).fullPath;
const maybePath = join(realDir, base);
const resolved = resolveMount(maybePath, $);
const stats = await resolved.fs.stat(resolved.path).catch((e) => {
if (e.code == 'ENOENT')
return;
throw setUVMessage(Object.assign(e, { syscall: 'stat', path: maybePath, ...extra }));
});
if (!stats)
return { ...resolved, fullPath: path };
if (!isSymbolicLink(stats)) {
return { ...resolved, fullPath: maybePath, stats };
}
const target = resolvePath.call($, realDir, await readlink.call($, maybePath));
return await resolve($, target, false, extra);
}
/**
* Opens a file. This helper handles the complexity of file flags.
* @internal
*/
export async function open($, path, opt) {
path = normalizePath(path);
const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag);
const $ex = { syscall: 'open', path };
const { fs, path: resolved, stats } = await resolve($, path, opt.preserveSymlinks, $ex);
if (!stats) {
if (!(flag & constants.O_CREAT))
throw UV('ENOENT', $ex);
// Create the file
const parentStats = await fs.stat(dirname(resolved));
if (checkAccess && !hasAccess($, parentStats, constants.W_OK))
throw UV('EACCES', 'open', dirname(path));
if (!isDirectory(parentStats))
throw UV('ENOTDIR', 'open', dirname(path));
if (!opt.allowDirectory && mode & constants.S_IFDIR)
throw UV('EISDIR', 'open', path);
const { euid: uid, egid: gid } = contextOf($).credentials;
const inode = await fs.createFile(resolved, {
mode,
uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
gid: parentStats.mode & constants.S_ISGID ? parentStats.gid : gid,
});
return new Handle($, path, fs, resolved, flag, inode);
}
if (checkAccess && !hasAccess($, stats, flags.toMode(flag)))
throw UV('EACCES', $ex);
if (flag & constants.O_EXCL)
throw UV('EEXIST', $ex);
const handle = new Handle($, path, fs, resolved, flag, stats);
if (!opt.allowDirectory && mode & constants.S_IFDIR)
throw UV('EISDIR', 'open', path);
if (flag & constants.O_TRUNC)
await handle.truncate(0);
return handle;
}
export async function readlink(path) {
path = normalizePath(path);
const $ex = { syscall: 'readlink', path };
const { fs, stats, path: resolved } = await resolve(this, path, true, $ex);
if (!stats)
throw UV('ENOENT', $ex);
if (checkAccess && !hasAccess(this, stats, constants.R_OK))
throw UV('EACCES', $ex);
if (!isSymbolicLink(stats))
throw UV('EINVAL', $ex);
const size = stats.size;
const data = new Uint8Array(size);
await fs.read(resolved, data, 0, size);
return decodeUTF8(data);
}
export async function mkdir(path, options = {}) {
path = normalizePath(path);
const { euid: uid, egid: gid } = contextOf(this).credentials;
const { mode = 0o777, recursive } = options;
const { fs, path: resolved } = resolveMount(path, this, { syscall: 'mkdir' });
const __create = async (path, resolved, parent) => {
if (checkAccess && !hasAccess(this, parent, constants.W_OK))
throw UV('EACCES', 'mkdir', path);
const inode = await fs.mkdir(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) {
await __create(path, resolved, await fs.stat(dirname(resolved)));
return;
}
const dirs = [];
let origDir = path;
for (let dir = resolved; !(await fs.exists(dir)); dir = dirname(dir), origDir = dirname(origDir)) {
dirs.unshift([origDir, dir]);
}
if (!dirs.length)
return;
const stats = [await fs.stat(dirname(dirs[0][1]))];
for (const [i, [path, resolved]] of dirs.entries()) {
stats.push(await __create(path, resolved, stats[i]));
}
return dirs[0][0];
}
export async function readdir(path, options = {}) {
path = normalizePath(path);
const $ex = { syscall: 'readdir', path };
const { fs, path: resolved, stats } = await resolve(this, path, false, $ex);
if (!stats)
throw UV('ENOENT', $ex);
if (checkAccess && !hasAccess(this, stats, constants.R_OK))
throw UV('EACCES', $ex);
if (!isDirectory(stats))
throw UV('ENOTDIR', $ex);
const entries = await fs.readdir(resolved);
const values = [];
const addEntry = async (entry) => {
const entryStats = await fs.stat(join(resolved, entry)).catch((e) => {
if (e.code == 'ENOENT')
return;
throw e;
});
if (!entryStats)
return;
const ent = new Dirent();
ent.ino = entryStats.ino;
ent.type = ifToDt(entryStats.mode);
ent.path = entry;
ent.name = basename(entry);
values.push(ent);
if (!options.recursive || !isDirectory(entryStats))
return;
const children = await fs.readdir(join(resolved, entry));
for (const child of children)
await addEntry(join(entry, child));
};
await Promise.all(entries.map(addEntry));
return values;
}
export async function rename(oldPath, newPath) {
oldPath = normalizePath(oldPath);
newPath = normalizePath(newPath);
const $ex = { syscall: 'rename', path: oldPath, dest: newPath };
const src = await 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 = await fs.stat(dirname(src.path));
const newParent = await fs.stat(dirname(dst.path));
const newStats = await fs.stat(dst.path).catch((e) => {
if (e.code == 'ENOENT')
return null;
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);
await src.fs.rename(src.path, dst.path);
emitChange(this, 'rename', oldPath);
emitChange(this, 'change', newPath);
}
export async 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 = await fs.stat(resolved);
if (checkAccess) {
if (!hasAccess(this, stats, constants.R_OK))
throw UV('EACCES', $ex);
const dirStats = await fs.stat(dirname(resolved));
if (!hasAccess(this, dirStats, constants.R_OK))
throw UV('EACCES', $ex);
const destStats = await fs.stat(dirname(dst.path));
if (!hasAccess(this, destStats, constants.W_OK))
throw UV('EACCES', $ex);
}
return await fs.link(resolved, dst.path);
}
export async function stat(path, lstat) {
path = normalizePath.call(this, path);
const extra = { syscall: lstat ? 'lstat' : 'stat', path };
let stats;
if (!lstat)
stats = (await resolve(this, path, false, extra)).stats;
else {
const { base, dir } = parse(path);
const { fs, path: parent } = await resolve(this, dir, false, extra);
stats = await fs.stat(base ? join(parent, base) : parent).catch(rethrow(extra));
}
if (!stats)
throw UV('ENOENT', extra);
if (checkAccess && !hasAccess(this, stats, constants.R_OK))
throw UV('EACCES', extra);
return stats;
}