packages/web-ide-fs/src/browserfs/OverlayFS.ts (141 lines of code) (raw):

/* eslint-disable class-methods-use-this, max-classes-per-file */ /** * Inspired (but not copied) from https://github.com/jvilk/BrowserFS/blob/v1.4.3/src/backend/OverlayFS.ts * * For the parts that are *very* inspired: * * ==== * * Copyright (c) 2013, 2014, 2015, 2016, 2017 John Vilk and other BrowserFS contributors. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * ==== */ import './shim'; import type { File } from 'browserfs/dist/node/core/file'; import type { FileFlag } from 'browserfs/dist/node/core/file_flag'; import type { BFSCallback, BFSOneArgCallback, FileSystem, } from 'browserfs/dist/node/core/file_system'; import { BaseFileSystem } from 'browserfs/dist/node/core/file_system'; import type Stats from 'browserfs/dist/node/core/node_fs_stats'; import LockedFS from 'browserfs/dist/node/generic/locked_fs'; import PreloadFile from 'browserfs/dist/node/generic/preload_file'; import { OverlayFSImpl } from './OverlayFSImpl'; import type { DeletedFilesLog } from './typesOverlayFS'; import type { PromisifiedBrowserFS, ReadonlyPromisifiedBrowserFS } from './types'; const NAME = 'OverlayFS'; /** * `FileSystem` with a `sync` method that can be used by a `File` * * why: This interface is needed to prevent circular dependencies and * implement `OverlayFile` which is needed to implelment FileSystem's * `open(...)` interface. */ interface FileSystemWithSync extends FileSystem { sync(file: PreloadFile<FileSystemWithSync>, cb: BFSOneArgCallback): void; } /** * Overlays a read-only file to make it writable to the OverlayFS. * * Origina impl https://github.com/jvilk/BrowserFS/blob/v1.4.3/src/backend/OverlayFS.ts#L33 */ class OverlayFile extends PreloadFile<FileSystemWithSync> implements File { static createFactory(fs: FileSystemWithSync) { return (path: string, flag: FileFlag, stats: Stats, data: Buffer) => new OverlayFile(fs, path, flag, stats, data); } sync(cb: BFSOneArgCallback): void { if (!this.isDirty()) { cb(null); return; } this._fs.sync(this, err => { this.resetDirty(); cb(err); }); } close(cb: BFSOneArgCallback): void { this.sync(cb); } } /** * Internal OverlayFS class that implements FileSystem interface using OverlayFSImpl * under-the-hood. * * For Overlay File System behavior specifics, see [OverlayFSImpl](./OverlayFSImpl.ts). * ## Why split into a separate implementation class? * * The OverlayFS interface requires using callbacks. This is * unwieldy. Let's write a promise based implementation which we'll use * under-the-hood and map to callbacks in `OverlayFS.ts`. * * Originam impl. https://github.com/jvilk/BrowserFS/blob/v1.4.3/src/backend/OverlayFS.ts#L72 * * ## PLEASE NOTE * * Some methods from original implementation are not included. These methods * are not supported by [BrowserFS's InMemoryFileSystem][0], so we can't support * them here: * * - `utimes` * - `chmod` * - `chown` * * [0]: https://github.com/jvilk/BrowserFS/blob/v1.4.3/src/generic/key_value_filesystem.ts#L265 */ class UnlockedOverlayFS extends BaseFileSystem implements FileSystemWithSync { readonly #impl: OverlayFSImpl; constructor( writable: PromisifiedBrowserFS, deletedFilesLog: DeletedFilesLog, readable: ReadonlyPromisifiedBrowserFS, ) { super(); this.#impl = new OverlayFSImpl( writable, deletedFilesLog, readable, OverlayFile.createFactory(this), ); } getName(): string { return NAME; } isReadOnly(): boolean { return false; } supportsLinks(): boolean { return false; } supportsProps(): boolean { return this.#impl.supportsProps(); } supportsSynch(): boolean { // why: Unlike the original implementation, let's not support the duplication of sync operations!! return false; } exists(p: string, cb: (exists: boolean) => void): void { // TODO: Generally .exists is not preferred in NodeJS file system API // A race condition can happen between checking if exists and performing // the actual operation. // https://nodejs.org/docs/latest-v16.x/api/fs.html#fsexistspath-callback this.#impl.exists(p).then( exists => cb(exists), // note: this shouldn't ever happen. See above TODO. () => cb(false), ); } mkdir(p: string, mode: number, cb: BFSOneArgCallback): void { this.#impl.mkdir(p, mode).then( () => cb(), e => cb(e), ); } readdir(p: string, cb: BFSCallback<string[]>): void { this.#impl.readdir(p).then( x => cb(null, x), e => cb(e), ); } rename(oldPath: string, newPath: string, cb: BFSOneArgCallback): void { this.#impl.rename(oldPath, newPath).then( () => cb(), e => cb(e), ); } rmdir(p: string, cb: BFSOneArgCallback): void { this.#impl.rmdir(p).then( () => cb(), e => cb(e), ); } stat(p: string, isLstat: boolean | null, cb: BFSCallback<Stats>): void { this.#impl.stat(p, isLstat).then( x => cb(null, x), e => cb(e), ); } unlink(p: string, cb: BFSOneArgCallback): void { this.#impl.unlink(p).then( () => cb(), e => cb(e), ); } open(p: string, flag: FileFlag, mode: number, cb: BFSCallback<File>): void { this.#impl.open(p, flag, mode).then( file => cb(null, file), e => cb(e), ); } sync(file: PreloadFile<FileSystemWithSync>, cb: BFSOneArgCallback): void { this.#impl.sync(file).then( () => cb(), e => cb(e), ); } } /** * Configuration options for OverlayFS instances. * * Original impl. https://github.com/jvilk/BrowserFS/blob/v1.4.3/src/backend/OverlayFS.ts#L977 */ export interface OverlayFSOptions { // The file system to write modified files to. writable: PromisifiedBrowserFS; // The interface for keeping track of deleted files deletedFilesLog: DeletedFilesLog; // The file system that initially populates this file system. readable: ReadonlyPromisifiedBrowserFS; } /** * OverlayFS makes a read-only filesystem writable by storing writes on a second, * writable file system. Deletes are persisted via metadata stored on the writable * file system. * * Original impl. https://github.com/jvilk/BrowserFS/blob/v1.4.3/src/backend/OverlayFS.ts#L989 */ export default class OverlayFS extends LockedFS<UnlockedOverlayFS> { /** * Constructs and initializes an OverlayFS instance with the given options. */ public static Create(opts: OverlayFSOptions, cb: BFSCallback<OverlayFS>): void { const fs = new OverlayFS(opts.writable, opts.deletedFilesLog, opts.readable); cb(null, fs); } // eslint-disable-next-line no-restricted-syntax private constructor( writable: PromisifiedBrowserFS, deletedFilesLog: DeletedFilesLog, readable: ReadonlyPromisifiedBrowserFS, ) { super(new UnlockedOverlayFS(writable, deletedFilesLog, readable)); } }