UNPKG

@zenfs/core

Version:

A filesystem, anywhere

298 lines (297 loc) 10.7 kB
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(); }