@zenfs/core
Version:
A filesystem, anywhere
298 lines (297 loc) • 10.7 kB
JavaScript
import { log, withErrno } from 'kerium';
import { checkOptions, isBackend, isBackendConfig } from './backends/backend.js';
import { defaultContext } from './internal/contexts.js';
import { createCredentials } from './internal/credentials.js';
import { DeviceFS } from './internal/devices.js';
import { FileSystem } from './internal/filesystem.js';
import { exists, mkdir, stat } from './node/promises.js';
import { existsSync, mkdirSync, statSync } from './node/sync.js';
import { _setAccessChecks } from './vfs/config.js';
import { mount, mounts, umount } from './vfs/shared.js';
/**
* Update the configuration of a file system.
* @category Backends and Configuration
*/
export function configureFileSystem(fs, config) {
if (config.disableAsyncCache)
fs.attributes.set('no_async_preload');
if (config.caseFold)
fs.attributes.set('case_fold', config.caseFold);
}
function isMountConfig(arg) {
return isBackendConfig(arg) || isBackend(arg) || arg instanceof FileSystem;
}
function isThenable(value) {
return typeof value?.then == 'function';
}
/**
* Retrieve a file system with `configuration`.
* @category Backends and Configuration
* @see MountConfiguration
*/
export async function resolveMountConfig(configuration, _depth = 0) {
if (typeof configuration !== 'object' || configuration == null) {
throw log.err(withErrno('EINVAL', 'Invalid options on mount configuration'));
}
if (!isMountConfig(configuration)) {
throw log.err(withErrno('EINVAL', 'Invalid mount configuration'));
}
if (configuration instanceof FileSystem) {
await configuration.ready();
return configuration;
}
if (isBackend(configuration)) {
configuration = { backend: configuration };
}
for (const [key, value] of Object.entries(configuration)) {
if (key == 'backend')
continue;
if (!isMountConfig(value))
continue;
log.info('Resolving nested mount configuration: ' + key);
if (_depth > 10) {
throw log.err(withErrno('EINVAL', 'Invalid configuration, too deep and possibly infinite'));
}
configuration[key] = await resolveMountConfig(value, ++_depth);
}
const { backend } = configuration;
if (typeof backend.isAvailable == 'function' && !(await backend.isAvailable(configuration))) {
throw log.err(withErrno('EPERM', 'Backend not available: ' + backend.name));
}
checkOptions(backend, configuration);
const mount = (await backend.create(configuration));
configureFileSystem(mount, configuration);
await mount.ready();
return mount;
}
/**
* @experimental
* Retrieve a file system with `configuration`.
* @category Backends and Configuration
* @see MountConfiguration
*/
export function resolveMountConfigSync(configuration, _depth = 0) {
if (typeof configuration !== 'object' || configuration == null) {
throw log.err(withErrno('EINVAL', 'Invalid options on mount configuration'));
}
if (!isMountConfig(configuration)) {
throw log.err(withErrno('EINVAL', 'Invalid mount configuration'));
}
if (configuration instanceof FileSystem) {
configuration.readySync();
return configuration;
}
if (isBackend(configuration)) {
configuration = { backend: configuration };
}
for (const [key, value] of Object.entries(configuration)) {
if (key == 'backend')
continue;
if (!isMountConfig(value))
continue;
log.info('Resolving nested mount configuration: ' + key);
if (_depth > 10) {
throw log.err(withErrno('EINVAL', 'Invalid configuration, too deep and possibly infinite'));
}
configuration[key] = resolveMountConfigSync(value, ++_depth);
}
const { backend } = configuration;
if (typeof backend.isAvailable == 'function') {
const available = backend.isAvailable(configuration);
if (isThenable(available)) {
throw log.err(withErrno('EAGAIN', 'Backend availability check would block: ' + backend.name));
}
if (!available) {
throw log.err(withErrno('EPERM', 'Backend not available: ' + backend.name));
}
}
checkOptions(backend, configuration);
const mountFs = backend.create(configuration);
if (isThenable(mountFs)) {
throw log.err(withErrno('EAGAIN', 'Backend initialization would block: ' + backend.name));
}
const resolved = mountFs;
configureFileSystem(resolved, configuration);
resolved.readySync();
return resolved;
}
/**
* Configures ZenFS with single mount point /
* @category Backends and Configuration
*/
export async function configureSingle(configuration) {
if (!isMountConfig(configuration)) {
throw new TypeError('Invalid single mount point configuration');
}
const resolved = await resolveMountConfig(configuration);
umount('/');
mount('/', resolved);
}
/**
* @experimental
* Configures ZenFS with single mount point /
* @category Backends and Configuration
*/
export function configureSingleSync(configuration) {
if (!isMountConfig(configuration)) {
throw new TypeError('Invalid single mount point configuration');
}
const resolved = resolveMountConfigSync(configuration);
umount('/');
mount('/', resolved);
}
/**
* Like `fs.mount`, but it also creates missing directories.
* @privateRemarks
* This is implemented as a separate function to avoid a circular dependency between vfs/shared.ts and other vfs layer files.
* @internal
*/
async function mountWithMkdir(path, fs) {
if (path == '/') {
mount(path, fs);
return;
}
const stats = await stat(path).catch(() => null);
if (!stats) {
await mkdir(path, { recursive: true });
}
else if (!stats.isDirectory()) {
throw withErrno('ENOTDIR', 'Missing directory at mount point: ' + path);
}
mount(path, fs);
}
/**
* Like `fs.mount`, but it also creates missing directories.
* @privateRemarks
* This is implemented as a separate function to avoid a circular dependency between vfs/shared.ts and other vfs layer files.
* @internal
*/
function mountWithMkdirSync(path, fs) {
if (path == '/') {
mount(path, fs);
return;
}
let stats = null;
try {
stats = statSync(path);
}
catch (error) {
if (error?.code != 'ENOENT')
throw error;
}
if (!stats) {
mkdirSync(path, { recursive: true });
}
else if (!stats.isDirectory()) {
throw withErrno('ENOTDIR', 'Missing directory at mount point: ' + path);
}
mount(path, fs);
}
/**
* @category Backends and Configuration
*/
export function addDevice(driver, options) {
const devfs = mounts.get('/dev');
if (!(devfs instanceof DeviceFS))
throw log.crit(withErrno('ENOTSUP', '/dev does not exist or is not a device file system'));
return devfs._createDevice(driver, options);
}
const _defaultDirectories = ['/tmp', '/var', '/etc'];
/**
* Configures ZenFS with `configuration`
* @category Backends and Configuration
* @see Configuration
*/
export async function configure(configuration) {
Object.assign(defaultContext.credentials, createCredentials({
uid: configuration.uid || 0,
gid: configuration.gid || 0,
}));
_setAccessChecks(!configuration.disableAccessChecks);
if (configuration.log)
log.configure(configuration.log);
if (configuration.mounts) {
// sort to make sure any root replacement is done first
for (const [_point, mountConfig] of Object.entries(configuration.mounts).sort(([a], [b]) => (a.length > b.length ? 1 : -1))) {
const point = _point.startsWith('/') ? _point : '/' + _point;
if (isBackendConfig(mountConfig)) {
mountConfig.disableAsyncCache ??= configuration.disableAsyncCache || false;
mountConfig.caseFold ??= configuration.caseFold;
}
if (point == '/')
umount('/');
await mountWithMkdir(point, await resolveMountConfig(mountConfig));
}
}
for (const fs of mounts.values()) {
configureFileSystem(fs, configuration);
}
if (configuration.addDevices && !mounts.has('/dev')) {
const devfs = new DeviceFS();
devfs.addDefaults();
await devfs.ready();
await mountWithMkdir('/dev', devfs);
}
if (configuration.defaultDirectories) {
for (const dir of _defaultDirectories) {
if (await exists(dir)) {
const stats = await stat(dir);
if (!stats.isDirectory())
log.warn('Default directory exists but is not a directory: ' + dir);
}
else
await mkdir(dir);
}
}
}
/**
* @experimental
* Configures ZenFS with `configuration`
* @category Backends and Configuration
* @see Configuration
*/
export function configureSync(configuration) {
Object.assign(defaultContext.credentials, createCredentials({
uid: configuration.uid || 0,
gid: configuration.gid || 0,
}));
_setAccessChecks(!configuration.disableAccessChecks);
if (configuration.log)
log.configure(configuration.log);
if (configuration.mounts) {
for (const [_point, mountConfig] of Object.entries(configuration.mounts).sort(([a], [b]) => (a.length > b.length ? 1 : -1))) {
const point = _point.startsWith('/') ? _point : '/' + _point;
if (isBackendConfig(mountConfig)) {
mountConfig.disableAsyncCache ??= configuration.disableAsyncCache || false;
mountConfig.caseFold ??= configuration.caseFold;
}
if (point == '/')
umount('/');
mountWithMkdirSync(point, resolveMountConfigSync(mountConfig));
}
}
for (const fs of mounts.values()) {
configureFileSystem(fs, configuration);
}
if (configuration.addDevices && !mounts.has('/dev')) {
const devfs = new DeviceFS();
devfs.addDefaults();
devfs.readySync();
mountWithMkdirSync('/dev', devfs);
}
if (configuration.defaultDirectories) {
for (const dir of _defaultDirectories) {
if (existsSync(dir)) {
const stats = statSync(dir);
if (!stats.isDirectory())
log.warn('Default directory exists but is not a directory: ' + dir);
continue;
}
mkdirSync(dir);
}
}
}
export async function sync() {
for (const fs of mounts.values())
await fs.sync();
}