filesniffer.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
const bluebird = require("bluebird");
const fs = require("fs");
const filehound = require("filehound");
const events_1 = require("events");
const byline = require("byline");
const path = require("path");
const zlib = require("zlib");
const isbinary_1 = require("./isbinary");
const bind_1 = require("./bind");
const ArrayCollector_1 = require("./ArrayCollector");
const ObjectCollector_1 = require("./ObjectCollector");
const NoopCollector_1 = require("./NoopCollector");
const LineStream = byline.LineStream;
function stringMatch(data, str) {
    return data.indexOf(str) !== -1;
}
function regExpMatch(data, pattern) {
    return pattern.test(data);
}
function getStats(file) {
    return fs.statSync(file);
}
function isDirectory(file) {
    return getStats(file).isDirectory();
}
function flatten(a, b) {
    return a.concat(b);
}
function asArray() {
    return new ArrayCollector_1.ArrayCollector();
}
exports.asArray = asArray;
function asObject() {
    return new ObjectCollector_1.ObjectCollector();
}
exports.asObject = asObject;
/** @class */
class FileSniffer extends events_1.EventEmitter {
    constructor() {
        super();
        this.filenames = [];
        this.targets = [];
        this.pending = 0;
        this.gzipMode = false;
        this.collector = new NoopCollector_1.NoopCollector();
        this.maxDepth = 0;
        bind_1.default(this);
    }
    createStream(file) {
        if (this.handleGzip(file)) {
            const lineStream = new LineStream();
            fs.createReadStream(file)
                .pipe(zlib.createGunzip())
                .pipe(lineStream);
            return lineStream;
        }
        return byline(fs.createReadStream(file, {
            encoding: 'utf-8'
        }));
    }
    createMatcher(pattern) {
        return _.isString(pattern) ? stringMatch : regExpMatch;
    }
    handleGzip(file) {
        return path.extname(file) === '.gz' && this.gzipMode;
    }
    nonBinaryFiles(file) {
        if (this.handleGzip(file)) {
            return true;
        }
        return !isbinary_1.isbinary(file);
    }
    groupByFileType(paths) {
        const dirs = [];
        const files = [];
        /* tslint:disable:no-increment-decrement */
        for (let i = 0; i < paths.length; i++) {
            try {
                isDirectory(paths[i]) ? dirs.push(paths[i]) : files.push(paths[i]);
            }
            catch (err) {
                this.emit('error', err);
            }
        }
        return {
            files,
            dirs
        };
    }
    getFiles() {
        if (this.targets.length === 0) {
            this.targets = [process.cwd()];
        }
        const fileTypes = this.groupByFileType(this.targets);
        const allFiles = Promise.resolve(fileTypes.files);
        let allDirs = Promise.resolve([]);
        if (fileTypes.dirs.length > 0) {
            allDirs = filehound
                .create()
                .depth(this.maxDepth)
                .ignoreHiddenFiles()
                .paths(fileTypes.dirs)
                .find();
        }
        return bluebird.join(allFiles, allDirs, flatten);
    }
    search(pattern) {
        return (files) => {
            this.pending = files.length;
            if (this.pending > 0) {
                bluebird.resolve(files).each((file) => {
                    this.searchFile(file, pattern);
                });
            }
            else {
                this.emitEnd();
            }
        };
    }
    searchFile(file, pattern) {
        const snifferStream = this.createStream(file);
        const isMatch = this.createMatcher(pattern);
        let matched = false;
        snifferStream.on('readable', () => {
            let line;
            /* tslint:disable:no-conditional-assignment */
            while (null !== (line = snifferStream.read())) {
                if (line instanceof Buffer) {
                    line = line.toString('utf8');
                }
                if (isMatch(line, pattern)) {
                    matched = true;
                    this.emit('match', file, line);
                    if (!(this.collector instanceof NoopCollector_1.NoopCollector)) {
                        this.collector.collect(line, { path: file });
                    }
                }
            }
        });
        snifferStream.on('end', () => {
            /* tslint:disable:no-increment-decrement */
            this.pending--;
            this.emit('eof', file);
            if (matched) {
                this.filenames.push(file);
            }
            this.emitEnd();
        });
    }
    emitEnd() {
        if (this.pending === 0) {
            this.emit('end', this.filenames);
        }
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Enables FileSniffer to search gzipped files
     *
     * @memberOf FileSniffer
     * @method
     * gzip
     * @param none
     * @return a FileSniffer instance
     * @example
     * import FileSniffer from 'FileSniffer';
     *
     * const sniffer = FileSniffer.create()
     *   .gzip()
     *   .collect(asArray())
     *   .find('str');
     */
    gzip() {
        this.gzipMode = true;
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Set the search path for FileSniffer - file or directory
     *
     * @memberOf FileSniffer
     * @method
     * path
     * @param {string} path - search path
     * @return a FileSniffer instance
     * @example
     * import FileSniffer from 'FileSniffer';
     *
     * const sniffer = FileSniffer.create()
     *   .path(someFile)
     *   .collect(asArray())
     *   .find('str');
     */
    path(path) {
        this.targets.push(path);
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Enables FileSniffer to search recursively
     *
     * @memberOf FileSniffer
     * @method
     * path
     * @param {string} path - path of file (or directory) to search
     * @return a FileSniffer instance
     * @example
     * import FileSniffer from 'FileSniffer';
     *
     * const sniffer = FileSniffer.create()
     *   .path(someFile)
     *   .collect(asArray())
     *   .find('str');
     */
    depth(maxDepth) {
        if (maxDepth < 0) {
            throw new Error('Depth must be >= 0');
        }
        this.maxDepth = maxDepth;
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Set the search paths for FileSniffer - can be a mixture of file and directories
     *
     * @memberOf FileSniffer
     * @method
     * paths
     * @param {string} paths - search paths
     * @return a FileSniffer instance
     * @example
     * import FileSniffer from 'FileSniffer';
     *
     * const sniffer = FileSniffer.create()
     *   .paths(arrayOfPaths)
     *   .collect(asArray())
     *   .find('str');
     */
    paths(...paths) {
        if (typeof paths[0] !== 'string' && !_.isArray(paths[0])) {
            throw new TypeError('paths must be an array');
        }
        this.targets = _.flatten(paths);
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Set the search paths for FileSniffer - can be a mixture of file and directories
     *
     * @memberOf FileSniffer
     * @method
     * find
     * @param {string|pattern}  - search criteria
     * @return a promise of matches
     * @example
     * import FileSniffer from 'FileSniffer';
     *
     * const matches = await FileSniffer.create()
     *   .collect(asArray())
     *   .find('str');
     */
    find(pattern) {
        if (!pattern) {
            return Promise.reject(new Error('Search string or pattern must be specified'));
        }
        const search = this.getFiles()
            .filter(this.nonBinaryFiles)
            .then(this.search(pattern));
        return search.then(() => {
            return new Promise((resolve, reject) => {
                this.on('end', () => {
                    resolve(this.collector.matches());
                });
            });
        });
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Sets the collector
     *
     * @memberOf FileSniffer
     * @method
     * collect
     * @param {Collector}  - logic to collect matching lines
     * @return a FileSniffer instance
     * @example
     * import FileSniffer from 'FileSniffer';
     *
     * const matches = await FileSniffer.create()
     *   .collect(asObject())
     *   .find('str');
     */
    collect(collector) {
        this.collector = collector;
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Static factory method to create an instance of FileSniffer
     *
     * @static
     * @memberOf FileSniffer
     * @method
     * create
     * @return FileSniffer instance
     * @example
     * import FileSniffer from 'filesniffer';
     *
     * const FileSniffer = FileSniffer.create();
     */
    static create() {
        return new FileSniffer();
    }
}
exports.FileSniffer = FileSniffer;
//# sourceMappingURL=filesniffer.js.map