/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const normalize = require("./normalize"); const join = require("./join"); const MemoryFileSystemError = require("./MemoryFileSystemError"); const errors = require("errno"); const stream = require("readable-stream"); const ReadableStream = stream.Readable; const WritableStream = stream.Writable; function isDir(item) { if(typeof item !== "object") return false; return item[""] === true; } function isFile(item) { if(typeof item !== "object") return false; return !item[""]; } function pathToArray(path) { path = normalize(path); const nix = /^\//.test(path); if(!nix) { if(!/^[A-Za-z]:/.test(path)) { throw new MemoryFileSystemError(errors.code.EINVAL, path); } path = path.replace(/[\\\/]+/g, "\\"); // multi slashs path = path.split(/[\\\/]/); path[0] = path[0].toUpperCase(); } else { path = path.replace(/\/+/g, "/"); // multi slashs path = path.substr(1).split("/"); } if(!path[path.length-1]) path.pop(); return path; } function trueFn() { return true; } function falseFn() { return false; } class MemoryFileSystem { constructor(data) { this.data = data || {}; this.join = join; this.pathToArray = pathToArray; this.normalize = normalize; } meta(_path) { const path = pathToArray(_path); let current = this.data; let i = 0; for(; i < path.length - 1; i++) { if(!isDir(current[path[i]])) return; current = current[path[i]]; } return current[path[i]]; } existsSync(_path) { return !!this.meta(_path); } statSync(_path) { let current = this.meta(_path); if(_path === "/" || isDir(current)) { return { isFile: falseFn, isDirectory: trueFn, isBlockDevice: falseFn, isCharacterDevice: falseFn, isSymbolicLink: falseFn, isFIFO: falseFn, isSocket: falseFn }; } else if(isFile(current)) { return { isFile: trueFn, isDirectory: falseFn, isBlockDevice: falseFn, isCharacterDevice: falseFn, isSymbolicLink: falseFn, isFIFO: falseFn, isSocket: falseFn }; } else { throw new MemoryFileSystemError(errors.code.ENOENT, _path, "stat"); } } readFileSync(_path, optionsOrEncoding) { const path = pathToArray(_path); let current = this.data; let i = 0 for(; i < path.length - 1; i++) { if(!isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readFile"); current = current[path[i]]; } if(!isFile(current[path[i]])) { if(isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.EISDIR, _path, "readFile"); else throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readFile"); } current = current[path[i]]; const encoding = typeof optionsOrEncoding === "object" ? optionsOrEncoding.encoding : optionsOrEncoding; return encoding ? current.toString(encoding) : current; } readdirSync(_path) { if(_path === "/") return Object.keys(this.data).filter(Boolean); const path = pathToArray(_path); let current = this.data; let i = 0; for(; i < path.length - 1; i++) { if(!isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readdir"); current = current[path[i]]; } if(!isDir(current[path[i]])) { if(isFile(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOTDIR, _path, "readdir"); else throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readdir"); } return Object.keys(current[path[i]]).filter(Boolean); } mkdirpSync(_path) { const path = pathToArray(_path); if(path.length === 0) return; let current = this.data; for(let i = 0; i < path.length; i++) { if(isFile(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOTDIR, _path, "mkdirp"); else if(!isDir(current[path[i]])) current[path[i]] = {"":true}; current = current[path[i]]; } return; } mkdirSync(_path) { const path = pathToArray(_path); if(path.length === 0) return; let current = this.data; let i = 0; for(; i < path.length - 1; i++) { if(!isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOENT, _path, "mkdir"); current = current[path[i]]; } if(isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.EEXIST, _path, "mkdir"); else if(isFile(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOTDIR, _path, "mkdir"); current[path[i]] = {"":true}; return; } _remove(_path, name, testFn) { const path = pathToArray(_path); const operation = name === "File" ? "unlink" : "rmdir"; if(path.length === 0) { throw new MemoryFileSystemError(errors.code.EPERM, _path, operation); } let current = this.data; let i = 0; for(; i < path.length - 1; i++) { if(!isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOENT, _path, operation); current = current[path[i]]; } if(!testFn(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOENT, _path, operation); delete current[path[i]]; return; } rmdirSync(_path) { return this._remove(_path, "Directory", isDir); } unlinkSync(_path) { return this._remove(_path, "File", isFile); } readlinkSync(_path) { throw new MemoryFileSystemError(errors.code.ENOSYS, _path, "readlink"); } writeFileSync(_path, content, optionsOrEncoding) { if(!content && !optionsOrEncoding) throw new Error("No content"); const path = pathToArray(_path); if(path.length === 0) { throw new MemoryFileSystemError(errors.code.EISDIR, _path, "writeFile"); } let current = this.data; let i = 0 for(; i < path.length - 1; i++) { if(!isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.ENOENT, _path, "writeFile"); current = current[path[i]]; } if(isDir(current[path[i]])) throw new MemoryFileSystemError(errors.code.EISDIR, _path, "writeFile"); const encoding = typeof optionsOrEncoding === "object" ? optionsOrEncoding.encoding : optionsOrEncoding; current[path[i]] = optionsOrEncoding || typeof content === "string" ? new Buffer(content, encoding) : content; return; } // stream methods createReadStream(path, options) { let stream = new ReadableStream(); let done = false; let data; try { data = this.readFileSync(path); } catch (e) { stream._read = function() { if (done) { return; } done = true; this.emit('error', e); this.push(null); }; return stream; } options = options || { }; options.start = options.start || 0; options.end = options.end || data.length; stream._read = function() { if (done) { return; } done = true; this.push(data.slice(options.start, options.end)); this.push(null); }; return stream; } createWriteStream(path) { let stream = new WritableStream(); try { // Zero the file and make sure it is writable this.writeFileSync(path, new Buffer(0)); } catch(e) { // This or setImmediate? stream.once('prefinish', function() { stream.emit('error', e); }); return stream; } let bl = [ ], len = 0; stream._write = (chunk, encoding, callback) => { bl.push(chunk); len += chunk.length; this.writeFile(path, Buffer.concat(bl, len), callback); } return stream; } // async functions exists(path, callback) { return callback(this.existsSync(path)); } writeFile(path, content, encoding, callback) { if(!callback) { callback = encoding; encoding = undefined; } try { this.writeFileSync(path, content, encoding); } catch(e) { return callback(e); } return callback(); } } // async functions ["stat", "readdir", "mkdirp", "rmdir", "unlink", "readlink"].forEach(function(fn) { MemoryFileSystem.prototype[fn] = function(path, callback) { let result; try { result = this[fn + "Sync"](path); } catch(e) { setImmediate(function() { callback(e); }); return; } setImmediate(function() { callback(null, result); }); }; }); ["mkdir", "readFile"].forEach(function(fn) { MemoryFileSystem.prototype[fn] = function(path, optArg, callback) { if(!callback) { callback = optArg; optArg = undefined; } let result; try { result = this[fn + "Sync"](path, optArg); } catch(e) { setImmediate(function() { callback(e); }); return; } setImmediate(function() { callback(null, result); }); }; }); module.exports = MemoryFileSystem;