UNPKG

browserfs

Version:

A filesystem in your browser!

1,480 lines (1,378 loc) 49 kB
import {File} from './file'; import {ApiError, ErrorCode} from './api_error'; import file_system = require('./file_system'); import {FileFlag} from './file_flag'; import path = require('path'); import Stats from './node_fs_stats'; // Typing info only. import _fs = require('fs'); import global = require('./global'); declare var __numWaiting: number; declare var setImmediate: (cb: Function) => void; declare var RELEASE: boolean; /** * Wraps a callback with a setImmediate call. * @param [Function] cb The callback to wrap. * @param [Number] numArgs The number of arguments that the callback takes. * @return [Function] The wrapped callback. */ function wrapCb<T extends Function>(cb: T, numArgs: number): T { if (RELEASE) { return cb; } else { if (typeof cb !== 'function') { throw new ApiError(ErrorCode.EINVAL, 'Callback must be a function.'); } // This is used for unit testing. // We could use `arguments`, but Function.call/apply is expensive. And we only // need to handle 1-3 arguments if (typeof __numWaiting === 'undefined') { global.__numWaiting = 0; } __numWaiting++; switch (numArgs) { case 1: return <any> function(arg1: any) { setImmediate(function() { __numWaiting--; return cb(arg1); }); }; case 2: return <any> function(arg1: any, arg2: any) { setImmediate(function() { __numWaiting--; return cb(arg1, arg2); }); }; case 3: return <any> function(arg1: any, arg2: any, arg3: any) { setImmediate(function() { __numWaiting--; return cb(arg1, arg2, arg3); }); }; default: throw new Error('Invalid invocation of wrapCb.'); } } } function normalizeMode(mode: number|string, def: number): number { switch(typeof mode) { case 'number': // (path, flag, mode, cb?) return <number> mode; case 'string': // (path, flag, modeString, cb?) var trueMode = parseInt(<string> mode, 8); if (trueMode !== NaN) { return trueMode; } // FALL THROUGH if mode is an invalid string! default: return def; } } function normalizeTime(time: number | Date): Date { if (time instanceof Date) { return time; } else if (typeof time === 'number') { return new Date(time * 1000); } else { throw new ApiError(ErrorCode.EINVAL, `Invalid time.`); } } function normalizePath(p: string): string { // Node doesn't allow null characters in paths. if (p.indexOf('\u0000') >= 0) { throw new ApiError(ErrorCode.EINVAL, 'Path must be a string without null bytes.'); } else if (p === '') { throw new ApiError(ErrorCode.EINVAL, 'Path must not be empty.'); } return path.resolve(p); } function normalizeOptions(options: any, defEnc: string, defFlag: string, defMode: number): {encoding: string; flag: string; mode: number} { switch (typeof options) { case 'object': return { encoding: typeof options['encoding'] !== 'undefined' ? options['encoding'] : defEnc, flag: typeof options['flag'] !== 'undefined' ? options['flag'] : defFlag, mode: normalizeMode(options['mode'], defMode) }; case 'string': return { encoding: options, flag: defFlag, mode: defMode }; default: return { encoding: defEnc, flag: defFlag, mode: defMode }; } } // The default callback is a NOP. function nopCb() {}; /** * The node frontend to all filesystems. * This layer handles: * * * Sanity checking inputs. * * Normalizing paths. * * Resetting stack depth for asynchronous operations which may not go through * the browser by wrapping all input callbacks using `setImmediate`. * * Performing the requested operation through the filesystem or the file * descriptor, as appropriate. * * Handling optional arguments and setting default arguments. * @see http://nodejs.org/api/fs.html * @class */ export default class FS { // Exported fs.Stats. public static Stats = Stats; private root: file_system.FileSystem = null; private fdMap: {[fd: number]: File} = {}; private nextFd = 100; private getFdForFile(file: File): number { let fd = this.nextFd++; this.fdMap[fd] = file; return fd; } private fd2file(fd: number): File { let rv = this.fdMap[fd]; if (rv) { return rv; } else { throw new ApiError(ErrorCode.EBADF, 'Invalid file descriptor.'); } } private closeFd(fd: number): void { delete this.fdMap[fd]; } public initialize(rootFS: file_system.FileSystem): file_system.FileSystem { if (!(<any> rootFS).constructor.isAvailable()) { throw new ApiError(ErrorCode.EINVAL, 'Tried to instantiate BrowserFS with an unavailable file system.'); } return this.root = rootFS; } /** * converts Date or number to a fractional UNIX timestamp * Grabbed from NodeJS sources (lib/fs.js) */ public _toUnixTimestamp(time: Date | number): number { if (typeof time === 'number') { return time; } else if (time instanceof Date) { return time.getTime() / 1000; } throw new Error("Cannot parse time: " + time); } /** * **NONSTANDARD**: Grab the FileSystem instance that backs this API. * @return [BrowserFS.FileSystem | null] Returns null if the file system has * not been initialized. */ public getRootFS(): file_system.FileSystem { if (this.root) { return this.root; } else { return null; } } // FILE OR DIRECTORY METHODS /** * Asynchronous rename. No arguments other than a possible exception are given * to the completion callback. * @param [String] oldPath * @param [String] newPath * @param [Function(BrowserFS.ApiError)] callback */ public rename(oldPath: string, newPath: string, cb: (err?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { this.root.rename(normalizePath(oldPath), normalizePath(newPath), newCb); } catch (e) { newCb(e); } } /** * Synchronous rename. * @param [String] oldPath * @param [String] newPath */ public renameSync(oldPath: string, newPath: string): void { this.root.renameSync(normalizePath(oldPath), normalizePath(newPath)); } /** * Test whether or not the given path exists by checking with the file system. * Then call the callback argument with either true or false. * @example Sample invocation * fs.exists('/etc/passwd', function (exists) { * util.debug(exists ? "it's there" : "no passwd!"); * }); * @param [String] path * @param [Function(Boolean)] callback */ public exists(path: string, cb: (exists: boolean) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { return this.root.exists(normalizePath(path), newCb); } catch (e) { // Doesn't return an error. If something bad happens, we assume it just // doesn't exist. return newCb(false); } } /** * Test whether or not the given path exists by checking with the file system. * @param [String] path * @return [boolean] */ public existsSync(path: string): boolean { try { return this.root.existsSync(normalizePath(path)); } catch (e) { // Doesn't return an error. If something bad happens, we assume it just // doesn't exist. return false; } } /** * Asynchronous `stat`. * @param [String] path * @param [Function(BrowserFS.ApiError, BrowserFS.node.fs.Stats)] callback */ public stat(path: string, cb: (err: ApiError, stats?: Stats) => any = nopCb): void { var newCb = wrapCb(cb, 2); try { return this.root.stat(normalizePath(path), false, newCb); } catch (e) { return newCb(e, null); } } /** * Synchronous `stat`. * @param [String] path * @return [BrowserFS.node.fs.Stats] */ public statSync(path: string): Stats { return this.root.statSync(normalizePath(path), false); } /** * Asynchronous `lstat`. * `lstat()` is identical to `stat()`, except that if path is a symbolic link, * then the link itself is stat-ed, not the file that it refers to. * @param [String] path * @param [Function(BrowserFS.ApiError, BrowserFS.node.fs.Stats)] callback */ public lstat(path: string, cb: (err: ApiError, stats?: Stats) => any = nopCb): void { var newCb = wrapCb(cb, 2); try { return this.root.stat(normalizePath(path), true, newCb); } catch (e) { return newCb(e, null); } } /** * Synchronous `lstat`. * `lstat()` is identical to `stat()`, except that if path is a symbolic link, * then the link itself is stat-ed, not the file that it refers to. * @param [String] path * @return [BrowserFS.node.fs.Stats] */ public lstatSync(path: string): Stats { return this.root.statSync(normalizePath(path), true); } // FILE-ONLY METHODS /** * Asynchronous `truncate`. * @param [String] path * @param [Number] len * @param [Function(BrowserFS.ApiError)] callback */ public truncate(path: string, cb?: (err?: ApiError) => void): void; public truncate(path: string, len: number, cb?: (err?: ApiError) => void): void; public truncate(path: string, arg2: any = 0, cb: (err?: ApiError) => void = nopCb): void { var len = 0; if (typeof arg2 === 'function') { cb = arg2; } else if (typeof arg2 === 'number') { len = arg2; } var newCb = wrapCb(cb, 1); try { if (len < 0) { throw new ApiError(ErrorCode.EINVAL); } return this.root.truncate(normalizePath(path), len, newCb); } catch (e) { return newCb(e); } } /** * Synchronous `truncate`. * @param [String] path * @param [Number] len */ public truncateSync(path: string, len: number = 0): void { if (len < 0) { throw new ApiError(ErrorCode.EINVAL); } return this.root.truncateSync(normalizePath(path), len); } /** * Asynchronous `unlink`. * @param [String] path * @param [Function(BrowserFS.ApiError)] callback */ public unlink(path: string, cb: (err?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { return this.root.unlink(normalizePath(path), newCb); } catch (e) { return newCb(e); } } /** * Synchronous `unlink`. * @param [String] path */ public unlinkSync(path: string): void { return this.root.unlinkSync(normalizePath(path)); } /** * Asynchronous file open. * Exclusive mode ensures that path is newly created. * * `flags` can be: * * * `'r'` - Open file for reading. An exception occurs if the file does not exist. * * `'r+'` - Open file for reading and writing. An exception occurs if the file does not exist. * * `'rs'` - Open file for reading in synchronous mode. Instructs the filesystem to not cache writes. * * `'rs+'` - Open file for reading and writing, and opens the file in synchronous mode. * * `'w'` - Open file for writing. The file is created (if it does not exist) or truncated (if it exists). * * `'wx'` - Like 'w' but opens the file in exclusive mode. * * `'w+'` - Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). * * `'wx+'` - Like 'w+' but opens the file in exclusive mode. * * `'a'` - Open file for appending. The file is created if it does not exist. * * `'ax'` - Like 'a' but opens the file in exclusive mode. * * `'a+'` - Open file for reading and appending. The file is created if it does not exist. * * `'ax+'` - Like 'a+' but opens the file in exclusive mode. * * @see http://www.manpagez.com/man/2/open/ * @param [String] path * @param [String] flags * @param [Number?] mode defaults to `0644` * @param [Function(BrowserFS.ApiError, BrowserFS.File)] callback */ public open(path: string, flag: string, cb?: (err: ApiError, fd?: number) => any): void; public open(path: string, flag: string, mode: number|string, cb?: (err: ApiError, fd?: number) => any): void; public open(path: string, flag: string, arg2?: any, cb: (err: ApiError, fd?: number) => any = nopCb): void { var mode = normalizeMode(arg2, 0x1a4); cb = typeof arg2 === 'function' ? arg2 : cb; var newCb = wrapCb(cb, 2); try { this.root.open(normalizePath(path), FileFlag.getFileFlag(flag), mode, (e: ApiError, file?: File) => { if (file) { newCb(e, this.getFdForFile(file)); } else { newCb(e); } }); } catch (e) { newCb(e, null); } } /** * Synchronous file open. * @see http://www.manpagez.com/man/2/open/ * @param [String] path * @param [String] flags * @param [Number?] mode defaults to `0644` * @return [BrowserFS.File] */ public openSync(path: string, flag: string, mode: number|string = 0x1a4): number { return this.getFdForFile( this.root.openSync(normalizePath(path), FileFlag.getFileFlag(flag), normalizeMode(mode, 0x1a4))); } /** * Asynchronously reads the entire contents of a file. * @example Usage example * fs.readFile('/etc/passwd', function (err, data) { * if (err) throw err; * console.log(data); * }); * @param [String] filename * @param [Object?] options * @option options [String] encoding The string encoding for the file contents. Defaults to `null`. * @option options [String] flag Defaults to `'r'`. * @param [Function(BrowserFS.ApiError, String | BrowserFS.node.Buffer)] callback If no encoding is specified, then the raw buffer is returned. */ public readFile(filename: string, cb: (err: ApiError, data?: Buffer) => void ): void; public readFile(filename: string, options: { flag?: string; }, callback: (err: ApiError, data: Buffer) => void): void; public readFile(filename: string, options: { encoding: string; flag?: string; }, callback: (err: ApiError, data: string) => void): void; public readFile(filename: string, encoding: string, cb?: (err: ApiError, data?: string) => void ): void; public readFile(filename: string, arg2: any = {}, cb: (err: ApiError, data?: any) => void = nopCb ) { var options = normalizeOptions(arg2, null, 'r', null); cb = typeof arg2 === 'function' ? arg2 : cb; var newCb = wrapCb(cb, 2); try { var flag = FileFlag.getFileFlag(options['flag']); if (!flag.isReadable()) { return newCb(new ApiError(ErrorCode.EINVAL, 'Flag passed to readFile must allow for reading.')); } return this.root.readFile(normalizePath(filename), options.encoding, flag, newCb); } catch (e) { return newCb(e, null); } } /** * Synchronously reads the entire contents of a file. * @param [String] filename * @param [Object?] options * @option options [String] encoding The string encoding for the file contents. Defaults to `null`. * @option options [String] flag Defaults to `'r'`. * @return [String | BrowserFS.node.Buffer] */ public readFileSync(filename: string, options?: { flag?: string; }): Buffer; public readFileSync(filename: string, options: { encoding: string; flag?: string; }): string; public readFileSync(filename: string, encoding: string): string; public readFileSync(filename: string, arg2: any = {}): any { var options = normalizeOptions(arg2, null, 'r', null); var flag = FileFlag.getFileFlag(options.flag); if (!flag.isReadable()) { throw new ApiError(ErrorCode.EINVAL, 'Flag passed to readFile must allow for reading.'); } return this.root.readFileSync(normalizePath(filename), options.encoding, flag); } /** * Asynchronously writes data to a file, replacing the file if it already * exists. * * The encoding option is ignored if data is a buffer. * * @example Usage example * fs.writeFile('message.txt', 'Hello Node', function (err) { * if (err) throw err; * console.log('It\'s saved!'); * }); * @param [String] filename * @param [String | BrowserFS.node.Buffer] data * @param [Object?] options * @option options [String] encoding Defaults to `'utf8'`. * @option options [Number] mode Defaults to `0644`. * @option options [String] flag Defaults to `'w'`. * @param [Function(BrowserFS.ApiError)] callback */ public writeFile(filename: string, data: any, cb?: (err?: ApiError) => void): void; public writeFile(filename: string, data: any, encoding?: string, cb?: (err?: ApiError) => void): void; public writeFile(filename: string, data: any, options?: { encoding?: string; mode?: string | number; flag?: string; }, cb?: (err?: ApiError) => void): void; public writeFile(filename: string, data: any, arg3: any = {}, cb: (err?: ApiError) => void = nopCb): void { var options = normalizeOptions(arg3, 'utf8', 'w', 0x1a4); cb = typeof arg3 === 'function' ? arg3 : cb; var newCb = wrapCb(cb, 1); try { var flag = FileFlag.getFileFlag(options.flag); if (!flag.isWriteable()) { return newCb(new ApiError(ErrorCode.EINVAL, 'Flag passed to writeFile must allow for writing.')); } return this.root.writeFile(normalizePath(filename), data, options.encoding, flag, options.mode, newCb); } catch (e) { return newCb(e); } } /** * Synchronously writes data to a file, replacing the file if it already * exists. * * The encoding option is ignored if data is a buffer. * @param [String] filename * @param [String | BrowserFS.node.Buffer] data * @param [Object?] options * @option options [String] encoding Defaults to `'utf8'`. * @option options [Number] mode Defaults to `0644`. * @option options [String] flag Defaults to `'w'`. */ public writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number | string; flag?: string; }): void; public writeFileSync(filename: string, data: any, encoding?: string): void; public writeFileSync(filename: string, data: any, arg3?: any): void { var options = normalizeOptions(arg3, 'utf8', 'w', 0x1a4); var flag = FileFlag.getFileFlag(options.flag); if (!flag.isWriteable()) { throw new ApiError(ErrorCode.EINVAL, 'Flag passed to writeFile must allow for writing.'); } return this.root.writeFileSync(normalizePath(filename), data, options.encoding, flag, options.mode); } /** * Asynchronously append data to a file, creating the file if it not yet * exists. * * @example Usage example * fs.appendFile('message.txt', 'data to append', function (err) { * if (err) throw err; * console.log('The "data to append" was appended to file!'); * }); * @param [String] filename * @param [String | BrowserFS.node.Buffer] data * @param [Object?] options * @option options [String] encoding Defaults to `'utf8'`. * @option options [Number] mode Defaults to `0644`. * @option options [String] flag Defaults to `'a'`. * @param [Function(BrowserFS.ApiError)] callback */ public appendFile(filename: string, data: any, cb?: (err: ApiError) => void): void; public appendFile(filename: string, data: any, options?: { encoding?: string; mode?: number|string; flag?: string; }, cb?: (err: ApiError) => void): void; public appendFile(filename: string, data: any, encoding?: string, cb?: (err: ApiError) => void): void; public appendFile(filename: string, data: any, arg3?: any, cb: (err: ApiError) => void = nopCb): void { var options = normalizeOptions(arg3, 'utf8', 'a', 0x1a4); cb = typeof arg3 === 'function' ? arg3 : cb; var newCb = wrapCb(cb, 1); try { var flag = FileFlag.getFileFlag(options.flag); if (!flag.isAppendable()) { return newCb(new ApiError(ErrorCode.EINVAL, 'Flag passed to appendFile must allow for appending.')); } this.root.appendFile(normalizePath(filename), data, options.encoding, flag, options.mode, newCb); } catch (e) { newCb(e); } } /** * Asynchronously append data to a file, creating the file if it not yet * exists. * * @example Usage example * fs.appendFile('message.txt', 'data to append', function (err) { * if (err) throw err; * console.log('The "data to append" was appended to file!'); * }); * @param [String] filename * @param [String | BrowserFS.node.Buffer] data * @param [Object?] options * @option options [String] encoding Defaults to `'utf8'`. * @option options [Number] mode Defaults to `0644`. * @option options [String] flag Defaults to `'a'`. */ public appendFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number | string; flag?: string; }): void; public appendFileSync(filename: string, data: any, encoding?: string): void; public appendFileSync(filename: string, data: any, arg3?: any): void { var options = normalizeOptions(arg3, 'utf8', 'a', 0x1a4); var flag = FileFlag.getFileFlag(options.flag); if (!flag.isAppendable()) { throw new ApiError(ErrorCode.EINVAL, 'Flag passed to appendFile must allow for appending.'); } return this.root.appendFileSync(normalizePath(filename), data, options.encoding, flag, options.mode); } // FILE DESCRIPTOR METHODS /** * Asynchronous `fstat`. * `fstat()` is identical to `stat()`, except that the file to be stat-ed is * specified by the file descriptor `fd`. * @param [BrowserFS.File] fd * @param [Function(BrowserFS.ApiError, BrowserFS.node.fs.Stats)] callback */ public fstat(fd: number, cb: (err: ApiError, stats?: Stats) => any = nopCb): void { var newCb = wrapCb(cb, 2); try { let file = this.fd2file(fd); file.stat(newCb); } catch (e) { newCb(e); } } /** * Synchronous `fstat`. * `fstat()` is identical to `stat()`, except that the file to be stat-ed is * specified by the file descriptor `fd`. * @param [BrowserFS.File] fd * @return [BrowserFS.node.fs.Stats] */ public fstatSync(fd: number): Stats { return this.fd2file(fd).statSync(); } /** * Asynchronous close. * @param [BrowserFS.File] fd * @param [Function(BrowserFS.ApiError)] callback */ public close(fd: number, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { this.fd2file(fd).close((e: ApiError) => { if (!e) { this.closeFd(fd); } newCb(e); }); } catch (e) { newCb(e); } } /** * Synchronous close. * @param [BrowserFS.File] fd */ public closeSync(fd: number): void { this.fd2file(fd).closeSync(); this.closeFd(fd); } /** * Asynchronous ftruncate. * @param [BrowserFS.File] fd * @param [Number] len * @param [Function(BrowserFS.ApiError)] callback */ public ftruncate(fd: number, cb?: (err?: ApiError) => void): void; public ftruncate(fd: number, len?: number, cb?: (err?: ApiError) => void): void; public ftruncate(fd: number, arg2?: any, cb: (err?: ApiError) => void = nopCb): void { var length = typeof arg2 === 'number' ? arg2 : 0; cb = typeof arg2 === 'function' ? arg2 : cb; var newCb = wrapCb(cb, 1); try { let file = this.fd2file(fd); if (length < 0) { throw new ApiError(ErrorCode.EINVAL); } file.truncate(length, newCb); } catch (e) { newCb(e); } } /** * Synchronous ftruncate. * @param [BrowserFS.File] fd * @param [Number] len */ public ftruncateSync(fd: number, len: number = 0): void { let file = this.fd2file(fd); if (len < 0) { throw new ApiError(ErrorCode.EINVAL); } file.truncateSync(len); } /** * Asynchronous fsync. * @param [BrowserFS.File] fd * @param [Function(BrowserFS.ApiError)] callback */ public fsync(fd: number, cb: (err?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { this.fd2file(fd).sync(newCb); } catch (e) { newCb(e); } } /** * Synchronous fsync. * @param [BrowserFS.File] fd */ public fsyncSync(fd: number): void { this.fd2file(fd).syncSync(); } /** * Asynchronous fdatasync. * @param [BrowserFS.File] fd * @param [Function(BrowserFS.ApiError)] callback */ public fdatasync(fd: number, cb: (err?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { this.fd2file(fd).datasync(newCb); } catch (e) { newCb(e); } } /** * Synchronous fdatasync. * @param [BrowserFS.File] fd */ public fdatasyncSync(fd: number): void { this.fd2file(fd).datasyncSync(); } /** * Write buffer to the file specified by `fd`. * Note that it is unsafe to use fs.write multiple times on the same file * without waiting for the callback. * @param [BrowserFS.File] fd * @param [BrowserFS.node.Buffer] buffer Buffer containing the data to write to * the file. * @param [Number] offset Offset in the buffer to start reading data from. * @param [Number] length The amount of bytes to write to the file. * @param [Number] position Offset from the beginning of the file where this * data should be written. If position is null, the data will be written at * the current position. * @param [Function(BrowserFS.ApiError, Number, BrowserFS.node.Buffer)] * callback The number specifies the number of bytes written into the file. */ public write(fd: number, buffer: Buffer, offset: number, length: number, cb?: (err: ApiError, written: number, buffer: Buffer) => void): void; public write(fd: number, buffer: Buffer, offset: number, length: number, position: number, cb?: (err: ApiError, written: number, buffer: Buffer) => void): void; public write(fd: number, data: any, cb?: (err: ApiError, written: number, str: string) => any): void; public write(fd: number, data: any, position: number, cb?: (err: ApiError, written: number, str: string) => any): void; public write(fd: number, data: any, position: number, encoding: string, cb?: (err: ApiError, written: number, str: string) => void): void; public write(fd: number, arg2: any, arg3?: any, arg4?: any, arg5?: any, cb: (err: ApiError, written?: number, buffer?: Buffer) => void = nopCb): void { var buffer: Buffer, offset: number, length: number, position: number = null; if (typeof arg2 === 'string') { // Signature 1: (fd, string, [position?, [encoding?]], cb?) var encoding = 'utf8'; switch (typeof arg3) { case 'function': // (fd, string, cb) cb = arg3; break; case 'number': // (fd, string, position, encoding?, cb?) position = arg3; encoding = typeof arg4 === 'string' ? arg4 : 'utf8'; cb = typeof arg5 === 'function' ? arg5 : cb; break; default: // ...try to find the callback and get out of here! cb = typeof arg4 === 'function' ? arg4 : typeof arg5 === 'function' ? arg5 : cb; return cb(new ApiError(ErrorCode.EINVAL, 'Invalid arguments.')); } buffer = new Buffer(arg2, encoding); offset = 0; length = buffer.length; } else { // Signature 2: (fd, buffer, offset, length, position?, cb?) buffer = arg2; offset = arg3; length = arg4; position = typeof arg5 === 'number' ? arg5 : null; cb = typeof arg5 === 'function' ? arg5 : cb; } var newCb = wrapCb(cb, 3); try { let file = this.fd2file(fd); if (position == null) { position = file.getPos(); } file.write(buffer, offset, length, position, newCb); } catch (e) { newCb(e); } } /** * Write buffer to the file specified by `fd`. * Note that it is unsafe to use fs.write multiple times on the same file * without waiting for it to return. * @param [BrowserFS.File] fd * @param [BrowserFS.node.Buffer] buffer Buffer containing the data to write to * the file. * @param [Number] offset Offset in the buffer to start reading data from. * @param [Number] length The amount of bytes to write to the file. * @param [Number] position Offset from the beginning of the file where this * data should be written. If position is null, the data will be written at * the current position. * @return [Number] */ public writeSync(fd: number, buffer: Buffer, offset: number, length: number, position?: number): number; public writeSync(fd: number, data: string, position?: number, encoding?: string): number; public writeSync(fd: number, arg2: any, arg3?: any, arg4?: any, arg5?: any): number { var buffer: Buffer, offset: number = 0, length: number, position: number; if (typeof arg2 === 'string') { // Signature 1: (fd, string, [position?, [encoding?]]) position = typeof arg3 === 'number' ? arg3 : null; var encoding = typeof arg4 === 'string' ? arg4 : 'utf8'; offset = 0; buffer = new Buffer(arg2, encoding); length = buffer.length; } else { // Signature 2: (fd, buffer, offset, length, position?) buffer = arg2; offset = arg3; length = arg4; position = typeof arg5 === 'number' ? arg5 : null; } let file = this.fd2file(fd); if (position == null) { position = file.getPos(); } return file.writeSync(buffer, offset, length, position); } /** * Read data from the file specified by `fd`. * @param [BrowserFS.File] fd * @param [BrowserFS.node.Buffer] buffer The buffer that the data will be * written to. * @param [Number] offset The offset within the buffer where writing will * start. * @param [Number] length An integer specifying the number of bytes to read. * @param [Number] position An integer specifying where to begin reading from * in the file. If position is null, data will be read from the current file * position. * @param [Function(BrowserFS.ApiError, Number, BrowserFS.node.Buffer)] * callback The number is the number of bytes read */ public read(fd: number, length: number, position: number, encoding: string, cb?: (err: ApiError, data?: string, bytesRead?: number) => void): void; public read(fd: number, buffer: Buffer, offset: number, length: number, position: number, cb?: (err: ApiError, bytesRead?: number, buffer?: Buffer) => void): void; public read(fd: number, arg2: any, arg3: any, arg4: any, arg5?: any, cb: (err: ApiError, arg2?: any, arg3?: any) => void = nopCb): void { var position: number, offset: number, length: number, buffer: Buffer, newCb: (err: ApiError, bytesRead?: number, buffer?: Buffer) => void; if (typeof arg2 === 'number') { // legacy interface // (fd, length, position, encoding, callback) length = arg2; position = arg3; var encoding = arg4; cb = typeof arg5 === 'function' ? arg5 : cb; offset = 0; buffer = new Buffer(length); // XXX: Inefficient. // Wrap the cb so we shelter upper layers of the API from these // shenanigans. newCb = wrapCb((function(err: any, bytesRead: number, buf: Buffer) { if (err) { return cb(err); } cb(err, buf.toString(encoding), bytesRead); }), 3); } else { buffer = arg2; offset = arg3; length = arg4; position = arg5; newCb = wrapCb(cb, 3); } try { let file = this.fd2file(fd); if (position == null) { position = file.getPos(); } file.read(buffer, offset, length, position, newCb); } catch (e) { newCb(e); } } /** * Read data from the file specified by `fd`. * @param [BrowserFS.File] fd * @param [BrowserFS.node.Buffer] buffer The buffer that the data will be * written to. * @param [Number] offset The offset within the buffer where writing will * start. * @param [Number] length An integer specifying the number of bytes to read. * @param [Number] position An integer specifying where to begin reading from * in the file. If position is null, data will be read from the current file * position. * @return [Number] */ public readSync(fd: number, length: number, position: number, encoding: string): string; public readSync(fd: number, buffer: Buffer, offset: number, length: number, position: number): number; public readSync(fd: number, arg2: any, arg3: any, arg4: any, arg5?: any): any { var shenanigans = false; var buffer: Buffer, offset: number, length: number, position: number; if (typeof arg2 === 'number') { length = arg2; position = arg3; var encoding = arg4; offset = 0; buffer = new Buffer(length); shenanigans = true; } else { buffer = arg2; offset = arg3; length = arg4; position = arg5; } let file = this.fd2file(fd); if (position == null) { position = file.getPos(); } var rv = file.readSync(buffer, offset, length, position); if (!shenanigans) { return rv; } else { return [buffer.toString(encoding), rv]; } } /** * Asynchronous `fchown`. * @param [BrowserFS.File] fd * @param [Number] uid * @param [Number] gid * @param [Function(BrowserFS.ApiError)] callback */ public fchown(fd: number, uid: number, gid: number, callback: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(callback, 1); try { this.fd2file(fd).chown(uid, gid, newCb); } catch (e) { newCb(e); } } /** * Synchronous `fchown`. * @param [BrowserFS.File] fd * @param [Number] uid * @param [Number] gid */ public fchownSync(fd: number, uid: number, gid: number): void { this.fd2file(fd).chownSync(uid, gid); } /** * Asynchronous `fchmod`. * @param [BrowserFS.File] fd * @param [Number] mode * @param [Function(BrowserFS.ApiError)] callback */ public fchmod(fd: number, mode: string | number, cb?: (e?: ApiError) => void): void { var newCb = wrapCb(cb, 1); try { let numMode = typeof mode === 'string' ? parseInt(mode, 8) : mode; this.fd2file(fd).chmod(numMode, newCb); } catch (e) { newCb(e); } } /** * Synchronous `fchmod`. * @param [BrowserFS.File] fd * @param [Number] mode */ public fchmodSync(fd: number, mode: number | string): void { let numMode = typeof mode === 'string' ? parseInt(mode, 8) : mode; this.fd2file(fd).chmodSync(numMode); } /** * Change the file timestamps of a file referenced by the supplied file * descriptor. * @param [BrowserFS.File] fd * @param [Date] atime * @param [Date] mtime * @param [Function(BrowserFS.ApiError)] callback */ public futimes(fd: number, atime: number, mtime: number, cb: (e?: ApiError) => void): void; public futimes(fd: number, atime: Date, mtime: Date, cb: (e?: ApiError) => void): void; public futimes(fd: number, atime: any, mtime: any, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { let file = this.fd2file(fd); if (typeof atime === 'number') { atime = new Date(atime * 1000); } if (typeof mtime === 'number') { mtime = new Date(mtime * 1000); } file.utimes(atime, mtime, newCb); } catch (e) { newCb(e); } } /** * Change the file timestamps of a file referenced by the supplied file * descriptor. * @param [BrowserFS.File] fd * @param [Date] atime * @param [Date] mtime */ public futimesSync(fd: number, atime: number | Date, mtime: number | Date): void { this.fd2file(fd).utimesSync(normalizeTime(atime), normalizeTime(mtime)); } // DIRECTORY-ONLY METHODS /** * Asynchronous `rmdir`. * @param [String] path * @param [Function(BrowserFS.ApiError)] callback */ public rmdir(path: string, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { path = normalizePath(path); this.root.rmdir(path, newCb); } catch (e) { newCb(e); } } /** * Synchronous `rmdir`. * @param [String] path */ public rmdirSync(path: string): void { path = normalizePath(path); return this.root.rmdirSync(path); } /** * Asynchronous `mkdir`. * @param [String] path * @param [Number?] mode defaults to `0777` * @param [Function(BrowserFS.ApiError)] callback */ public mkdir(path: string, mode?: any, cb: (e?: ApiError) => void = nopCb): void { if (typeof mode === 'function') { cb = mode; mode = 0x1ff; } var newCb = wrapCb(cb, 1); try { path = normalizePath(path); this.root.mkdir(path, mode, newCb); } catch (e) { newCb(e); } } /** * Synchronous `mkdir`. * @param [String] path * @param [Number?] mode defaults to `0777` */ public mkdirSync(path: string, mode?: number | string): void { this.root.mkdirSync(normalizePath(path), normalizeMode(mode, 0x1ff)); } /** * Asynchronous `readdir`. Reads the contents of a directory. * The callback gets two arguments `(err, files)` where `files` is an array of * the names of the files in the directory excluding `'.'` and `'..'`. * @param [String] path * @param [Function(BrowserFS.ApiError, String[])] callback */ public readdir(path: string, cb: (err: ApiError, files?: string[]) => void = nopCb): void { var newCb = <(err: ApiError, files?: string[]) => void> wrapCb(cb, 2); try { path = normalizePath(path); this.root.readdir(path, newCb); } catch (e) { newCb(e); } } /** * Synchronous `readdir`. Reads the contents of a directory. * @param [String] path * @return [String[]] */ public readdirSync(path: string): string[] { path = normalizePath(path); return this.root.readdirSync(path); } // SYMLINK METHODS /** * Asynchronous `link`. * @param [String] srcpath * @param [String] dstpath * @param [Function(BrowserFS.ApiError)] callback */ public link(srcpath: string, dstpath: string, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { srcpath = normalizePath(srcpath); dstpath = normalizePath(dstpath); this.root.link(srcpath, dstpath, newCb); } catch (e) { newCb(e); } } /** * Synchronous `link`. * @param [String] srcpath * @param [String] dstpath */ public linkSync(srcpath: string, dstpath: string): void { srcpath = normalizePath(srcpath); dstpath = normalizePath(dstpath); return this.root.linkSync(srcpath, dstpath); } /** * Asynchronous `symlink`. * @param [String] srcpath * @param [String] dstpath * @param [String?] type can be either `'dir'` or `'file'` (default is `'file'`) * @param [Function(BrowserFS.ApiError)] callback */ public symlink(srcpath: string, dstpath: string, cb?: (e?: ApiError) => void): void; public symlink(srcpath: string, dstpath: string, type?: string, cb?: (e?: ApiError) => void): void; public symlink(srcpath: string, dstpath: string, arg3?: any, cb: (e?: ApiError) => void = nopCb): void { var type = typeof arg3 === 'string' ? arg3 : 'file'; cb = typeof arg3 === 'function' ? arg3 : cb; var newCb = wrapCb(cb, 1); try { if (type !== 'file' && type !== 'dir') { return newCb(new ApiError(ErrorCode.EINVAL, "Invalid type: " + type)); } srcpath = normalizePath(srcpath); dstpath = normalizePath(dstpath); this.root.symlink(srcpath, dstpath, type, newCb); } catch (e) { newCb(e); } } /** * Synchronous `symlink`. * @param [String] srcpath * @param [String] dstpath * @param [String?] type can be either `'dir'` or `'file'` (default is `'file'`) */ public symlinkSync(srcpath: string, dstpath: string, type?: string): void { if (type == null) { type = 'file'; } else if (type !== 'file' && type !== 'dir') { throw new ApiError(ErrorCode.EINVAL, "Invalid type: " + type); } srcpath = normalizePath(srcpath); dstpath = normalizePath(dstpath); return this.root.symlinkSync(srcpath, dstpath, type); } /** * Asynchronous readlink. * @param [String] path * @param [Function(BrowserFS.ApiError, String)] callback */ public readlink(path: string, cb: (err: ApiError, linkString?: string) => any = nopCb): void { var newCb = wrapCb(cb, 2); try { path = normalizePath(path); this.root.readlink(path, newCb); } catch (e) { newCb(e); } } /** * Synchronous readlink. * @param [String] path * @return [String] */ public readlinkSync(path: string): string { path = normalizePath(path); return this.root.readlinkSync(path); } // PROPERTY OPERATIONS /** * Asynchronous `chown`. * @param [String] path * @param [Number] uid * @param [Number] gid * @param [Function(BrowserFS.ApiError)] callback */ public chown(path: string, uid: number, gid: number, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { path = normalizePath(path); this.root.chown(path, false, uid, gid, newCb); } catch (e) { newCb(e); } } /** * Synchronous `chown`. * @param [String] path * @param [Number] uid * @param [Number] gid */ public chownSync(path: string, uid: number, gid: number): void { path = normalizePath(path); this.root.chownSync(path, false, uid, gid); } /** * Asynchronous `lchown`. * @param [String] path * @param [Number] uid * @param [Number] gid * @param [Function(BrowserFS.ApiError)] callback */ public lchown(path: string, uid: number, gid: number, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { path = normalizePath(path); this.root.chown(path, true, uid, gid, newCb); } catch (e) { newCb(e); } } /** * Synchronous `lchown`. * @param [String] path * @param [Number] uid * @param [Number] gid */ public lchownSync(path: string, uid: number, gid: number): void { path = normalizePath(path); this.root.chownSync(path, true, uid, gid); } /** * Asynchronous `chmod`. * @param [String] path * @param [Number] mode * @param [Function(BrowserFS.ApiError)] callback */ public chmod(path: string, mode: number | string, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { let numMode = normalizeMode(mode, -1); if (numMode < 0) { throw new ApiError(ErrorCode.EINVAL, `Invalid mode.`); } this.root.chmod(normalizePath(path), false, numMode, newCb); } catch (e) { newCb(e); } } /** * Synchronous `chmod`. * @param [String] path * @param [Number] mode */ public chmodSync(path: string, mode: string|number): void { let numMode = normalizeMode(mode, -1); if (numMode < 0) { throw new ApiError(ErrorCode.EINVAL, `Invalid mode.`); } path = normalizePath(path); this.root.chmodSync(path, false, numMode); } /** * Asynchronous `lchmod`. * @param [String] path * @param [Number] mode * @param [Function(BrowserFS.ApiError)] callback */ public lchmod(path: string, mode: number|string, cb: Function = nopCb): void { var newCb = wrapCb(cb, 1); try { let numMode = normalizeMode(mode, -1); if (numMode < 0) { throw new ApiError(ErrorCode.EINVAL, `Invalid mode.`); } this.root.chmod(normalizePath(path), true, numMode, newCb); } catch (e) { newCb(e); } } /** * Synchronous `lchmod`. * @param [String] path * @param [Number] mode */ public lchmodSync(path: string, mode: number|string): void { let numMode = normalizeMode(mode, -1); if (numMode < 1) { throw new ApiError(ErrorCode.EINVAL, `Invalid mode.`); } this.root.chmodSync(normalizePath(path), true, numMode); } /** * Change file timestamps of the file referenced by the supplied path. * @param [String] path * @param [Date] atime * @param [Date] mtime * @param [Function(BrowserFS.ApiError)] callback */ public utimes(path: string, atime: number|Date, mtime: number|Date, cb: (e?: ApiError) => void = nopCb): void { var newCb = wrapCb(cb, 1); try { this.root.utimes(normalizePath(path), normalizeTime(atime), normalizeTime(mtime), newCb); } catch (e) { newCb(e); } } /** * Change file timestamps of the file referenced by the supplied path. * @param [String] path * @param [Date] atime * @param [Date] mtime */ public utimesSync(path: string, atime: number|Date, mtime: number|Date): void { this.root.utimesSync(normalizePath(path), normalizeTime(atime), normalizeTime(mtime)); } /** * Asynchronous `realpath`. The callback gets two arguments * `(err, resolvedPath)`. May use `process.cwd` to resolve relative paths. * * @example Usage example * var cache = {'/etc':'/private/etc'}; * fs.realpath('/etc/passwd', cache, function (err, resolvedPath) { * if (err) throw err; * console.log(resolvedPath); * }); * * @param [String] path * @param [Object?] cache An object literal of mapped paths that can be used to * force a specific path resolution or avoid additional `fs.stat` calls for * known real paths. * @param [Function(BrowserFS.ApiError, String)] callback */ public realpath(path: string, cb?: (err: ApiError, resolvedPath?: string) =>any): void; public realpath(path: string, cache: {[path: string]: string}, cb: (err: ApiError, resolvedPath?: string) =>any): void; public realpath(path: string, arg2?: any, cb: (err: ApiError, resolvedPath?: string) => any = nopCb): void { var cache = typeof arg2 === 'object' ? arg2 : {}; cb = typeof arg2 === 'function' ? arg2 : nopCb; var newCb = <(err: ApiError, resolvedPath?: string) =>any> wrapCb(cb, 2); try { path = normalizePath(path); this.root.realpath(path, cache, newCb); } catch (e) { newCb(e); } } /** * Synchronous `realpath`. * @param [String] path * @param [Object?] cache An object literal of mapped paths that can be used to * force a specific path resolution or avoid additional `fs.stat` calls for * known real paths. * @return [String] */ public realpathSync(path: string, cache: {[path: string]: string} = {}): string { path = normalizePath(path); return this.root.realpathSync(path, cache); } public watchFile(filename: string, listener: (curr: Stats, prev: Stats) => void): void; public watchFile(filename: string, options: { persistent?: boolean; interval?: number; }, listener: (curr: Stats, prev: Stats) => void): void; public watchFile(filename: string, arg2: any, listener: (curr: Stats, prev: Stats) => void = nopCb): void { throw new ApiError(ErrorCode.ENOTSUP); } public unwatchFile(filename: string, listener: (curr: Stats, prev: Stats) => void = nopCb): void { throw new ApiError(ErrorCode.ENOTSUP); } public watch(filename: string, listener?: (event: string, filename: string) => any): _fs.FSWatcher; public watch(filename: string, options: { persistent?: boolean; }, listener?: (event: string, filename: string) => any): _fs.FSWatcher; public watch(filename: string, arg2: any, listener: (event: string, filename: string) => any = nopCb): _fs.FSWatcher { throw new ApiError(ErrorCode.ENOTSUP); } public F_OK: number = 0; public R_OK: number = 4; public W_OK: number = 2; public X_OK: number = 1; public access(path: string, callback: (err: ApiError) => void): void; public access(path: string, mode: number, callback: (err: ApiError) => void): void; public access(path: string, arg2: any, cb: (e: ApiError) => void = nopCb): void { throw new ApiError(ErrorCode.ENOTSUP); } public accessSync(path: string, mode?: number): void { throw new ApiError(ErrorCode.ENOTSUP); } public createReadStream(path: string, options?: { flags?: string; encoding?: string; fd?: number; mode?: number; autoClose?: boolean; }): _fs.ReadStream { throw new ApiError(ErrorCode.ENOTSUP); } public createWriteStream(path: string, options?: { flags?: string; encoding?: string; fd?: number; mode?: number; }): _fs.WriteStream { throw new ApiError(ErrorCode.ENOTSUP); } public _wrapCb: (cb: Function, args: number) => Function = wrapCb; } // Type checking. var _: typeof _fs = new FS(); export interface FSModule extends FS { /** * Retrieve the FS object backing the fs module. */ getFSModule(): FS; /** * Set the FS object backing the fs module. */ changeFSModule(newFs: FS): void; /** * The FS constructor. */ FS: typeof FS; }