Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions documentation/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ await configure({
});
```

## Synchronous configuration

If you require configuration to be done synchronously, you can use `configureSync`. Note that will throw `EWOULDBLOCK`/`EAGAIN` when asynchronous backends are encountered.

```ts
import { configureSync, InMemory } from '@zenfs/core';

configureSync({
mounts: {
'/tmp': { backend: InMemory, label: 'temp-storage' },
},
});
```

Backends that do all their work eagerly, such as `InMemory` and `SingleBuffer`, are designed to work with these synchronous configuration helpers.

For single-mount scenarios there are matching helpers: use `configureSingle` (or `configureSingleSync`) to replace the root mount without providing a full configuration object.

## Mounting File Systems and `resolveMountConfig`

Mounting file systems in ZenFS is handled dynamically. When a mount configuration is provided, it is processed using `resolveMountConfig`, which determines how the backend should be initialized and mounted.
Expand All @@ -39,6 +57,8 @@ const tmpfs = await resolveMountConfig({
mount('/mnt/tmp', tmpfs);
```

When dealing exclusively with synchronous backends, you can call `resolveMountConfigSync`. It performs the same validation, but it throws if a backend performs any asynchronous work during creation or readiness.

### Dynamic Mounting

Mounts can be resolved dynamically at runtime, allowing flexibility when configuring storage. This is especially useful for:
Expand Down
11 changes: 8 additions & 3 deletions src/backends/cow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import { dirname, join } from '../path.js';
*/
export interface CopyOnWriteOptions {
/** The file system that initially populates this file system. */
readable: MountConfiguration<any>;
readable: MountConfiguration<Backend>;

/** The file system to write modified files to. */
writable: MountConfiguration<any>;
writable: MountConfiguration<Backend>;

/** @see {@link Journal} */
journal?: Journal;
Expand All @@ -37,7 +37,7 @@ export type JournalOperation = (typeof journalOperations)[number];

/** Because TS doesn't work right w/o it */
function isJournalOp(op: string): op is JournalOperation {
return journalOperations.includes(op as any);
return journalOperations.some(operation => operation === op);
}

const maxOpLength = Math.max(...journalOperations.map(op => op.length));
Expand Down Expand Up @@ -129,6 +129,11 @@ export class CopyOnWriteFS extends FileSystem {
await this.writable.ready();
}

public readySync(): void {
this.readable.readySync();
this.writable.readySync();
}

public constructor(
/** The file system that initially populates this file system. */
public readonly readable: FileSystem,
Expand Down
80 changes: 80 additions & 0 deletions src/backends/store/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
this._initialized = true;
}

public readySync(): void {
if (this._initialized) return;

if (!this.attributes.has('no_async_preload')) {
this.checkRootSync();
}

this.checkRootSync();
this._populateSync();
this._initialized = true;
}

public constructor(protected readonly store: T) {
super(store.type ?? 0x6b766673, store.name);
store.fs = this;
Expand Down Expand Up @@ -617,6 +629,74 @@ export class StoreFS<T extends Store = Store> extends FileSystem {
debug(`Added ${i} existing inode(s) from store`);
}

private _populateSync(): void {
if (this._initialized) {
warn('Attempted to populate tables after initialization');
return;
}
debug('Populating tables with existing store metadata');
using tx = this.transaction();

const rootData = tx.getSync(rootIno);
if (!rootData) {
notice('Store does not have a root inode');
const inode = new Inode({ ino: rootIno, data: 1, mode: 0o777 | S_IFDIR });
tx.setSync(inode.data, encodeUTF8('{}'));
this._add(rootIno, '/');
tx.setSync(rootIno, inode);
tx.commitSync();
return;
}

if (rootData.length < sizeof(Inode)) {
crit('Store contains an invalid root inode. Refusing to populate tables');
return;
}

const visitedDirectories = new Set<number>();
let i = 0;
const queue: Array<[path: string, ino: number]> = [['/', rootIno]];

while (queue.length > 0) {
i++;
const [path, ino] = queue.shift()!;

this._add(ino, path);

const inodeData = tx.getSync(ino);
if (!inodeData) {
warn('Store is missing data for inode: ' + ino);
continue;
}

if (inodeData.length < sizeof(Inode)) {
warn(`Invalid inode size for ino ${ino}: ${inodeData.length}`);
continue;
}

const inode = new Inode(inodeData);

if ((inode.mode & S_IFDIR) != S_IFDIR || visitedDirectories.has(ino)) {
continue;
}

visitedDirectories.add(ino);

const dirData = tx.getSync(inode.data);
if (!dirData) {
warn('Store is missing directory data: ' + inode.data);
continue;
}
const dirListing = decodeDirListing(dirData);

for (const [entryName, childIno] of Object.entries(dirListing)) {
queue.push([join(path, entryName), childIno]);
}
}

debug(`Added ${i} existing inode(s) from store`);
}

/**
* Find an inode without using the ID tables
*/
Expand Down
164 changes: 164 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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';

Expand All @@ -31,6 +32,10 @@
return isBackendConfig(arg) || isBackend(arg) || arg instanceof FileSystem;
}

function isThenable(value: unknown): value is PromiseLike<unknown> {
return typeof (value as PromiseLike<unknown>)?.then == 'function';
}

/**
* Retrieve a file system with `configuration`.
* @category Backends and Configuration
Expand Down Expand Up @@ -80,6 +85,66 @@
return mount;
}

/**
* @experimental
* Retrieve a file system with `configuration`.
* @category Backends and Configuration
* @see MountConfiguration
*/
export function resolveMountConfigSync<T extends Backend>(configuration: MountConfiguration<T>, _depth = 0): FilesystemOf<T> {
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 } as BackendConfiguration<T>;
}

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 as Record<string, FileSystem>)[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 as FilesystemOf<T>;
configureFileSystem(resolved, configuration);
resolved.readySync();
return resolved;
}

/**
* An object mapping mount points to backends
* @category Backends and Configuration
Expand Down Expand Up @@ -160,6 +225,21 @@
mount('/', resolved);
}

/**
* @experimental
* Configures ZenFS with single mount point /
* @category Backends and Configuration
*/
export function configureSingleSync<T extends Backend>(configuration: MountConfiguration<T>): void {
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
Expand All @@ -181,6 +261,33 @@
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: string, fs: FileSystem): void {
if (path == '/') {
mount(path, fs);
return;
}

let stats: { isDirectory(): boolean } | null = null;
try {
stats = statSync(path);
} catch (error: any) {

Check warning on line 279 in src/config.ts

View workflow job for this annotation

GitHub Actions / Continuous Integration

Unexpected any. Specify a different type
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
*/
Expand Down Expand Up @@ -247,6 +354,63 @@
}
}

/**
* @experimental
* Configures ZenFS with `configuration`
* @category Backends and Configuration
* @see Configuration
*/
export function configureSync<T extends ConfigMounts>(configuration: Partial<Configuration<T>>): void {
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(): Promise<void> {
for (const fs of mounts.values()) await fs.sync();
}
4 changes: 4 additions & 0 deletions src/internal/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { UUID } from 'node:crypto';
import type { ConstMap } from 'utilium';
import type { InodeLike } from './inode.js';
import { withErrno } from 'kerium';

/**
* Usage information about a file system
Expand Down Expand Up @@ -208,6 +209,9 @@ export abstract class FileSystem {
}

public async ready(): Promise<void> {}
public readySync(): void {
if (this.ready !== FileSystem.prototype.ready) throw withErrno('EAGAIN');
}

public abstract rename(oldPath: string, newPath: string): Promise<void>;
public abstract renameSync(oldPath: string, newPath: string): void;
Expand Down
4 changes: 4 additions & 0 deletions src/mixins/mutexed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export class _MutexedFS<T extends FileSystem> implements FileSystem {
return await this._fs.ready();
}

public readySync(): void {
return this._fs.readySync();
}

public usage(): UsageInfo {
return this._fs.usage();
}
Expand Down
Loading
Loading