UNPKG

@zenfs/core

Version:

A filesystem, anywhere

336 lines (279 loc) 8.67 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import type { FileReadResult } from 'node:fs/promises'; import type { ExtractProperties } from 'utilium'; import { resolveMountConfig, type MountConfiguration } from '../../config.js'; import { Errno, ErrnoError } from '../../error.js'; import { File } from '../../file.js'; import { FileSystem, type FileSystemMetadata } from '../../filesystem.js'; import { Async } from '../../mixins/async.js'; import { Stats, type FileType } from '../../stats.js'; import type { Backend, FilesystemOf } from '../backend.js'; import { InMemory } from '../memory.js'; import * as RPC from './rpc.js'; type FileMethods = Omit<ExtractProperties<File, (...args: any[]) => Promise<any>>, typeof Symbol.asyncDispose>; type FileMethod = keyof FileMethods; /** @internal */ export interface FileRequest<TMethod extends FileMethod = FileMethod> extends RPC.Request { fd: number; scope: 'file'; method: TMethod; args: Parameters<FileMethods[TMethod]>; } export class PortFile extends File { public constructor( public fs: PortFS, public readonly fd: number, path: string, public position: number ) { super(fs, path); } public rpc<const T extends FileMethod>(method: T, ...args: Parameters<FileMethods[T]>): Promise<Awaited<ReturnType<FileMethods[T]>>> { return RPC.request<FileRequest<T>, Awaited<ReturnType<FileMethods[T]>>>( { scope: 'file', fd: this.fd, method, args, }, this.fs.options ); } protected _throwNoSync(syscall: string): never { throw new ErrnoError(Errno.ENOTSUP, 'Synchronous operations not supported on PortFile', this.path, syscall); } public async stat(): Promise<Stats> { return new Stats(await this.rpc('stat')); } public statSync(): Stats { this._throwNoSync('stat'); } public truncate(len: number): Promise<void> { return this.rpc('truncate', len); } public truncateSync(): void { this._throwNoSync('truncate'); } public write(buffer: Uint8Array, offset?: number, length?: number, position?: number): Promise<number> { return this.rpc('write', buffer, offset, length, position); } public writeSync(): number { this._throwNoSync('write'); } public async read<TBuffer extends NodeJS.ArrayBufferView>(buffer: TBuffer, offset?: number, length?: number, position?: number): Promise<FileReadResult<TBuffer>> { const result = await this.rpc('read', buffer, offset, length, position); return result as FileReadResult<TBuffer>; } public readSync(): number { this._throwNoSync('read'); } public chown(uid: number, gid: number): Promise<void> { return this.rpc('chown', uid, gid); } public chownSync(): void { this._throwNoSync('chown'); } public chmod(mode: number): Promise<void> { return this.rpc('chmod', mode); } public chmodSync(): void { this._throwNoSync('chmod'); } public utimes(atime: Date, mtime: Date): Promise<void> { return this.rpc('utimes', atime, mtime); } public utimesSync(): void { this._throwNoSync('utimes'); } public _setType(type: FileType): Promise<void> { return this.rpc('_setType', type); } public _setTypeSync(): void { this._throwNoSync('_setType'); } public close(): Promise<void> { return this.rpc('close'); } public closeSync(): void { this._throwNoSync('close'); } public sync(): Promise<void> { return this.rpc('sync'); } public syncSync(): void { this._throwNoSync('sync'); } } type FSMethods = ExtractProperties<FileSystem, (...args: any[]) => Promise<any> | FileSystemMetadata>; type FSMethod = keyof FSMethods; /** @internal */ export interface FSRequest<TMethod extends FSMethod = FSMethod> extends RPC.Request { scope: 'fs'; method: TMethod; args: Parameters<FSMethods[TMethod]>; } /** * PortFS lets you access an FS instance that is running in a port, or the other way around. * * Note that *direct* synchronous operations are not permitted on the PortFS, * regardless of the configuration option of the remote FS. */ export class PortFS extends Async(FileSystem) { public readonly port: RPC.Port; /** * @hidden */ _sync = InMemory.create({ name: 'port-tmpfs' }); /** * Constructs a new PortFS instance that connects with the FS running on `options.port`. */ public constructor(public readonly options: RPC.Options) { super(); this.port = options.port; RPC.attach<RPC.Response>(this.port, RPC.handleResponse); } public metadata(): FileSystemMetadata { return { ...super.metadata(), name: 'PortFS', }; } protected rpc<const T extends FSMethod>(method: T, ...args: Parameters<FSMethods[T]>): Promise<Awaited<ReturnType<FSMethods[T]>>> { return RPC.request<FSRequest<T>, Awaited<ReturnType<FSMethods[T]>>>( { scope: 'fs', method, args, }, { ...this.options, fs: this } ); } public async ready(): Promise<void> { await this.rpc('ready'); await super.ready(); } public rename(oldPath: string, newPath: string): Promise<void> { return this.rpc('rename', oldPath, newPath); } public async stat(path: string): Promise<Stats> { return new Stats(await this.rpc('stat', path)); } public sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> { return this.rpc('sync', path, data, stats); } public openFile(path: string, flag: string): Promise<File> { return this.rpc('openFile', path, flag); } public createFile(path: string, flag: string, mode: number): Promise<File> { return this.rpc('createFile', path, flag, mode); } public unlink(path: string): Promise<void> { return this.rpc('unlink', path); } public rmdir(path: string): Promise<void> { return this.rpc('rmdir', path); } public mkdir(path: string, mode: number): Promise<void> { return this.rpc('mkdir', path, mode); } public readdir(path: string): Promise<string[]> { return this.rpc('readdir', path); } public exists(path: string): Promise<boolean> { return this.rpc('exists', path); } public link(srcpath: string, dstpath: string): Promise<void> { return this.rpc('link', srcpath, dstpath); } } let nextFd = 0; const descriptors: Map<number, File> = new Map(); /** @internal */ export type FileOrFSRequest = FSRequest | FileRequest; /** @internal */ export async function handleRequest(port: RPC.Port, fs: FileSystem, request: FileOrFSRequest): Promise<void> { if (!RPC.isMessage(request)) { return; } const { method, args, id, scope, stack } = request; let value, error: boolean = false; try { switch (scope) { case 'fs': // @ts-expect-error 2556 value = await fs[method](...args); if (value instanceof File) { descriptors.set(++nextFd, value); value = { fd: nextFd, path: value.path, position: value.position, }; } break; case 'file': { const { fd } = request; if (!descriptors.has(fd)) { throw new ErrnoError(Errno.EBADF); } // @ts-expect-error 2556 value = await descriptors.get(fd)![method](...args); if (method == 'close') { descriptors.delete(fd); } break; } default: return; } } catch (e) { value = e instanceof ErrnoError ? e.toJSON() : (e as object).toString(); error = true; } port.postMessage({ _zenfs: true, scope, id, error, method, stack, value }); } export function attachFS(port: RPC.Port, fs: FileSystem): void { RPC.attach<FileOrFSRequest>(port, request => handleRequest(port, fs, request)); } export function detachFS(port: RPC.Port, fs: FileSystem): void { RPC.detach<FileOrFSRequest>(port, request => handleRequest(port, fs, request)); } const _Port = { name: 'Port', options: { port: { type: 'object', required: true, description: 'The target port that you want to connect to', validator(port: RPC.Port) { // Check for a `postMessage` function. if (typeof port?.postMessage != 'function') { throw new ErrnoError(Errno.EINVAL, 'option must be a port.'); } }, }, timeout: { type: 'number', required: false, description: 'How long to wait before the request times out', }, }, isAvailable(): boolean { return true; }, create(options: RPC.Options) { return new PortFS(options); }, } satisfies Backend<PortFS, RPC.Options>; type _Port = typeof _Port; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Port extends _Port {} export const Port: Port = _Port; export async function resolveRemoteMount<T extends Backend>(port: RPC.Port, config: MountConfiguration<T>, _depth = 0): Promise<FilesystemOf<T>> { const stopAndReplay = RPC.catchMessages(port); const fs = await resolveMountConfig(config, _depth); attachFS(port, fs); stopAndReplay(fs); return fs; }