@zenfs/core
Version:
A filesystem, anywhere
154 lines (137 loc) • 4.4 kB
text/typescript
// Utilities and shared data
import type { BigIntStatsFs, StatsFs } from 'node:fs';
import { InMemory } from '../backends/memory.js';
import { Errno, ErrnoError } from '../error.js';
import type { File } from '../file.js';
import type { FileSystem } from '../filesystem.js';
import { normalizePath } from '../utils.js';
import { resolve, type AbsolutePath } from './path.js';
import { size_max } from './constants.js';
// descriptors
export const fdMap: Map<number, File> = new Map();
let nextFd = 100;
export function file2fd(file: File): number {
const fd = nextFd++;
fdMap.set(fd, file);
return fd;
}
export function fd2file(fd: number): File {
if (!fdMap.has(fd)) {
throw new ErrnoError(Errno.EBADF);
}
return fdMap.get(fd)!;
}
export type MountObject = Record<AbsolutePath, FileSystem>;
/**
* The map of mount points
* @internal
*/
export const mounts: Map<string, FileSystem> = new Map();
// Set a default root.
mount('/', InMemory.create({ name: 'root' }));
/**
* Mounts the file system at `mountPoint`.
*/
export function mount(mountPoint: string, fs: FileSystem): void {
if (mountPoint[0] !== '/') {
mountPoint = '/' + mountPoint;
}
mountPoint = resolve(mountPoint);
if (mounts.has(mountPoint)) {
throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already in use.');
}
mounts.set(mountPoint, fs);
}
/**
* Unmounts the file system at `mountPoint`.
*/
export function umount(mountPoint: string): void {
if (mountPoint[0] !== '/') {
mountPoint = `/${mountPoint}`;
}
mountPoint = resolve(mountPoint);
if (!mounts.has(mountPoint)) {
throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already unmounted.');
}
mounts.delete(mountPoint);
}
/**
* Gets the internal FileSystem for the path, then returns it along with the path relative to the FS' root
*/
export function resolveMount(path: string): { fs: FileSystem; path: string; mountPoint: string } {
path = normalizePath(path);
// Maybe do something for devices here
const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length
for (const [mountPoint, fs] of sortedMounts) {
// We know path is normalized, so it would be a substring of the mount point.
if (mountPoint.length <= path.length && path.startsWith(mountPoint)) {
path = path.slice(mountPoint.length > 1 ? mountPoint.length : 0); // Resolve the path relative to the mount point
if (path === '') {
path = '/';
}
return { fs, path, mountPoint };
}
}
throw new ErrnoError(Errno.EIO, 'ZenFS not initialized with a file system');
}
/**
* Reverse maps the paths in text from the mounted FileSystem to the global path
* @hidden
*/
export function fixPaths(text: string, paths: Record<string, string>): string {
for (const [from, to] of Object.entries(paths)) {
text = text?.replaceAll(from, to);
}
return text;
}
/**
* Fix paths in error stacks
* @hidden
*/
export function fixError<E extends ErrnoError>(e: E, paths: Record<string, string>): E {
if (typeof e.stack == 'string') {
e.stack = fixPaths(e.stack, paths);
}
e.message = fixPaths(e.message, paths);
return e;
}
export function mountObject(mounts: MountObject): void {
if ('/' in mounts) {
umount('/');
}
for (const [point, fs] of Object.entries(mounts)) {
mount(point, fs);
}
}
/**
* @hidden
*/
export function _statfs<const T extends boolean>(fs: FileSystem, bigint?: T): T extends true ? BigIntStatsFs : StatsFs {
const md = fs.metadata();
const bs = md.blockSize || 4096;
return {
type: (bigint ? BigInt : Number)(md.type),
bsize: (bigint ? BigInt : Number)(bs),
ffree: (bigint ? BigInt : Number)(md.freeNodes || size_max),
files: (bigint ? BigInt : Number)(md.totalNodes || size_max),
bavail: (bigint ? BigInt : Number)(md.freeSpace / bs),
bfree: (bigint ? BigInt : Number)(md.freeSpace / bs),
blocks: (bigint ? BigInt : Number)(md.totalSpace / bs),
} as T extends true ? BigIntStatsFs : StatsFs;
}
/**
* Options used for caching, among other things.
* @internal *UNSTABLE*
*/
export interface InternalOptions {
/**
* If true, then this readdir was called from another function.
* In this case, don't clear the cache when done.
* @internal *UNSTABLE*
*/
_isIndirect?: boolean;
}
export interface ReaddirOptions extends InternalOptions {
withFileTypes?: boolean;
recursive?: boolean;
}