UNPKG

@zenfs/core

Version:

A filesystem, anywhere

209 lines (183 loc) 5.98 kB
import { join } from '../emulation/path.js'; import { Errno, ErrnoError } from '../error.js'; import { parseFlag, PreloadFile, type File } from '../file.js'; import type { FileSystem } from '../filesystem.js'; import type { Stats } from '../stats.js'; import type { AsyncFSMethods, Mixin } from './shared.js'; /** @internal */ export type AsyncOperation = { [K in keyof AsyncFSMethods]: [K, ...Parameters<FileSystem[K]>]; }[keyof AsyncFSMethods]; /** * Async() implements synchronous methods on an asynchronous file system * * Implementing classes must define `_sync` for the synchronous file system used as a cache. * * Synchronous methods on an asynchronous FS are implemented by performing operations over the in-memory copy, * while asynchronously pipelining them to the backing store. * During loading, the contents of the async file system are preloaded into the synchronous store. * */ export function Async<T extends typeof FileSystem>( FS: T ): Mixin< T, { /** * @internal @protected */ _sync?: FileSystem; queueDone(): Promise<void>; ready(): Promise<void>; renameSync(oldPath: string, newPath: string): void; statSync(path: string): Stats; createFileSync(path: string, flag: string, mode: number): File; openFileSync(path: string, flag: string): File; unlinkSync(path: string): void; rmdirSync(path: string): void; mkdirSync(path: string, mode: number): void; readdirSync(path: string): string[]; linkSync(srcpath: string, dstpath: string): void; syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void; } > { abstract class AsyncFS extends FS { /** * Queue of pending asynchronous operations. */ private _queue: AsyncOperation[] = []; private get _queueRunning(): boolean { return !!this._queue.length; } public queueDone(): Promise<void> { return new Promise(resolve => { const check = (): unknown => (this._queueRunning ? setTimeout(check) : resolve()); check(); }); } private _isInitialized: boolean = false; abstract _sync?: FileSystem; public async ready(): Promise<void> { await super.ready(); if (this._isInitialized || this._disableSync) { return; } this.checkSync(); await this._sync.ready(); try { await this.crossCopy('/'); this._isInitialized = true; } catch (e) { this._isInitialized = false; throw e; } } protected checkSync(path?: string, syscall?: string): asserts this is { _sync: FileSystem } { if (this._disableSync) { throw new ErrnoError(Errno.ENOTSUP, 'Sync caching has been disabled for this async file system', path, syscall); } if (!this._sync) { throw new ErrnoError(Errno.ENOTSUP, 'No sync cache is attached to this async file system', path, syscall); } } public renameSync(oldPath: string, newPath: string): void { this.checkSync(oldPath, 'rename'); this._sync.renameSync(oldPath, newPath); this.queue('rename', oldPath, newPath); } public statSync(path: string): Stats { this.checkSync(path, 'stat'); return this._sync.statSync(path); } public createFileSync(path: string, flag: string, mode: number): PreloadFile<this> { this.checkSync(path, 'createFile'); this._sync.createFileSync(path, flag, mode); this.queue('createFile', path, flag, mode); return this.openFileSync(path, flag); } public openFileSync(path: string, flag: string): PreloadFile<this> { this.checkSync(path, 'openFile'); const file = this._sync.openFileSync(path, flag); const stats = file.statSync(); const buffer = new Uint8Array(stats.size); file.readSync(buffer); return new PreloadFile(this, path, flag, stats, buffer); } public unlinkSync(path: string): void { this.checkSync(path, 'unlinkSync'); this._sync.unlinkSync(path); this.queue('unlink', path); } public rmdirSync(path: string): void { this.checkSync(path, 'rmdir'); this._sync.rmdirSync(path); this.queue('rmdir', path); } public mkdirSync(path: string, mode: number): void { this.checkSync(path, 'mkdir'); this._sync.mkdirSync(path, mode); this.queue('mkdir', path, mode); } public readdirSync(path: string): string[] { this.checkSync(path, 'readdir'); return this._sync.readdirSync(path); } public linkSync(srcpath: string, dstpath: string): void { this.checkSync(srcpath, 'link'); this._sync.linkSync(srcpath, dstpath); this.queue('link', srcpath, dstpath); } public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void { this.checkSync(path, 'sync'); this._sync.syncSync(path, data, stats); this.queue('sync', path, data, stats); } public existsSync(path: string): boolean { this.checkSync(path, 'exists'); return this._sync.existsSync(path); } /** * @internal */ protected async crossCopy(path: string): Promise<void> { this.checkSync(path, 'crossCopy'); const stats = await this.stat(path); if (!stats.isDirectory()) { await using asyncFile = await this.openFile(path, parseFlag('r')); using syncFile = this._sync.createFileSync(path, parseFlag('w'), stats.mode); const buffer = new Uint8Array(stats.size); await asyncFile.read(buffer); syncFile.writeSync(buffer, 0, stats.size); return; } if (path !== '/') { const stats = await this.stat(path); this._sync.mkdirSync(path, stats.mode); } const files = await this.readdir(path); for (const file of files) { await this.crossCopy(join(path, file)); } } /** * @internal */ private async _next(): Promise<void> { if (!this._queueRunning) { return; } const [method, ...args] = this._queue.shift()!; // @ts-expect-error 2556 (since ...args is not correctly picked up as being a tuple) await this[method](...args); await this._next(); } /** * @internal */ private queue(...op: AsyncOperation) { this._queue.push(op); void this._next(); } } return AsyncFS; }