UNPKG

@forabi/memfs

Version:

In-memory file-system with Node's fs API.

1,457 lines (1,189 loc) 80.6 kB
import {resolve as resolveCrossPlatform} from 'path'; import * as pathModule from 'path'; import {Node, Link, File, Stats} from "./node"; import {Buffer} from 'buffer'; import setImmediate from './setImmediate'; import process from './process'; const extend = require('fast-extend'); const errors = require('./internal/errors'); import setTimeoutUnref, {TSetTimeout} from "./setTimeoutUnref"; import {Readable, Writable} from 'stream'; const util = require('util'); import {constants} from "./constants"; import {EventEmitter} from "events"; import {ReadStream, WriteStream} from "fs"; const {O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC, O_APPEND, O_DIRECTORY, O_NOATIME, O_NOFOLLOW, O_SYNC, O_DIRECT, O_NONBLOCK, F_OK, R_OK, W_OK, X_OK} = constants; let sep, relative; if (pathModule.posix) { const {posix} = pathModule; sep = posix.sep; relative = posix.relative; } else { sep = pathModule.sep; relative = pathModule.relative; } const isWin = process.platform === 'win32'; // ---------------------------------------- Types // Node-style errors with a `code` property. export interface IError extends Error { code?: string, } export type TFilePath = string | Buffer | URL; export type TFileId = TFilePath | number; // Number is used as a file descriptor. export type TDataOut = string | Buffer; // Data formats we give back to users. export type TData = TDataOut | Uint8Array; // Data formats users can give us. export type TFlags = string | number; export type TMode = string | number; // Mode can be a String, although docs say it should be a Number. export type TEncoding = 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'binary' | 'hex'; export type TEncodingExtended = TEncoding | 'buffer'; export type TTime = number | string | Date; export type TCallback<TData> = (error?: IError, data?: TData) => void; // type TCallbackWrite = (err?: IError, bytesWritten?: number, source?: Buffer) => void; // type TCallbackWriteStr = (err?: IError, written?: number, str?: string) => void; // ---------------------------------------- Constants const ENCODING_UTF8: TEncoding = 'utf8'; // Default modes for opening files. const enum MODE { FILE = 0o666, DIR = 0o777, DEFAULT = MODE.FILE, } const kMinPoolSpace = 128; // const kMaxLength = require('buffer').kMaxLength; // ---------------------------------------- Error messages // TODO: Use `internal/errors.js` in the future. const ERRSTR = { PATH_STR: 'path must be a string or Buffer', // FD: 'file descriptor must be a unsigned 32-bit integer', FD: 'fd must be a file descriptor', MODE_INT: 'mode must be an int', CB: 'callback must be a function', UID: 'uid must be an unsigned int', GID: 'gid must be an unsigned int', LEN: 'len must be an integer', ATIME: 'atime must be an integer', MTIME: 'mtime must be an integer', PREFIX: 'filename prefix is required', BUFFER: 'buffer must be an instance of Buffer or StaticBuffer', OFFSET: 'offset must be an integer', LENGTH: 'length must be an integer', POSITION: 'position must be an integer', }; const ERRSTR_OPTS = tipeof => `Expected options to be either an object or a string, but got ${tipeof} instead`; // const ERRSTR_FLAG = flag => `Unknown file open flag: ${flag}`; const ENOENT = 'ENOENT'; const EBADF = 'EBADF'; const EINVAL = 'EINVAL'; const EPERM = 'EPERM'; const EPROTO = 'EPROTO'; const EEXIST = 'EEXIST'; const ENOTDIR = 'ENOTDIR'; const EMFILE = 'EMFILE'; const EACCES = 'EACCES'; const EISDIR = 'EISDIR'; const ENOTEMPTY = 'ENOTEMPTY'; function formatError(errorCode: string, func = '', path = '', path2 = '') { let pathFormatted = ''; if(path) pathFormatted = ` '${path}'`; if(path2) pathFormatted += ` -> '${path2}'`; switch(errorCode) { case ENOENT: return `ENOENT: no such file or directory, ${func}${pathFormatted}`; case EBADF: return `EBADF: bad file descriptor, ${func}${pathFormatted}`; case EINVAL: return `EINVAL: invalid argument, ${func}${pathFormatted}`; case EPERM: return `EPERM: operation not permitted, ${func}${pathFormatted}`; case EPROTO: return `EPROTO: protocol error, ${func}${pathFormatted}`; case EEXIST: return `EEXIST: file already exists, ${func}${pathFormatted}`; case ENOTDIR: return `ENOTDIR: not a directory, ${func}${pathFormatted}`; case EISDIR: return `EISDIR: illegal operation on a directory, ${func}${pathFormatted}`; case EACCES: return `EACCES: permission denied, ${func}${pathFormatted}`; case ENOTEMPTY: return `ENOTEMPTY: directory not empty, ${func}${pathFormatted}`; case EMFILE: return `EMFILE: too many open files, ${func}${pathFormatted}`; default: return `${errorCode}: error occurred, ${func}${pathFormatted}`; } } function createError(errorCode: string, func = '', path = '', path2 = '', Constructor = Error) { const error = new Constructor(formatError(errorCode, func, path, path2)); (error as any).code = errorCode; return error; } function throwError(errorCode: string, func = '', path = '', path2 = '', Constructor = Error) { throw createError(errorCode, func, path, path2, Constructor); } // ---------------------------------------- Flags // List of file `flags` as defined by Node. export enum FLAGS { // Open file for reading. An exception occurs if the file does not exist. r = O_RDONLY, // Open file for reading and writing. An exception occurs if the file does not exist. 'r+' = O_RDWR, // Open file for reading in synchronous mode. Instructs the operating system to bypass the local file system cache. rs = O_RDONLY | O_SYNC, sr = FLAGS.rs, // Open file for reading and writing, telling the OS to open it synchronously. See notes for 'rs' about using this with caution. 'rs+' = O_RDWR | O_SYNC, 'sr+' = FLAGS['rs+'], // Open file for writing. The file is created (if it does not exist) or truncated (if it exists). w = O_WRONLY | O_CREAT | O_TRUNC, // Like 'w' but fails if path exists. wx = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, xw = FLAGS.wx, // Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). 'w+' = O_RDWR | O_CREAT | O_TRUNC, // Like 'w+' but fails if path exists. 'wx+' = O_RDWR | O_CREAT | O_TRUNC | O_EXCL, 'xw+' = FLAGS['wx+'], // Open file for appending. The file is created if it does not exist. a = O_WRONLY | O_APPEND | O_CREAT, // Like 'a' but fails if path exists. ax = O_WRONLY | O_APPEND | O_CREAT | O_EXCL, xa = FLAGS.ax, // Open file for reading and appending. The file is created if it does not exist. 'a+' = O_RDWR | O_APPEND | O_CREAT, // Like 'a+' but fails if path exists. 'ax+' = O_RDWR | O_APPEND | O_CREAT | O_EXCL, 'xa+' = FLAGS['ax+'], } export function flagsToNumber(flags: TFlags): number { if(typeof flags === 'number') return flags; if(typeof flags === 'string') { const flagsNum = FLAGS[flags]; if(typeof flagsNum !== 'undefined') return flagsNum; } // throw new TypeError(formatError(ERRSTR_FLAG(flags))); throw new errors.TypeError('ERR_INVALID_OPT_VALUE', 'flags', flags); } // ---------------------------------------- Options function assertEncoding(encoding: string) { if(encoding && !Buffer.isEncoding(encoding)) throw new errors.TypeError('ERR_INVALID_OPT_VALUE_ENCODING', encoding); } function getOptions <T extends IOptions> (defaults: T, options?: T|string): T { let opts: T; if(!options) return defaults; else { var tipeof = typeof options; switch(tipeof) { case 'string': opts = extend({}, defaults, {encoding: options as string}); break; case 'object': opts = extend({}, defaults, options); break; default: throw TypeError(ERRSTR_OPTS(tipeof)); } } if(opts.encoding !== 'buffer') assertEncoding(opts.encoding); return opts; } function optsGenerator<TOpts>(defaults: TOpts): (opts) => TOpts { return options => getOptions(defaults, options); } function validateCallback(callback) { if(typeof callback !== 'function') throw TypeError(ERRSTR.CB); return callback; } function optsAndCbGenerator<TOpts, TResult>(getOpts): (options, callback?) => [TOpts, TCallback<TResult>] { return (options, callback?) => typeof options === 'function' ? [getOpts(), options] : [getOpts(options), validateCallback(callback)]; } // General options with optional `encoding` property that most commands accept. export interface IOptions { encoding?: TEncoding | TEncodingExtended; } export interface IFileOptions extends IOptions { mode?: TMode; flag?: TFlags; } const optsDefaults: IOptions = { encoding: 'utf8', }; const getDefaultOpts = optsGenerator<IOptions>(optsDefaults); const getDefaultOptsAndCb = optsAndCbGenerator<IOptions, any>(getDefaultOpts); // Options for `fs.readFile` and `fs.readFileSync`. export interface IReadFileOptions extends IOptions { flag?: string; } const readFileOptsDefaults: IReadFileOptions = { flag: 'r', }; const getReadFileOptions = optsGenerator<IReadFileOptions>(readFileOptsDefaults); // Options for `fs.writeFile` and `fs.writeFileSync` export interface IWriteFileOptions extends IFileOptions {} const writeFileDefaults: IWriteFileOptions = { encoding: 'utf8', mode: MODE.DEFAULT, flag: FLAGS[FLAGS.w], }; const getWriteFileOptions = optsGenerator<IWriteFileOptions>(writeFileDefaults); // Options for `fs.appendFile` and `fs.appendFileSync` export interface IAppendFileOptions extends IFileOptions {} const appendFileDefaults: IAppendFileOptions = { encoding: 'utf8', mode: MODE.DEFAULT, flag: FLAGS[FLAGS.a], }; const getAppendFileOpts = optsGenerator<IAppendFileOptions>(appendFileDefaults); const getAppendFileOptsAndCb = optsAndCbGenerator<IAppendFileOptions, void>(getAppendFileOpts); // Options for `fs.realpath` and `fs.realpathSync` export interface IRealpathOptions { encoding?: TEncodingExtended, } const realpathDefaults: IReadFileOptions = optsDefaults; const getRealpathOptions = optsGenerator<IRealpathOptions>(realpathDefaults); const getRealpathOptsAndCb = optsAndCbGenerator<IRealpathOptions, TDataOut>(getRealpathOptions); // Options for `fs.watchFile` export interface IWatchFileOptions { persistent?: boolean, interval?: number, } // Options for `fs.createReadStream` export interface IReadStreamOptions { flags?: TFlags, encoding?: TEncoding, fd?: number, mode?: TMode, autoClose?: boolean, start?: number, end?: number, } // Options for `fs.createWriteStream` export interface IWriteStreamOptions { flags?: TFlags, defaultEncoding?: TEncoding, fd?: number, mode?: TMode, autoClose?: boolean, start?: number, } // Options for `fs.watch` export interface IWatchOptions extends IOptions { persistent?: boolean, recursive?: boolean, } // ---------------------------------------- Utility functions function getPathFromURLPosix(url) { if (url.hostname !== '') { return new errors.TypeError('ERR_INVALID_FILE_URL_HOST', process.platform); } let pathname = url.pathname; for (let n = 0; n < pathname.length; n++) { if (pathname[n] === '%') { let third = pathname.codePointAt(n + 2) | 0x20; if (pathname[n + 1] === '2' && third === 102) { return new errors.TypeError('ERR_INVALID_FILE_URL_PATH', 'must not include encoded / characters'); } } } return decodeURIComponent(pathname); } export function pathToFilename(path: TFilePath): string { if((typeof path !== 'string') && !Buffer.isBuffer(path)) { try { if(!(path instanceof require('url').URL)) throw new TypeError(ERRSTR.PATH_STR); } catch (err) { throw new TypeError(ERRSTR.PATH_STR); } path = getPathFromURLPosix(path); } const pathString = String(path); nullCheck(pathString); // return slash(pathString); return pathString; } type TResolve = (filename: string, base?: string) => string; let resolve: TResolve = (filename, base = process.cwd()) => resolveCrossPlatform(base, filename); if(isWin) { const _resolve = resolve; const {unixify} = require("fs-monkey/lib/correctPath"); resolve = (filename, base) => unixify(_resolve(filename, base)); } export function filenameToSteps(filename: string, base?: string): string[] { const fullPath = resolve(filename, base); const fullPathSansSlash = fullPath.substr(1); if(!fullPathSansSlash) return []; return fullPathSansSlash.split(sep); } export function pathToSteps(path: TFilePath): string[] { return filenameToSteps(pathToFilename(path)); } export function dataToStr(data: TData, encoding: string = ENCODING_UTF8): string { if(Buffer.isBuffer(data)) return data.toString(encoding); else if(data instanceof Uint8Array) return Buffer.from(data).toString(encoding); else return String(data); } export function dataToBuffer(data: TData, encoding: string = ENCODING_UTF8): Buffer { if(Buffer.isBuffer(data)) return data; else if(data instanceof Uint8Array) return Buffer.from(data); else return Buffer.from(String(data), encoding); } export function strToEncoding(str: string, encoding?: TEncodingExtended): TDataOut { if(!encoding || (encoding === ENCODING_UTF8)) return str; // UTF-8 if(encoding === 'buffer') return new Buffer(str); // `buffer` encoding return (new Buffer(str)).toString(encoding); // Custom encoding } export function bufferToEncoding(buffer: Buffer, encoding?: TEncodingExtended): TDataOut { if(!encoding || (encoding === 'buffer')) return buffer; else return buffer.toString(encoding); } function nullCheck(path, callback?) { if(('' + path).indexOf('\u0000') !== -1) { const er = new Error('Path must be a string without null bytes'); (er as any).code = ENOENT; if(typeof callback !== 'function') throw er; process.nextTick(callback, er); return false; } return true; } function _modeToNumber(mode: TMode, def?): number { if(typeof mode === 'number') return mode; if(typeof mode === 'string') return parseInt(mode, 8); if(def) return modeToNumber(def); return undefined; } function modeToNumber(mode: TMode, def?): number { const result = _modeToNumber(mode, def); if((typeof result !== 'number') || isNaN(result)) throw new TypeError(ERRSTR.MODE_INT); return result; } function isFd(path): boolean { return (path >>> 0) === path; } function validateFd(fd) { if(!isFd(fd)) throw TypeError(ERRSTR.FD); } // converts Date or number to a fractional UNIX timestamp export function toUnixTimestamp(time) { if(typeof time === 'string' && (+time == (time as any))) { return +time; } if(isFinite(time)) { if (time < 0) { return Date.now() / 1000; } return time; } if(time instanceof Date) { return time.getTime() / 1000; } throw new Error('Cannot parse time: ' + time); } /** * Returns optional argument and callback * @param arg Argument or callback value * @param callback Callback or undefined * @param def Default argument value */ function getArgAndCb<TArg, TRes>(arg: TArg | TCallback<TRes>, callback?: TCallback<TRes>, def?: TArg): [TArg, TCallback<TRes>] { return typeof arg === 'function' ? [def, arg] : [arg, callback]; } function validateUid(uid: number) { if(typeof uid !== 'number') throw TypeError(ERRSTR.UID); } function validateGid(gid: number) { if(typeof gid !== 'number') throw TypeError(ERRSTR.GID); } // ---------------------------------------- Volume /** * `Volume` represents a file system. */ export class Volume { static fromJSON(json: {[filename: string]: string}, cwd?: string): Volume { const vol = new Volume; vol.fromJSON(json, cwd); return vol; } /** * Global file descriptor counter. UNIX file descriptors start from 0 and go sequentially * up, so here, in order not to conflict with them, we choose some big number and descrease * the file descriptor of every new opened file. * @type {number} * @todo This should not be static, right? */ static fd: number = 0xFFFFFFFF; // Constructor function used to create new nodes. // NodeClass: new (...args) => TNode = Node as new (...args) => TNode; // Hard link to the root of this volume. // root: Node = new (this.NodeClass)(null, '', true); root: Link; // I-node number counter. ino: number = 0; // A mapping for i-node numbers to i-nodes (`Node`); inodes: {[ino: number]: Node} = {}; // List of released i-node numbers, for reuse. releasedInos: number[] = []; // A mapping for file descriptors to `File`s. fds: {[fd: number]: File} = {}; // A list of reusable (opened and closed) file descriptors, that should be // used first before creating a new file descriptor. releasedFds = []; // Max number of open files. maxFiles = 10000; // Current number of open files. openFiles = 0; StatWatcher: new () => StatWatcher; ReadStream: new (...args) => IReadStream; WriteStream: new (...args) => IWriteStream; FSWatcher: new () => FSWatcher; props: { Node: new (...args) => Node, Link: new (...args) => Link, File: new (...File) => File, }; constructor(props = {}) { this.props = extend({Node, Link, File}, props); const root = this.createLink(); root.setNode(this.createNode(true)); const self = this; this.StatWatcher = class extends StatWatcher { constructor() { super(self); } }; const _ReadStream: new (...args) => IReadStream = FsReadStream as any; this.ReadStream = class extends _ReadStream { constructor(...args) { super(self, ...args); } } as any as new (...args) => IReadStream; const _WriteStream: new (...args) => IWriteStream = FsWriteStream as any; this.WriteStream = class extends _WriteStream { constructor(...args) { super(self, ...args); } } as any as new (...args) => IWriteStream; this.FSWatcher = class extends FSWatcher { constructor() { super(self); } }; // root.setChild('.', root); // root.getNode().nlink++; // root.setChild('..', root); // root.getNode().nlink++; this.root = root; } createLink(): Link; createLink(parent: Link, name: string, isDirectory?: boolean, perm?: number): Link; createLink(parent?: Link, name?: string, isDirectory: boolean = false, perm?: number): Link { return parent ? parent.createChild(name, this.createNode(isDirectory, perm)) : new this.props.Link(this, null, ''); } deleteLink(link: Link): boolean { const parent = link.parent; if(parent) { parent.deleteChild(link); link.vol = null; link.parent = null; return true; } return false; } private newInoNumber(): number { if(this.releasedInos.length) return this.releasedInos.pop(); else { this.ino = (this.ino + 1) % 0xFFFFFFFF; return this.ino; } } private newFdNumber(): number { return this.releasedFds.length ? this.releasedFds.pop() : Volume.fd--; } createNode(isDirectory: boolean = false, perm?: number): Node { const node = new this.props.Node(this.newInoNumber(), perm); if(isDirectory) node.setIsDirectory(); this.inodes[node.ino] = node; return node; } private getNode(ino: number) { return this.inodes[ino]; } private deleteNode(node: Node) { node.del(); delete this.inodes[node.ino]; this.releasedInos.push(node.ino); } // Generates 6 character long random string, used by `mkdtemp`. genRndStr() { const str = (Math.random() + 1).toString(36).substr(2, 6); if(str.length === 6) return str; else return this.genRndStr(); } // Returns a `Link` (hard link) referenced by path "split" into steps. getLink(steps: string[]): Link { return this.root.walk(steps); } // Just link `getLink`, but throws a correct user error, if link to found. getLinkOrThrow(filename: string, funcName?: string): Link { const steps = filenameToSteps(filename); const link = this.getLink(steps); if(!link) throwError(ENOENT, funcName, filename); return link; } // Just like `getLink`, but also dereference/resolves symbolic links. getResolvedLink(filenameOrSteps: string | string[]): Link { let steps: string[] = typeof filenameOrSteps === 'string' ? filenameToSteps(filenameOrSteps) : filenameOrSteps; let link = this.root; let i = 0; while(i < steps.length) { let step = steps[i]; link = link.getChild(step); if(!link) return null; const node = link.getNode(); if(node.isSymlink()) { steps = node.symlink.concat(steps.slice(i + 1)); link = this.root; i = 0; continue; } i++; } return link; } // Just like `getLinkOrThrow`, but also dereference/resolves symbolic links. getResolvedLinkOrThrow(filename: string, funcName?: string): Link { let link = this.getResolvedLink(filename); if(!link) throwError(ENOENT, funcName, filename); return link; } resolveSymlinks(link: Link): Link { // let node: Node = link.getNode(); // while(link && node.isSymlink()) { // link = this.getLink(node.symlink); // if(!link) return null; // node = link.getNode(); // } // return link; return this.getResolvedLink(link.steps.slice(1)); } // Just like `getLinkOrThrow`, but also verifies that the link is a directory. private getLinkAsDirOrThrow(filename: string, funcName?: string): Link { const link = this.getLinkOrThrow(filename, funcName); if(!link.getNode().isDirectory()) throwError(ENOTDIR, funcName, filename); return link; } // Get the immediate parent directory of the link. private getLinkParent(steps: string[]): Link { return this.root.walk(steps, steps.length - 1); } private getLinkParentAsDirOrThrow(filenameOrSteps: string | string[], funcName?: string): Link { const steps = filenameOrSteps instanceof Array ? filenameOrSteps : filenameToSteps(filenameOrSteps); const link = this.getLinkParent(steps); if(!link) throwError(ENOENT, funcName, sep + steps.join(sep)); if(!link.getNode().isDirectory()) throwError(ENOTDIR, funcName, sep + steps.join(sep)); return link; } private getFileByFd(fd: number): File { return this.fds[String(fd)]; } private getFileByFdOrThrow(fd: number, funcName?: string): File { if(!isFd(fd)) throw TypeError(ERRSTR.FD); const file = this.getFileByFd(fd); if(!file) throwError(EBADF, funcName); return file; } private getNodeByIdOrCreate(id: TFileId, flags: number, perm: number): Node { if(typeof id === 'number') { const file = this.getFileByFd(id); if(!file) throw Error('File nto found'); return file.node; } else { const steps = pathToSteps(id as TFilePath); let link: Link = this.getLink(steps); if(link) return link.getNode(); // Try creating a node if not found. if(flags & O_CREAT) { const dirLink = this.getLinkParent(steps); if(dirLink) { const name = steps[steps.length - 1]; link = this.createLink(dirLink, name, false, perm); return link.getNode(); } } throwError(ENOENT, 'getNodeByIdOrCreate', pathToFilename(id)) } } private wrapAsync(method: (...args) => void, args: any[], callback: TCallback<any>) { validateCallback(callback); setImmediate(() => { try { callback(null, method.apply(this, args)); } catch(err) { callback(err); } }); } private _toJSON(link = this.root, json = {}, path?: string) { let isEmpty = true; for(let name in link.children) { isEmpty = false; let child = link.getChild(name); let node = child.getNode(); if(node.isFile()) { let filename = child.getPath(); if(path) filename = relative(path, filename); json[filename] = node.getString(); } else if(node.isDirectory()) { this._toJSON(child, json, path); } } let dirPath = link.getPath(); if(path) dirPath = relative(path, dirPath); if (dirPath && isEmpty) { json[dirPath] = null; } return json; } toJSON(paths?: TFilePath | TFilePath[], json = {}, isRelative = false) { let links: Link[] = []; if(paths) { if(!(paths instanceof Array)) paths = [paths]; for(let path of paths) { const filename = pathToFilename(path); const link = this.getResolvedLink(filename); if(!link) continue; links.push(link); } } else { links.push(this.root); } if(!links.length) return json; for(let link of links) this._toJSON(link, json, isRelative ? link.getPath() : ''); return json; } // fromJSON(json: {[filename: string]: string}, cwd: string = '/') { fromJSON(json: {[filename: string]: string}, cwd: string = process.cwd()) { for(let filename in json) { const data = json[filename]; if (typeof data === 'string') { filename = resolve(filename, cwd); const steps = filenameToSteps(filename); if(steps.length > 1) { const dirname = sep + steps.slice(0, steps.length - 1).join(sep); this.mkdirpBase(dirname, MODE.DIR); } this.writeFileSync(filename, data); } else { this.mkdirpBase(filename, MODE.DIR); } } } reset() { this.ino = 0; this.inodes = {}; this.releasedInos = []; this.fds = {}; this.releasedFds = []; this.openFiles = 0; this.root = this.createLink(); this.root.setNode(this.createNode(true)); } // Legacy interface mountSync(mountpoint: string, json: {[filename: string]: string}) { this.fromJSON(json, mountpoint); } private openLink(link: Link, flagsNum: number, resolveSymlinks: boolean = true): File { if(this.openFiles >= this.maxFiles) { // Too many open files. throw createError(EMFILE, 'open', link.getPath()); } // Resolve symlinks. let realLink: Link = link; if(resolveSymlinks) realLink = this.resolveSymlinks(link); if(!realLink) throwError(ENOENT, 'open', link.getPath()); const node = realLink.getNode(); if(node.isDirectory() && (flagsNum !== FLAGS.r)) throwError(EISDIR, 'open', link.getPath()); // Check node permissions if(!(flagsNum & O_WRONLY)) { if(!node.canRead()) { throwError(EACCES, 'open', link.getPath()); } } if(flagsNum & O_RDWR) { } const file = new this.props.File(link, node, flagsNum, this.newFdNumber()); this.fds[file.fd] = file; this.openFiles++; if(flagsNum & O_TRUNC) file.truncate(); return file; } private openFile(filename: string, flagsNum: number, modeNum: number, resolveSymlinks: boolean = true): File { const steps = filenameToSteps(filename); let link: Link = this.getResolvedLink(steps); // Try creating a new file, if it does not exist. if(!link) { // const dirLink: Link = this.getLinkParent(steps); const dirLink: Link = this.getResolvedLink(steps.slice(0, steps.length - 1)); // if(!dirLink) throwError(ENOENT, 'open', filename); if(!dirLink) throwError(ENOENT, 'open', sep + steps.join(sep)); if((flagsNum & O_CREAT) && (typeof modeNum === 'number')) { link = this.createLink(dirLink, steps[steps.length - 1], false, modeNum); } } if(link) return this.openLink(link, flagsNum, resolveSymlinks); } private openBase(filename: string, flagsNum: number, modeNum: number, resolveSymlinks: boolean = true): number { const file = this.openFile(filename, flagsNum, modeNum, resolveSymlinks); if(!file) throwError(ENOENT, 'open', filename); return file.fd; } openSync(path: TFilePath, flags: TFlags, mode: TMode = MODE.DEFAULT): number { // Validate (1) mode; (2) path; (3) flags - in that order. const modeNum = modeToNumber(mode); const fileName = pathToFilename(path); const flagsNum = flagsToNumber(flags); return this.openBase(fileName, flagsNum, modeNum); } open(path: TFilePath, flags: TFlags, /* ... */ callback: TCallback<number>); open(path: TFilePath, flags: TFlags, mode: TMode, callback: TCallback<number>); open(path: TFilePath, flags: TFlags, a: TMode|TCallback<number>, b?: TCallback<number>) { let mode: TMode = a as TMode; let callback: TCallback<number> = b as TCallback<number>; if(typeof a === 'function') { mode = MODE.DEFAULT; callback = a; } mode = mode || MODE.DEFAULT; const modeNum = modeToNumber(mode); const fileName = pathToFilename(path); const flagsNum = flagsToNumber(flags); this.wrapAsync(this.openBase, [fileName, flagsNum, modeNum], callback); } private closeFile(file: File) { if(!this.fds[file.fd]) return; this.openFiles--; delete this.fds[file.fd]; this.releasedFds.push(file.fd); } closeSync(fd: number) { validateFd(fd); const file = this.getFileByFdOrThrow(fd, 'close'); this.closeFile(file); } close(fd: number, callback: TCallback<void>) { validateFd(fd); this.wrapAsync(this.closeSync, [fd], callback); } private openFileOrGetById(id: TFileId, flagsNum: number, modeNum?: number): File { if(typeof id === 'number') { const file = this.fds[id]; if(!file) throw createError(ENOENT); return file; } else { return this.openFile(pathToFilename(id), flagsNum, modeNum); } } private readBase(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, position: number): number { const file = this.getFileByFdOrThrow(fd); return file.read(buffer, Number(offset), Number(length), position); } readSync(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, position: number): number { validateFd(fd); return this.readBase(fd, buffer, offset, length, position); } read(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, position: number, callback: (err?: Error, bytesRead?: number, buffer?: Buffer | Uint8Array) => void) { validateCallback(callback); // This `if` branch is from Node.js if(length === 0) { return process.nextTick(() => { callback && callback(null, 0, buffer); }); } setImmediate(() => { try { const bytes = this.readBase(fd, buffer, offset, length, position); callback(null, bytes, buffer); } catch(err) { callback(err); } }); } private readFileBase(id: TFileId, flagsNum: number, encoding: TEncoding): Buffer | string { let result: Buffer | string; const userOwnsFd = isFd(id); let fd: number; if(userOwnsFd) { fd = id as number; } else { fd = this.openSync(id as TFilePath, flagsNum); } try { result = bufferToEncoding(this.getFileByFdOrThrow(fd).getBuffer(), encoding); } finally { if(!userOwnsFd) { this.closeSync(fd); } } return result; } readFileSync(file: TFileId, options?: IReadFileOptions|string): TDataOut { const opts = getReadFileOptions(options); const flagsNum = flagsToNumber(opts.flag); return this.readFileBase(file, flagsNum, opts.encoding as TEncoding); } readFile(id: TFileId, callback: TCallback<TDataOut>); readFile(id: TFileId, options: IReadFileOptions|string, callback: TCallback<TDataOut>); readFile(id: TFileId, a: TCallback<TDataOut>|IReadFileOptions|string, b?: TCallback<TDataOut>) { const [opts, callback] = optsAndCbGenerator<IReadFileOptions, TCallback<TDataOut>>(getReadFileOptions)(a, b); const flagsNum = flagsToNumber(opts.flag); this.wrapAsync(this.readFileBase, [id, flagsNum, opts.encoding], callback); } private writeBase(fd: number, buf: Buffer, offset?: number, length?: number, position?: number): number { const file = this.getFileByFdOrThrow(fd, 'write'); return file.write(buf, offset, length, position); } writeSync(fd: number, buffer: Buffer | Uint8Array, offset?: number, length?: number, position?: number): number; writeSync(fd: number, str: string, position?: number, encoding?: TEncoding): number; writeSync(fd: number, a: string | Buffer | Uint8Array, b?: number, c?: number | TEncoding, d?: number): number { validateFd(fd); let encoding: TEncoding; let offset: number; let length: number; let position: number; const isBuffer = typeof a !== 'string'; if(isBuffer) { offset = b | 0; length = (c as number); position = d; } else { position = b; encoding = c as TEncoding; } const buf: Buffer = dataToBuffer(a, encoding); if(isBuffer) { if(typeof length === 'undefined') { length = buf.length; } } else { offset = 0; length = buf.length; } return this.writeBase(fd, buf, offset, length, position); } write(fd: number, buffer: Buffer | Uint8Array, callback: (...args) => void); write(fd: number, buffer: Buffer | Uint8Array, offset: number, callback: (...args) => void); write(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, callback: (...args) => void); write(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, position: number, callback: (...args) => void); write(fd: number, str: string, callback: (...args) => void); write(fd: number, str: string, position: number, callback: (...args) => void); write(fd: number, str: string, position: number, encoding: TEncoding, callback: (...args) => void); write(fd: number, a?, b?, c?, d?, e?) { validateFd(fd); let offset: number; let length: number; let position: number; let encoding: TEncoding; let callback: (...args) => void; const tipa = typeof a; const tipb = typeof b; const tipc = typeof c; const tipd = typeof d; if(tipa !== 'string') { if(tipb === 'function') { callback = b; } else if(tipc === 'function') { offset = b | 0; callback = c; } else if(tipd === 'function') { offset = b | 0; length = c; callback = d; } else { offset = b | 0; length = c; position = d; callback = e; } } else { if(tipb === 'function') { callback = b; } else if(tipc === 'function') { position = b; callback = c; } else if(tipd === 'function') { position = b; encoding = c; callback = d; } } const buf: Buffer = dataToBuffer(a, encoding); if(tipa !== 'string') { if(typeof length === 'undefined') length = buf.length; } else { offset = 0; length = buf.length; } validateCallback(callback); setImmediate(() => { try { const bytes = this.writeBase(fd, buf, offset, length, position); if(tipa !== 'string') { callback(null, bytes, buf); } else { callback(null, bytes, a); } } catch(err) { callback(err); } }); } private writeFileBase(id: TFileId, buf: Buffer, flagsNum: number, modeNum: number) { // console.log('writeFileBase', id, buf, flagsNum, modeNum); // const node = this.getNodeByIdOrCreate(id, flagsNum, modeNum); // node.setBuffer(buf); const isUserFd = typeof id === 'number'; let fd: number; if(isUserFd) fd = id as number; else { fd = this.openBase(pathToFilename(id as TFilePath), flagsNum, modeNum); // fd = this.openSync(id as TFilePath, flagsNum, modeNum); } let offset = 0; let length = buf.length; let position = (flagsNum & O_APPEND) ? null : 0; try { while(length > 0) { let written = this.writeSync(fd, buf, offset, length, position); offset += written; length -= written; if(position !== null) position += written; } } finally { if(!isUserFd) this.closeSync(fd); } } writeFileSync(id: TFileId, data: TData, options?: IWriteFileOptions) { const opts = getWriteFileOptions(options); const flagsNum = flagsToNumber(opts.flag); const modeNum = modeToNumber(opts.mode); const buf = dataToBuffer(data, opts.encoding); this.writeFileBase(id, buf, flagsNum, modeNum); } writeFile(id: TFileId, data: TData, callback: TCallback<void>); writeFile(id: TFileId, data: TData, options: IWriteFileOptions|string, callback: TCallback<void>); writeFile(id: TFileId, data: TData, a: TCallback<void>|IWriteFileOptions|string, b?: TCallback<void>) { let options: IWriteFileOptions|string = a as IWriteFileOptions; let callback: TCallback<void> = b; if(typeof a === 'function') { options = writeFileDefaults; callback = a; } const opts = getWriteFileOptions(options); const flagsNum = flagsToNumber(opts.flag); const modeNum = modeToNumber(opts.mode); const buf = dataToBuffer(data, opts.encoding); this.wrapAsync(this.writeFileBase, [id, buf, flagsNum, modeNum], callback); } private linkBase(filename1: string, filename2: string) { const steps1 = filenameToSteps(filename1); const link1 = this.getLink(steps1); if(!link1) throwError(ENOENT, 'link', filename1, filename2); const steps2 = filenameToSteps(filename2); // Check new link directory exists. const dir2 = this.getLinkParent(steps2); if(!dir2) throwError(ENOENT, 'link', filename1, filename2); const name = steps2[steps2.length - 1]; // Check if new file already exists. if(dir2.getChild(name)) throwError(EEXIST, 'link', filename1, filename2); const node =link1.getNode(); node.nlink++; dir2.createChild(name, node); } linkSync(existingPath: TFilePath, newPath: TFilePath) { const existingPathFilename = pathToFilename(existingPath); const newPathFilename = pathToFilename(newPath); this.linkBase(existingPathFilename, newPathFilename); } link(existingPath: TFilePath, newPath: TFilePath, callback: TCallback<void>) { const existingPathFilename = pathToFilename(existingPath); const newPathFilename = pathToFilename(newPath); this.wrapAsync(this.linkBase, [existingPathFilename, newPathFilename], callback); } private unlinkBase(filename: string) { const steps = filenameToSteps(filename); const link = this.getLink(steps); if(!link) throwError(ENOENT, 'unlink', filename); // TODO: Check if it is file, dir, other... if(link.length) throw Error('Dir not empty...'); this.deleteLink(link); const node = link.getNode(); node.nlink--; // When all hard links to i-node are deleted, remove the i-node, too. if(node.nlink <= 0) { this.deleteNode(node); } } unlinkSync(path: TFilePath) { const filename = pathToFilename(path); this.unlinkBase(filename); } unlink(path: TFilePath, callback: TCallback<void>) { const filename = pathToFilename(path); this.wrapAsync(this.unlinkBase, [filename], callback); } private symlinkBase(targetFilename: string, pathFilename: string): Link { const pathSteps = filenameToSteps(pathFilename); // Check if directory exists, where we about to create a symlink. const dirLink = this.getLinkParent(pathSteps); if(!dirLink) throwError(ENOENT, 'symlink', targetFilename, pathFilename); const name = pathSteps[pathSteps.length - 1]; // Check if new file already exists. if(dirLink.getChild(name)) throwError(EEXIST, 'symlink', targetFilename, pathFilename); // Create symlink. const symlink: Link = dirLink.createChild(name); symlink.getNode().makeSymlink(filenameToSteps(targetFilename)); return symlink; } // `type` argument works only on Windows. symlinkSync(target: TFilePath, path: TFilePath, type?: 'file' | 'dir' | 'junction') { const targetFilename = pathToFilename(target); const pathFilename = pathToFilename(path); this.symlinkBase(targetFilename, pathFilename); } symlink(target: TFilePath, path: TFilePath, callback: TCallback<void>); symlink(target: TFilePath, path: TFilePath, type: 'file' | 'dir' | 'junction', callback: TCallback<void>); symlink(target: TFilePath, path: TFilePath, a, b?) { const [type, callback] = getArgAndCb<'file' | 'dir' | 'junction', TCallback<void>>(a, b); const targetFilename = pathToFilename(target); const pathFilename = pathToFilename(path); this.wrapAsync(this.symlinkBase, [targetFilename, pathFilename], callback); } private realpathBase(filename: string, encoding: TEncodingExtended): TDataOut { const steps = filenameToSteps(filename); const link: Link = this.getLink(steps); // TODO: this check has to be perfomed by `lstat`. if(!link) throwError(ENOENT, 'realpath', filename); // Resolve symlinks. const realLink = this.resolveSymlinks(link); if(!realLink) throwError(ENOENT, 'realpath', filename); return strToEncoding(realLink.getPath(), encoding); } realpathSync(path: TFilePath, options?: IRealpathOptions): TDataOut { return this.realpathBase(pathToFilename(path), getRealpathOptions(options).encoding); } realpath(path: TFilePath, callback: TCallback<TDataOut>); realpath(path: TFilePath, options: IRealpathOptions | string, callback: TCallback<TDataOut>); realpath(path: TFilePath, a: TCallback<TDataOut> | IRealpathOptions | string, b?: TCallback<TDataOut>) { const [opts, callback] = getRealpathOptsAndCb(a, b); const pathFilename = pathToFilename(path); this.wrapAsync(this.realpathBase, [pathFilename, opts.encoding], callback); } private lstatBase(filename: string): Stats { const link: Link = this.getLink(filenameToSteps(filename)); if(!link) throwError(ENOENT, 'lstat', filename); return Stats.build(link.getNode()); } lstatSync(path: TFilePath): Stats { return this.lstatBase(pathToFilename(path)); } lstat(path: TFilePath, callback: TCallback<Stats>) { this.wrapAsync(this.lstatBase, [pathToFilename(path)], callback); } private statBase(filename: string): Stats { let link: Link = this.getLink(filenameToSteps(filename)); if(!link) throwError(ENOENT, 'stat', filename); // Resolve symlinks. link = this.resolveSymlinks(link); if(!link) throwError(ENOENT, 'stat', filename); return Stats.build(link.getNode()); } statSync(path: TFilePath): Stats { return this.statBase(pathToFilename(path)); } stat(path: TFilePath, callback: TCallback<Stats>) { this.wrapAsync(this.statBase, [pathToFilename(path)], callback); } private fstatBase(fd: number): Stats { const file = this.getFileByFd(fd); if(!file) throwError(EBADF, 'fstat'); return Stats.build(file.node); } fstatSync(fd: number): Stats { return this.fstatBase(fd); } fstat(fd: number, callback: TCallback<Stats>) { this.wrapAsync(this.fstatBase, [fd], callback); } private renameBase(oldPathFilename: string, newPathFilename: string) { const link: Link = this.getLink(filenameToSteps(oldPathFilename)); if(!link) throwError(ENOENT, 'rename', oldPathFilename, newPathFilename); // TODO: Check if it is directory, if non-empty, we cannot move it, right? const newPathSteps = filenameToSteps(newPathFilename); // Check directory exists for the new location. const newPathDirLink: Link = this.getLinkParent(newPathSteps); if(!newPathDirLink) throwError(ENOENT, 'rename', oldPathFilename, newPathFilename); // TODO: Also treat cases with directories and symbolic links. // TODO: See: http://man7.org/linux/man-pages/man2/rename.2.html // Remove hard link from old folder. const oldLinkParent = link.parent; if(oldLinkParent) { oldLinkParent.deleteChild(link); } // Rename should overwrite the new path, if that exists. const name = newPathSteps[newPathSteps.length - 1]; link.steps = [...newPathDirLink.steps, name]; newPathDirLink.setChild(link.getName(), link); } renameSync(oldPath: TFilePath, newPath: TFilePath) { const oldPathFilename = pathToFilename(oldPath); const newPathFilename = pathToFilename(newPath); this.renameBase(oldPathFilename, newPathFilename); } rename(oldPath: TFilePath, newPath: TFilePath, callback: TCallback<void>) { const oldPathFilename = pathToFilename(oldPath); const newPathFilename = pathToFilename(newPath); this.wrapAsync(this.renameBase, [oldPathFilename, newPathFilename], callback); } private existsBase(filename: string): boolean { return !!this.statBase(filename); } existsSync(path: TFilePath): boolean { try { return this.existsBase(pathToFilename(path)); } catch(err) { return false; } } exists(path: TFilePath, callback: (exists: boolean) => void) { const filename = pathToFilename(path); if(typeof callback !== 'function') throw Error(ERRSTR.CB); setImmediate(() => { try { callback(this.existsBase(filename)); } catch(err) { callback(false); } }); } private accessBase(filename: string, mode: number) { const link = this.getLinkOrThrow(filename, 'access'); // TODO: Verify permissions } accessSync(path: TFilePath, mode: number = F_OK) { const filename = pathToFilename(path); mode = mode | 0; this.accessBase(filename, mode); } access(path: TFilePath, callback: TCallback<void>); access(path: TFilePath, mode: number, callback: TCallback<vo