UNPKG

browserfs

Version:

A filesystem in your browser!

359 lines (328 loc) 10.9 kB
import {FileSystem, SynchronousFileSystem, BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import {ApiError, ErrorCode} from '../core/api_error'; import {FileFlag} from '../core/file_flag'; import {File} from '../core/file'; import Stats from '../core/node_fs_stats'; import PreloadFile from '../generic/preload_file'; import {deprecationMessage} from '../core/util'; import * as path from 'path'; /** * @hidden */ interface IAsyncOperation { apiMethod: string; arguments: any[]; } /** * We define our own file to interpose on syncSync() for mirroring purposes. */ class MirrorFile extends PreloadFile<AsyncMirror> implements File { constructor(fs: AsyncMirror, path: string, flag: FileFlag, stat: Stats, data: Buffer) { super(fs, path, flag, stat, data); } public syncSync(): void { if (this.isDirty()) { this._fs._syncSync(this); this.resetDirty(); } } public closeSync(): void { this.syncSync(); } } /** * Configuration options for the AsyncMirror file system. */ export interface AsyncMirrorOptions { // The synchronous file system to mirror the asynchronous file system to. sync: FileSystem; // The asynchronous file system to mirror. async: FileSystem; } /** * AsyncMirrorFS mirrors a synchronous filesystem into an asynchronous filesystem * by: * * * Performing operations over the in-memory copy, while asynchronously pipelining them * to the backing store. * * During application loading, the contents of the async file system can be reloaded into * the synchronous store, if desired. * * The two stores will be kept in sync. The most common use-case is to pair a synchronous * in-memory filesystem with an asynchronous backing store. * * Example: Mirroring an IndexedDB file system to an in memory file system. Now, you can use * IndexedDB synchronously. * * ```javascript * BrowserFS.configure({ * fs: "AsyncMirror", * options: { * sync: { fs: "InMemory" }, * async: { fs: "IndexedDB" } * } * }, function(e) { * // BrowserFS is initialized and ready-to-use! * }); * ``` * * Or, alternatively: * * ```javascript * BrowserFS.FileSystem.IndexedDB.Create(function(e, idbfs) { * BrowserFS.FileSystem.InMemory.Create(function(e, inMemory) { * BrowserFS.FileSystem.AsyncMirror({ * sync: inMemory, async: idbfs * }, function(e, mirrored) { * BrowserFS.initialize(mirrored); * }); * }); * }); * ``` */ export default class AsyncMirror extends SynchronousFileSystem implements FileSystem { public static readonly Name = "AsyncMirror"; public static readonly Options: FileSystemOptions = { sync: { type: "object", description: "The synchronous file system to mirror the asynchronous file system to." }, async: { type: "object", description: "The asynchronous file system to mirror." } }; /** * Constructs and initializes an AsyncMirror file system with the given options. */ public static Create(opts: AsyncMirrorOptions, cb: BFSCallback<AsyncMirror>): void { try { const fs = new AsyncMirror(opts.sync, opts.async, false); fs.initialize((e?) => { if (e) { cb(e); } else { cb(null, fs); } }, false); } catch (e) { cb(e); } } public static isAvailable(): boolean { return true; } /** * Queue of pending asynchronous operations. */ private _queue: IAsyncOperation[] = []; private _queueRunning: boolean = false; private _sync: FileSystem; private _async: FileSystem; private _isInitialized: boolean = false; private _initializeCallbacks: ((e?: ApiError) => void)[] = []; /** * **Deprecated; use AsyncMirror.Create() method instead.** * * Mirrors the synchronous file system into the asynchronous file system. * * **IMPORTANT**: You must call `initialize` on the file system before it can be used. * @param sync The synchronous file system to mirror the asynchronous file system to. * @param async The asynchronous file system to mirror. */ constructor(sync: FileSystem, async: FileSystem, deprecateMsg = true) { super(); this._sync = sync; this._async = async; if (!sync.supportsSynch()) { throw new Error("The first argument to AsyncMirror needs to be a synchronous file system."); } deprecationMessage(deprecateMsg, AsyncMirror.Name, { sync: "sync file system instance", async: "async file system instance"}); } public getName(): string { return AsyncMirror.Name; } public _syncSync(fd: PreloadFile<any>) { this._sync.writeFileSync(fd.getPath(), fd.getBuffer(), null, FileFlag.getFileFlag('w'), fd.getStats().mode); this.enqueueOp({ apiMethod: 'writeFile', arguments: [fd.getPath(), fd.getBuffer(), null, fd.getFlag(), fd.getStats().mode] }); } /** * Called once to load up files from async storage into sync storage. */ public initialize(userCb: BFSOneArgCallback, deprecateMsg = true): void { if (deprecateMsg) { console.warn(`[AsyncMirror] AsyncMirror.initialize() is deprecated and will be removed in the next major version. Please use 'AsyncMirror.Create({ sync: (sync file system instance), async: (async file system instance)}, cb)' to create and initialize AsyncMirror instances.`); } const callbacks = this._initializeCallbacks; const end = (e?: ApiError): void => { this._isInitialized = !e; this._initializeCallbacks = []; callbacks.forEach((cb) => cb(e)); }; if (!this._isInitialized) { // First call triggers initialization, the rest wait. if (callbacks.push(userCb) === 1) { const copyDirectory = (p: string, mode: number, cb: BFSOneArgCallback) => { if (p !== '/') { this._sync.mkdirSync(p, mode); } this._async.readdir(p, (err, files) => { let i = 0; // NOTE: This function must not be in a lexically nested statement, // such as an if or while statement. Safari refuses to run the // script since it is undefined behavior. function copyNextFile(err?: ApiError) { if (err) { cb(err); } else if (i < files!.length) { copyItem(path.join(p, files![i]), copyNextFile); i++; } else { cb(); } } if (err) { cb(err); } else { copyNextFile(); } }); }, copyFile = (p: string, mode: number, cb: BFSOneArgCallback) => { this._async.readFile(p, null, FileFlag.getFileFlag('r'), (err, data) => { if (err) { cb(err); } else { try { this._sync.writeFileSync(p, data!, null, FileFlag.getFileFlag('w'), mode); } catch (e) { err = e; } finally { cb(err); } } }); }, copyItem = (p: string, cb: BFSOneArgCallback) => { this._async.stat(p, false, (err, stats) => { if (err) { cb(err); } else if (stats!.isDirectory()) { copyDirectory(p, stats!.mode, cb); } else { copyFile(p, stats!.mode, cb); } }); }; copyDirectory('/', 0, end); } } else { userCb(); } } public isReadOnly(): boolean { return false; } public supportsSynch(): boolean { return true; } public supportsLinks(): boolean { return false; } public supportsProps(): boolean { return this._sync.supportsProps() && this._async.supportsProps(); } public renameSync(oldPath: string, newPath: string): void { this.checkInitialized(); this._sync.renameSync(oldPath, newPath); this.enqueueOp({ apiMethod: 'rename', arguments: [oldPath, newPath] }); } public statSync(p: string, isLstat: boolean): Stats { this.checkInitialized(); return this._sync.statSync(p, isLstat); } public openSync(p: string, flag: FileFlag, mode: number): File { this.checkInitialized(); // Sanity check: Is this open/close permitted? const fd = this._sync.openSync(p, flag, mode); fd.closeSync(); return new MirrorFile(this, p, flag, this._sync.statSync(p, false), this._sync.readFileSync(p, null, FileFlag.getFileFlag('r'))); } public unlinkSync(p: string): void { this.checkInitialized(); this._sync.unlinkSync(p); this.enqueueOp({ apiMethod: 'unlink', arguments: [p] }); } public rmdirSync(p: string): void { this.checkInitialized(); this._sync.rmdirSync(p); this.enqueueOp({ apiMethod: 'rmdir', arguments: [p] }); } public mkdirSync(p: string, mode: number): void { this.checkInitialized(); this._sync.mkdirSync(p, mode); this.enqueueOp({ apiMethod: 'mkdir', arguments: [p, mode] }); } public readdirSync(p: string): string[] { this.checkInitialized(); return this._sync.readdirSync(p); } public existsSync(p: string): boolean { this.checkInitialized(); return this._sync.existsSync(p); } public chmodSync(p: string, isLchmod: boolean, mode: number): void { this.checkInitialized(); this._sync.chmodSync(p, isLchmod, mode); this.enqueueOp({ apiMethod: 'chmod', arguments: [p, isLchmod, mode] }); } public chownSync(p: string, isLchown: boolean, uid: number, gid: number): void { this.checkInitialized(); this._sync.chownSync(p, isLchown, uid, gid); this.enqueueOp({ apiMethod: 'chown', arguments: [p, isLchown, uid, gid] }); } public utimesSync(p: string, atime: Date, mtime: Date): void { this.checkInitialized(); this._sync.utimesSync(p, atime, mtime); this.enqueueOp({ apiMethod: 'utimes', arguments: [p, atime, mtime] }); } private checkInitialized(): void { if (!this._isInitialized) { throw new ApiError(ErrorCode.EPERM, "AsyncMirrorFS is not initialized. Please initialize AsyncMirrorFS using its initialize() method before using it."); } } private enqueueOp(op: IAsyncOperation) { this._queue.push(op); if (!this._queueRunning) { this._queueRunning = true; const doNextOp = (err?: ApiError) => { if (err) { console.error(`WARNING: File system has desynchronized. Received following error: ${err}\n$`); } if (this._queue.length > 0) { const op = this._queue.shift()!, args = op.arguments; args.push(doNextOp); (<Function> (<any> this._async)[op.apiMethod]).apply(this._async, args); } else { this._queueRunning = false; } }; doNextOp(); } } }