/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ 'use strict'; const Promise = require('promise'); const {EventEmitter} = require('events'); const fs = require('graceful-fs'); const path = require('path'); const open = Promise.denodeify(fs.open); const readFile = Promise.denodeify(fs.readFile); const stat = Promise.denodeify(fs.stat); const hasOwn = Object.prototype.hasOwnProperty; const NOT_FOUND_IN_ROOTS = 'NotFoundInRootsError'; class Fastfs extends EventEmitter { constructor(name, roots, fileWatcher, {ignore, crawling, activity}) { super(); this._name = name; this._fileWatcher = fileWatcher; this._ignore = ignore; this._roots = roots.map(root => new File(root, { isDir: true })); this._fastPaths = Object.create(null); this._crawling = crawling; this._activity = activity; } build() { const rootsPattern = new RegExp( '^(' + this._roots.map(root => escapeRegExp(root.path)).join('|') + ')' ); return this._crawling.then(files => { let fastfsActivity; const activity = this._activity; if (activity) { fastfsActivity = activity.startEvent('Building in-memory fs for ' + this._name); } files.forEach(filePath => { if (filePath.match(rootsPattern)) { const newFile = new File(filePath, { isDir: false }); const parent = this._fastPaths[path.dirname(filePath)]; if (parent) { parent.addChild(newFile); } else { this._add(newFile); for (let file = newFile; file; file = file.parent) { if (!this._fastPaths[file.path]) { this._fastPaths[file.path] = file; } } } } }); if (activity) { activity.endEvent(fastfsActivity); } this._fileWatcher.on('all', this._processFileChange.bind(this)); }); } stat(filePath) { return Promise.resolve().then(() => { const file = this._getFile(filePath); return file.stat(); }); } getAllFiles() { // one-level-deep flatten of files return [].concat(...this._roots.map(root => root.getFiles())); } findFilesByExt(ext, { ignore } = {}) { return this.findFilesByExts([ext], {ignore}); } findFilesByExts(exts, { ignore } = {}) { return this.getAllFiles() .filter(file => ( exts.indexOf(file.ext()) !== -1 && (!ignore || !ignore(file.path)) )) .map(file => file.path); } findFilesByName(name, { ignore } = {}) { return this.getAllFiles() .filter( file => path.basename(file.path) === name && (!ignore || !ignore(file.path)) ) .map(file => file.path); } matchFilesByPattern(pattern) { return this.getAllFiles() .filter(file => file.path.match(pattern)) .map(file => file.path); } readFile(filePath) { const file = this._getFile(filePath); if (!file) { throw new Error(`Unable to find file with path: ${filePath}`); } return file.read(); } readWhile(filePath, predicate) { const file = this._getFile(filePath); if (!file) { throw new Error(`Unable to find file with path: ${filePath}`); } return file.readWhile(predicate); } closest(filePath, name) { for (let file = this._getFile(filePath).parent; file; file = file.parent) { if (file.children[name]) { return file.children[name].path; } } return null; } fileExists(filePath) { let file; try { file = this._getFile(filePath); } catch (e) { if (e.type === NOT_FOUND_IN_ROOTS) { return false; } throw e; } return file && !file.isDir; } dirExists(filePath) { let file; try { file = this._getFile(filePath); } catch (e) { if (e.type === NOT_FOUND_IN_ROOTS) { return false; } throw e; } return file && file.isDir; } matches(dir, pattern) { const dirFile = this._getFile(dir); if (!dirFile.isDir) { throw new Error(`Expected file ${dirFile.path} to be a directory`); } return Object.keys(dirFile.children) .filter(name => name.match(pattern)) .map(name => path.join(dirFile.path, name)); } _getRoot(filePath) { for (let i = 0; i < this._roots.length; i++) { const possibleRoot = this._roots[i]; if (isDescendant(possibleRoot.path, filePath)) { return possibleRoot; } } return null; } _getAndAssertRoot(filePath) { const root = this._getRoot(filePath); if (!root) { const error = new Error(`File ${filePath} not found in any of the roots`); error.type = NOT_FOUND_IN_ROOTS; throw error; } return root; } _getFile(filePath) { filePath = path.normalize(filePath); if (!hasOwn.call(this._fastPaths, filePath)) { this._fastPaths[filePath] = this._getAndAssertRoot(filePath).getFileFromPath(filePath); } return this._fastPaths[filePath]; } _add(file) { this._getAndAssertRoot(file.path).addChild(file); } _processFileChange(type, filePath, root, fstat) { const absPath = path.join(root, filePath); if (this._ignore(absPath) || (fstat && fstat.isDirectory())) { return; } // Make sure this event belongs to one of our roots. if (!this._getRoot(absPath)) { return; } if (type === 'delete' || type === 'change') { const file = this._getFile(absPath); if (file) { file.remove(); } } delete this._fastPaths[path.normalize(absPath)]; if (type !== 'delete') { this._add(new File(absPath, { isDir: false })); } this.emit('change', type, filePath, root, fstat); } } class File { constructor(filePath, { isDir }) { this.path = filePath; this.isDir = Boolean(isDir); if (this.isDir) { this.children = Object.create(null); } } read() { if (!this._read) { this._read = readFile(this.path, 'utf8'); } return this._read; } readWhile(predicate) { const CHUNK_SIZE = 512; let result = ''; return open(this.path, 'r').then(fd => { /* global Buffer: true */ const buffer = new Buffer(CHUNK_SIZE); const p = new Promise((resolve, reject) => { let counter = 0; const callback = (error, bytesRead) => { if (error) { reject(); return; } const chunk = buffer.toString('utf8', 0, bytesRead); result += chunk; if (bytesRead > 0 && predicate(chunk, counter++, result)) { readChunk(fd, buffer, callback); } else { if (bytesRead === 0 && !this._read) { // reached EOF this._read = Promise.resolve(result); } resolve(result); } }; readChunk(fd, buffer, callback); }); p.catch(() => fs.close(fd)); return p; }); function readChunk(fd, buffer, callback) { fs.read(fd, buffer, 0, CHUNK_SIZE, null, callback); } } stat() { if (!this._stat) { this._stat = stat(this.path); } return this._stat; } addChild(file) { const parts = path.relative(this.path, file.path).split(path.sep); if (parts.length === 0) { return; } if (parts.length === 1) { this.children[parts[0]] = file; file.parent = this; } else if (this.children[parts[0]]) { this.children[parts[0]].addChild(file); } else { const dir = new File(path.join(this.path, parts[0]), { isDir: true }); dir.parent = this; this.children[parts[0]] = dir; dir.addChild(file); } } getFileFromPath(filePath) { const parts = path.relative(this.path, filePath) .split(path.sep); /*eslint consistent-this:0*/ let file = this; for (let i = 0; i < parts.length; i++) { const fileName = parts[i]; if (!fileName) { continue; } if (!file || !file.isDir) { // File not found. return null; } file = file.children[fileName]; } return file; } getFiles() { let files = []; Object.keys(this.children).forEach(key => { const file = this.children[key]; if (file.isDir) { files = files.concat(file.getFiles()); } else { files.push(file); } }); return files; } ext() { return path.extname(this.path).replace(/^\./, ''); } remove() { if (!this.parent) { throw new Error(`No parent to delete ${this.path} from`); } delete this.parent.children[path.basename(this.path)]; } } function isDescendant(root, child) { return path.relative(root, child).indexOf('..') !== 0; } function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } module.exports = Fastfs;