UNPKG

browserfs

Version:

A filesystem in your browser!

770 lines (702 loc) 24.5 kB
import file_system = require('../core/file_system'); import {ApiError} from '../core/api_error'; import file_flag = require('../core/file_flag'); import {buffer2ArrayBuffer, arrayBuffer2Buffer} from '../core/util'; import file = require('../core/file'); import {default as Stats, FileType} from '../core/node_fs_stats'; import preload_file = require('../generic/preload_file'); import global = require('../core/global'); import fs = require('../core/node_fs'); interface IBrowserFSMessage { browserfsMessage: boolean; } enum SpecialArgType { // Callback CB, // File descriptor FD, // API error API_ERROR, // Stats object STATS, // Initial probe for file system information. PROBE, // FileFlag object. FILEFLAG, // Buffer object. BUFFER, // Generic Error object. ERROR } interface ISpecialArgument { type: SpecialArgType; } interface IProbeResponse extends ISpecialArgument { isReadOnly: boolean; supportsLinks: boolean; supportsProps: boolean; } interface ICallbackArgument extends ISpecialArgument { // The callback ID. id: number; } /** * Converts callback arguments into ICallbackArgument objects, and back * again. */ class CallbackArgumentConverter { private _callbacks: { [id: number]: Function } = {}; private _nextId: number = 0; public toRemoteArg(cb: Function): ICallbackArgument { var id = this._nextId++; this._callbacks[id] = cb; return { type: SpecialArgType.CB, id: id }; } public toLocalArg(id: number): Function { var cb = this._callbacks[id]; delete this._callbacks[id]; return cb; } } interface IFileDescriptorArgument extends ISpecialArgument { // The file descriptor's id on the remote side. id: number; // The entire file's data, as an array buffer. data: ArrayBuffer; // The file's stat object, as an array buffer. stat: ArrayBuffer; // The path to the file. path: string; // The flag of the open file descriptor. flag: string; } class FileDescriptorArgumentConverter { private _fileDescriptors: { [id: number]: file.File } = {}; private _nextId: number = 0; public toRemoteArg(fd: file.File, p: string, flag: file_flag.FileFlag, cb: (err: ApiError, arg?: IFileDescriptorArgument) => void): void { var id = this._nextId++, data: ArrayBuffer, stat: ArrayBuffer, argsLeft: number = 2; this._fileDescriptors[id] = fd; // Extract needed information asynchronously. fd.stat((err, stats) => { if (err) { cb(err); } else { stat = bufferToTransferrableObject(stats.toBuffer()); // If it's a readable flag, we need to grab contents. if (flag.isReadable()) { fd.read(new Buffer(stats.size), 0, stats.size, 0, (err, bytesRead, buff) => { if (err) { cb(err); } else { data = bufferToTransferrableObject(buff); cb(null, { type: SpecialArgType.FD, id: id, data: data, stat: stat, path: p, flag: flag.getFlagString() }); } }); } else { // File is not readable, which means writing to it will append or // truncate/replace existing contents. Return an empty arraybuffer. cb(null, { type: SpecialArgType.FD, id: id, data: new ArrayBuffer(0), stat: stat, path: p, flag: flag.getFlagString() }); } } }); } private _applyFdChanges(remoteFd: IFileDescriptorArgument, cb: (err: ApiError, fd?: file.File) => void): void { var fd = this._fileDescriptors[remoteFd.id], data = transferrableObjectToBuffer(remoteFd.data), remoteStats = Stats.fromBuffer(transferrableObjectToBuffer(remoteFd.stat)); // Write data if the file is writable. var flag = file_flag.FileFlag.getFileFlag(remoteFd.flag); if (flag.isWriteable()) { // Appendable: Write to end of file. // Writeable: Replace entire contents of file. fd.write(data, 0, data.length, flag.isAppendable() ? fd.getPos() : 0, (e) => { function applyStatChanges() { // Check if mode changed. fd.stat((e, stats?) => { if (e) { cb(e); } else { if (stats.mode !== remoteStats.mode) { fd.chmod(remoteStats.mode, (e: any) => { cb(e, fd); }); } else { cb(e, fd); } } }); } if (e) { cb(e); } else { // If writeable & not appendable, we need to ensure file contents are // identical to those from the remote FD. Thus, we truncate to the // length of the remote file. if (!flag.isAppendable()) { fd.truncate(data.length, () => { applyStatChanges(); }) } else { applyStatChanges(); } } }); } else { cb(null, fd); } } public applyFdAPIRequest(request: IAPIRequest, cb: (err?: ApiError) => void): void { var fdArg = <IFileDescriptorArgument> request.args[0]; this._applyFdChanges(fdArg, (err, fd?) => { if (err) { cb(err); } else { // Apply method on now-changed file descriptor. (<any> fd)[request.method]((e?: ApiError) => { if (request.method === 'close') { delete this._fileDescriptors[fdArg.id]; } cb(e); }); } }); } } interface IAPIErrorArgument extends ISpecialArgument { // The error object, as an array buffer. errorData: ArrayBuffer; } function apiErrorLocal2Remote(e: ApiError): IAPIErrorArgument { return { type: SpecialArgType.API_ERROR, errorData: bufferToTransferrableObject(e.writeToBuffer()) }; } function apiErrorRemote2Local(e: IAPIErrorArgument): ApiError { return ApiError.fromBuffer(transferrableObjectToBuffer(e.errorData)); } interface IErrorArgument extends ISpecialArgument { // The name of the error (e.g. 'TypeError'). name: string; // The message associated with the error. message: string; // The stack associated with the error. stack: string; } function errorLocal2Remote(e: Error): IErrorArgument { return { type: SpecialArgType.ERROR, name: e.name, message: e.message, stack: e.stack }; } function errorRemote2Local(e: IErrorArgument): Error { var cnstr: { new (msg: string): Error; } = global[e.name]; if (typeof(cnstr) !== 'function') { cnstr = Error; } var err = new cnstr(e.message); err.stack = e.stack; return err; } interface IStatsArgument extends ISpecialArgument { // The stats object as an array buffer. statsData: ArrayBuffer; } function statsLocal2Remote(stats: Stats): IStatsArgument { return { type: SpecialArgType.STATS, statsData: bufferToTransferrableObject(stats.toBuffer()) }; } function statsRemote2Local(stats: IStatsArgument): Stats { return Stats.fromBuffer(transferrableObjectToBuffer(stats.statsData)); } interface IFileFlagArgument extends ISpecialArgument { flagStr: string; } function fileFlagLocal2Remote(flag: file_flag.FileFlag): IFileFlagArgument { return { type: SpecialArgType.FILEFLAG, flagStr: flag.getFlagString() }; } function fileFlagRemote2Local(remoteFlag: IFileFlagArgument): file_flag.FileFlag { return file_flag.FileFlag.getFileFlag(remoteFlag.flagStr); } interface IBufferArgument extends ISpecialArgument { data: ArrayBuffer; } function bufferToTransferrableObject(buff: NodeBuffer): ArrayBuffer { return buffer2ArrayBuffer(buff); } function transferrableObjectToBuffer(buff: ArrayBuffer): Buffer { return arrayBuffer2Buffer(buff); } function bufferLocal2Remote(buff: Buffer): IBufferArgument { return { type: SpecialArgType.BUFFER, data: bufferToTransferrableObject(buff) }; } function bufferRemote2Local(buffArg: IBufferArgument): Buffer { return transferrableObjectToBuffer(buffArg.data); } interface IAPIRequest extends IBrowserFSMessage { method: string; args: Array<number | string | ISpecialArgument>; } function isAPIRequest(data: any): data is IAPIRequest { return data != null && typeof data === 'object' && data.hasOwnProperty('browserfsMessage') && data['browserfsMessage']; } interface IAPIResponse extends IBrowserFSMessage { cbId: number; args: Array<number | string | ISpecialArgument>; } function isAPIResponse(data: any): data is IAPIResponse { return data != null && typeof data === 'object' && data.hasOwnProperty('browserfsMessage') && data['browserfsMessage']; } /** * Represents a remote file in a different worker/thread. */ class WorkerFile extends preload_file.PreloadFile<WorkerFS> { private _remoteFdId: number; constructor(_fs: WorkerFS, _path: string, _flag: file_flag.FileFlag, _stat: Stats, remoteFdId: number, contents?: NodeBuffer) { super(_fs, _path, _flag, _stat, contents); this._remoteFdId = remoteFdId; } public getRemoteFdId() { return this._remoteFdId; } public toRemoteArg(): IFileDescriptorArgument { return <IFileDescriptorArgument> { type: SpecialArgType.FD, id: this._remoteFdId, data: bufferToTransferrableObject(this.getBuffer()), stat: bufferToTransferrableObject(this.getStats().toBuffer()), path: this.getPath(), flag: this.getFlag().getFlagString() }; } private _syncClose(type: string, cb: (e?: ApiError) => void): void { if (this.isDirty()) { (<WorkerFS> this._fs).syncClose(type, this, (e?: ApiError) => { if (!e) { this.resetDirty(); } cb(e); }); } else { cb(); } } public sync(cb: (e?: ApiError) => void): void { this._syncClose('sync', cb); } public close(cb: (e?: ApiError) => void): void { this._syncClose('close', cb); } } /** * WorkerFS lets you access a BrowserFS instance that is running in a different * JavaScript context (e.g. access BrowserFS in one of your WebWorkers, or * access BrowserFS running on the main page from a WebWorker). * * For example, to have a WebWorker access files in the main browser thread, * do the following: * * MAIN BROWSER THREAD: * ``` * // Listen for remote file system requests. * BrowserFS.FileSystem.WorkerFS.attachRemoteListener(webWorkerObject); * `` * * WEBWORKER THREAD: * ``` * // Set the remote file system as the root file system. * BrowserFS.initialize(new BrowserFS.FileSystem.WorkerFS(self)); * ``` * * Note that synchronous operations are not permitted on the WorkerFS, regardless * of the configuration option of the remote FS. */ export default class WorkerFS extends file_system.BaseFileSystem implements file_system.FileSystem { private _worker: Worker; private _callbackConverter = new CallbackArgumentConverter(); private _isInitialized: boolean = false; private _isReadOnly: boolean = false; private _supportLinks: boolean = false; private _supportProps: boolean = false; /** * Stores outstanding API requests to the remote BrowserFS instance. */ private _outstandingRequests: { [id: number]: () => void } = {}; /** * Constructs a new WorkerFS instance that connects with BrowserFS running on * the specified worker. */ constructor(worker: Worker) { super(); this._worker = worker; this._worker.addEventListener('message',(e: MessageEvent) => { var resp: Object = e.data; if (isAPIResponse(resp)) { var i: number, args = resp.args, fixedArgs = new Array(args.length); // Dispatch event to correct id. for (i = 0; i < fixedArgs.length; i++) { fixedArgs[i] = this._argRemote2Local(args[i]); } this._callbackConverter.toLocalArg(resp.cbId).apply(null, fixedArgs); } }); } public static isAvailable(): boolean { return typeof(importScripts) !== 'undefined' || typeof(Worker) !== 'undefined'; } public getName(): string { return 'WorkerFS'; } private _argRemote2Local(arg: any): any { if (arg == null) { return arg; } switch (typeof arg) { case 'object': if (arg['type'] != null && typeof arg['type'] === 'number') { var specialArg = <ISpecialArgument> arg; switch (specialArg.type) { case SpecialArgType.API_ERROR: return apiErrorRemote2Local(<IAPIErrorArgument> specialArg); case SpecialArgType.FD: var fdArg = <IFileDescriptorArgument> specialArg; return new WorkerFile(this, fdArg.path, file_flag.FileFlag.getFileFlag(fdArg.flag), Stats.fromBuffer(transferrableObjectToBuffer(fdArg.stat)), fdArg.id, transferrableObjectToBuffer(fdArg.data)); case SpecialArgType.STATS: return statsRemote2Local(<IStatsArgument> specialArg); case SpecialArgType.FILEFLAG: return fileFlagRemote2Local(<IFileFlagArgument> specialArg); case SpecialArgType.BUFFER: return bufferRemote2Local(<IBufferArgument> specialArg); case SpecialArgType.ERROR: return errorRemote2Local(<IErrorArgument> specialArg); default: return arg; } } else { return arg; } default: return arg; } } /** * Converts a local argument into a remote argument. Public so WorkerFile objects can call it. */ public _argLocal2Remote(arg: any): any { if (arg == null) { return arg; } switch (typeof arg) { case "object": if (arg instanceof Stats) { return statsLocal2Remote(arg); } else if (arg instanceof ApiError) { return apiErrorLocal2Remote(arg); } else if (arg instanceof WorkerFile) { return (<WorkerFile> arg).toRemoteArg(); } else if (arg instanceof file_flag.FileFlag) { return fileFlagLocal2Remote(arg); } else if (arg instanceof Buffer) { return bufferLocal2Remote(arg); } else if (arg instanceof Error) { return errorLocal2Remote(arg); } else { return "Unknown argument"; } case "function": return this._callbackConverter.toRemoteArg(arg); default: return arg; } } /** * Called once both local and remote sides are set up. */ public initialize(cb: () => void): void { if (!this._isInitialized) { var message: IAPIRequest = { browserfsMessage: true, method: 'probe', args: [this._argLocal2Remote(new Buffer(0)), this._callbackConverter.toRemoteArg((probeResponse: IProbeResponse) => { this._isInitialized = true; this._isReadOnly = probeResponse.isReadOnly; this._supportLinks = probeResponse.supportsLinks; this._supportProps = probeResponse.supportsProps; cb(); })] }; this._worker.postMessage(message); } else { cb(); } } public isReadOnly(): boolean { return this._isReadOnly; } public supportsSynch(): boolean { return false; } public supportsLinks(): boolean { return this._supportLinks; } public supportsProps(): boolean { return this._supportProps; } private _rpc(methodName: string, args: IArguments) { var message: IAPIRequest = { browserfsMessage: true, method: methodName, args: null }, fixedArgs = new Array(args.length), i: number; for (i = 0; i < args.length; i++) { fixedArgs[i] = this._argLocal2Remote(args[i]); } message.args = fixedArgs; this._worker.postMessage(message); } public rename(oldPath: string, newPath: string, cb: (err?: ApiError) => void): void { this._rpc('rename', arguments); } public stat(p: string, isLstat: boolean, cb: (err: ApiError, stat?: Stats) => void): void { this._rpc('stat', arguments); } public open(p: string, flag: file_flag.FileFlag, mode: number, cb: (err: ApiError, fd?: file.File) => any): void { this._rpc('open', arguments); } public unlink(p: string, cb: Function): void { this._rpc('unlink', arguments); } public rmdir(p: string, cb: Function): void { this._rpc('rmdir', arguments); } public mkdir(p: string, mode: number, cb: Function): void { this._rpc('mkdir', arguments); } public readdir(p: string, cb: (err: ApiError, files?: string[]) => void): void { this._rpc('readdir', arguments); } public exists(p: string, cb: (exists: boolean) => void): void { this._rpc('exists', arguments); } public realpath(p: string, cache: { [path: string]: string }, cb: (err: ApiError, resolvedPath?: string) => any): void { this._rpc('realpath', arguments); } public truncate(p: string, len: number, cb: Function): void { this._rpc('truncate', arguments); } public readFile(fname: string, encoding: string, flag: file_flag.FileFlag, cb: (err: ApiError, data?: any) => void): void { this._rpc('readFile', arguments); } public writeFile(fname: string, data: any, encoding: string, flag: file_flag.FileFlag, mode: number, cb: (err: ApiError) => void): void { this._rpc('writeFile', arguments); } public appendFile(fname: string, data: any, encoding: string, flag: file_flag.FileFlag, mode: number, cb: (err: ApiError) => void): void { this._rpc('appendFile', arguments); } public chmod(p: string, isLchmod: boolean, mode: number, cb: Function): void { this._rpc('chmod', arguments); } public chown(p: string, isLchown: boolean, uid: number, gid: number, cb: Function): void { this._rpc('chown', arguments); } public utimes(p: string, atime: Date, mtime: Date, cb: Function): void { this._rpc('utimes', arguments); } public link(srcpath: string, dstpath: string, cb: Function): void { this._rpc('link', arguments); } public symlink(srcpath: string, dstpath: string, type: string, cb: Function): void { this._rpc('symlink', arguments); } public readlink(p: string, cb: Function): void { this._rpc('readlink', arguments); } public syncClose(method: string, fd: file.File, cb: (e: ApiError) => void): void { this._worker.postMessage(<IAPIRequest> { browserfsMessage: true, method: method, args: [(<WorkerFile> fd).toRemoteArg(), this._callbackConverter.toRemoteArg(cb)] }); } /** * Attaches a listener to the remote worker for file system requests. */ public static attachRemoteListener(worker: Worker) { var fdConverter = new FileDescriptorArgumentConverter(); function argLocal2Remote(arg: any, requestArgs: any[], cb: (err: ApiError, arg?: any) => void): void { switch (typeof arg) { case 'object': if (arg instanceof Stats) { cb(null, statsLocal2Remote(arg)); } else if (arg instanceof ApiError) { cb(null, apiErrorLocal2Remote(arg)); } else if (arg instanceof file.BaseFile) { // Pass in p and flags from original request. cb(null, fdConverter.toRemoteArg(arg, requestArgs[0], requestArgs[1], cb)); } else if (arg instanceof file_flag.FileFlag) { cb(null, fileFlagLocal2Remote(arg)); } else if (arg instanceof Buffer) { cb(null, bufferLocal2Remote(arg)); } else if (arg instanceof Error) { cb(null, errorLocal2Remote(arg)); } else { cb(null, arg); } break; default: cb(null, arg); break; } } function argRemote2Local(arg: any, fixedRequestArgs: any[]): any { if (arg == null) { return arg; } switch (typeof arg) { case 'object': if (typeof arg['type'] === 'number') { var specialArg = <ISpecialArgument> arg; switch (specialArg.type) { case SpecialArgType.CB: var cbId = (<ICallbackArgument> arg).id; return function() { var i: number, fixedArgs = new Array(arguments.length), message: IAPIResponse, countdown = arguments.length; function abortAndSendError(err: ApiError) { if (countdown > 0) { countdown = -1; message = { browserfsMessage: true, cbId: cbId, args: [apiErrorLocal2Remote(err)] }; worker.postMessage(message); } } for (i = 0; i < arguments.length; i++) { // Capture i and argument. ((i: number, arg: any) => { argLocal2Remote(arg, fixedRequestArgs, (err, fixedArg?) => { fixedArgs[i] = fixedArg; if (err) { abortAndSendError(err); } else if (--countdown === 0) { message = { browserfsMessage: true, cbId: cbId, args: fixedArgs }; worker.postMessage(message); } }); })(i, arguments[i]); } if (arguments.length === 0) { message = { browserfsMessage: true, cbId: cbId, args: fixedArgs }; worker.postMessage(message); } }; case SpecialArgType.API_ERROR: return apiErrorRemote2Local(<IAPIErrorArgument> specialArg); case SpecialArgType.STATS: return statsRemote2Local(<IStatsArgument> specialArg); case SpecialArgType.FILEFLAG: return fileFlagRemote2Local(<IFileFlagArgument> specialArg); case SpecialArgType.BUFFER: return bufferRemote2Local(<IBufferArgument> specialArg); case SpecialArgType.ERROR: return errorRemote2Local(<IErrorArgument> specialArg); default: // No idea what this is. return arg; } } else { return arg; } default: return arg; } } worker.addEventListener('message',(e: MessageEvent) => { var request: Object = e.data; if (isAPIRequest(request)) { var args = request.args, fixedArgs = new Array<any>(args.length), i: number; switch (request.method) { case 'close': case 'sync': (() => { // File descriptor-relative methods. var remoteCb = <ICallbackArgument> args[1]; fdConverter.applyFdAPIRequest(request, (err?: ApiError) => { // Send response. var response: IAPIResponse = { browserfsMessage: true, cbId: remoteCb.id, args: err ? [apiErrorLocal2Remote(err)] : [] }; worker.postMessage(response); }); })(); break; case 'probe': (() => { var rootFs = <file_system.FileSystem> fs.getRootFS(), remoteCb = <ICallbackArgument> args[1], probeResponse: IProbeResponse = { type: SpecialArgType.PROBE, isReadOnly: rootFs.isReadOnly(), supportsLinks: rootFs.supportsLinks(), supportsProps: rootFs.supportsProps() }, response: IAPIResponse = { browserfsMessage: true, cbId: remoteCb.id, args: [probeResponse] }; worker.postMessage(response); })(); break; default: // File system methods. for (i = 0; i < args.length; i++) { fixedArgs[i] = argRemote2Local(args[i], fixedArgs); } var rootFS = fs.getRootFS(); (<Function> rootFS[request.method]).apply(rootFS, fixedArgs); break; } } }); } }