<commit_before>var minify = (function(undefined) {

	var exec = require('child_process').exec,
			_fs = require('fs');

	var minify = function(options) {
		this.type = options.type;
		this.tempFile = new Date().getTime().toString();

		if(options.type == 'gcc' && typeof options.fileIn === 'string') {
			this.fileIn = [options.fileIn];
		}
		else if(typeof options.fileIn === 'string') {
			this.fileIn = options.fileIn;
		}

		if(typeof options.fileIn === 'object' && options.fileIn instanceof Array && options.type != 'gcc') {
			var out = options.fileIn.map(function(path) {
				return _fs.readFileSync(path, 'utf8');
			});

			_fs.writeFileSync(this.tempFile, out.join('\n'), 'utf8');

			this.fileIn = this.tempFile;
		}
		else if(typeof options.fileIn === 'object' && options.fileIn instanceof Array && options.type == 'gcc') {
			this.fileIn = options.fileIn;
		}

		this.fileOut = options.fileOut;
		this.options = options.options || [];
		this.buffer = options.buffer || 1000 * 1024;
		if (typeof options.callback !== 'undefined') {
			this.callback = options.callback;
		}

		this.compress();
	};

	minify.prototype = minify.fn = {
		type: null,
		fileIn: null,
		fileOut: null,
		callback: null,
		buffer: null, // with larger files you will need a bigger buffer for closure compiler
		compress: function() {
			var self = this, command;

			switch (this.type) {
				case 'yui':
				case 'yui-css':
					command = 'java -jar -Xss2048k "' + __dirname + '/yuicompressor-2.4.7.jar" "' + this.fileIn + '" -o "' + this.fileOut + '" --type css ' + this.options.join(' ');
					break;
				case 'yui-js':
					command = 'java -jar -Xss2048k "' + __dirname + '/yuicompressor-2.4.7.jar" "' + this.fileIn + '" -o "' + this.fileOut + '" --type js ' + this.options.join(' ');
					break;
				case 'gcc':
					var fileInCommand = this.fileIn.map(function(file) {
						return '--js="' + file + '"';
					});
					command = 'java -jar "' + __dirname + '/google_closure_compiler-r1810.jar" ' + fileInCommand.join(' ') + ' --js_output_file="' + this.fileOut + '" ' + this.options.join(' ');
					break;
				case 'uglifyjs':
					command = '"' + __dirname  + '/../node_modules/uglify-js/bin/uglifyjs" --output "' + this.fileOut + '" --no-copyright "' + this.fileIn + '" ' + this.options.join(' ');
					break;
			}

			exec(command, { maxBuffer: this.buffer }, function (err, stdout, stderr) {
				//console.log(err);
				//console.log(stdout);
				//console.log(stderr);

				if(self.fileIn === self.tempFile) {
					// remove the temp concat file here
					_fs.unlinkSync(self.tempFile);
				}

				if(self.callback){
					if (err) {
						self.callback(err);
					} else {
						self.callback(null);
					}
				}
			});
		}
	};

	return minify;

})();

exports.version = '0.3.10';
exports.minify = minify;<commit_msg>Added tempPath option to allow temporary files to be created in a specified folder rather than just in the root folder.<commit_after>var minify = (function(undefined) {

	var exec = require('child_process').exec,
			_fs = require('fs');

	var minify = function(options) {
		this.type = options.type;
		this.tempFile = options.tempPath + new Date().getTime().toString();

		if(options.type == 'gcc' && typeof options.fileIn === 'string') {
			this.fileIn = [options.fileIn];
		}
		else if(typeof options.fileIn === 'string') {
			this.fileIn = options.fileIn;
		}

		if(typeof options.fileIn === 'object' && options.fileIn instanceof Array && options.type != 'gcc') {
			var out = options.fileIn.map(function(path) {
				return _fs.readFileSync(path, 'utf8');
			});

			_fs.writeFileSync(this.tempFile, out.join('\n'), 'utf8');

			this.fileIn = this.tempFile;
		}
		else if(typeof options.fileIn === 'object' && options.fileIn instanceof Array && options.type == 'gcc') {
			this.fileIn = options.fileIn;
		}

		this.fileOut = options.fileOut;
		this.options = options.options || [];
		this.buffer = options.buffer || 1000 * 1024;
		if (typeof options.callback !== 'undefined') {
			this.callback = options.callback;
		}

		this.compress();
	};

	minify.prototype = minify.fn = {
		type: null,
		fileIn: null,
		fileOut: null,
		callback: null,
		buffer: null, // with larger files you will need a bigger buffer for closure compiler
		compress: function() {
			var self = this, command;

			switch (this.type) {
				case 'yui':
				case 'yui-css':
					command = 'java -jar -Xss2048k "' + __dirname + '/yuicompressor-2.4.7.jar" "' + this.fileIn + '" -o "' + this.fileOut + '" --type css ' + this.options.join(' ');
					break;
				case 'yui-js':
					command = 'java -jar -Xss2048k "' + __dirname + '/yuicompressor-2.4.7.jar" "' + this.fileIn + '" -o "' + this.fileOut + '" --type js ' + this.options.join(' ');
					break;
				case 'gcc':
					var fileInCommand = this.fileIn.map(function(file) {
						return '--js="' + file + '"';
					});
					command = 'java -jar "' + __dirname + '/google_closure_compiler-r1810.jar" ' + fileInCommand.join(' ') + ' --js_output_file="' + this.fileOut + '" ' + this.options.join(' ');
					break;
				case 'uglifyjs':
					command = '"' + __dirname  + '/../node_modules/uglify-js/bin/uglifyjs" --output "' + this.fileOut + '" --no-copyright "' + this.fileIn + '" ' + this.options.join(' ');
					break;
			}

			exec(command, { maxBuffer: this.buffer }, function (err, stdout, stderr) {
				//console.log(err);
				//console.log(stdout);
				//console.log(stderr);

				if(self.fileIn === self.tempFile) {
					// remove the temp concat file here
					_fs.unlinkSync(self.tempFile);
				}

				if(self.callback){
					if (err) {
						self.callback(err);
					} else {
						self.callback(null);
					}
				}
			});
		}
	};

	return minify;

})();

exports.version = '0.3.10';
exports.minify = minify;<|endoftext|>


<commit_before>var fs     = require('fs')
  , events = require('events')
  , buffer = require('buffer')
  , http   = require('http')
  , url    = require('url')
  , path   = require('path')
  , mime   = require('mime')
  , util   = require('./node-static/util');

// Current version
var version = [0, 7, 7];

var Server = function (root, options) {
    if (root && (typeof(root) === 'object')) { options = root; root = null }

    // resolve() doesn't normalize (to lowercase) drive letters on Windows
    this.root    = path.normalize(path.resolve(root || '.'));
    this.options = options || {};
    this.cache   = 3600;

    this.defaultHeaders  = {};
    this.options.headers = this.options.headers || {};

    this.options.indexFile = this.options.indexFile || "index.html";

    if ('cache' in this.options) {
        if (typeof(this.options.cache) === 'number') {
            this.cache = this.options.cache;
        } else if (! this.options.cache) {
            this.cache = false;
        }
    }

    if ('serverInfo' in this.options) {
        this.serverInfo = this.options.serverInfo.toString();
    } else {
        this.serverInfo = 'node-static/' + version.join('.');
    }

    this.defaultHeaders['server'] = this.serverInfo;

    if (this.cache !== false) {
        this.defaultHeaders['cache-control'] = 'max-age=' + this.cache;
    }

    for (var k in this.defaultHeaders) {
        this.options.headers[k] = this.options.headers[k] ||
                                  this.defaultHeaders[k];
    }
};

Server.prototype.serveDir = function (pathname, req, res, finish) {
    var htmlIndex = path.join(pathname, this.options.indexFile),
        that = this;

    fs.stat(htmlIndex, function (e, stat) {
        if (!e) {
            var status = 200;
            var headers = {};
            var originalPathname = decodeURI(url.parse(req.url).pathname);
            if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') {
                return finish(301, { 'Location': originalPathname + '/' });
            } else {
                that.respond(null, status, headers, [htmlIndex], stat, req, res, finish);
            }
        } else {
            // Stream a directory of files as a single file.
            fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
                if (e) { return finish(404, {}) }
                var index = JSON.parse(contents);
                streamFiles(index.files);
            });
        }
    });
    function streamFiles(files) {
        util.mstat(pathname, files, function (e, stat) {
            if (e) { return finish(404, {}) }
            that.respond(pathname, 200, {}, files, stat, req, res, finish);
        });
    }
};

Server.prototype.serveFile = function (pathname, status, headers, req, res) {
    var that = this;
    var promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    fs.stat(pathname, function (e, stat) {
        if (e) {
            return promise.emit('error', e);
        }
        that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
            that.finish(status, headers, req, res, promise);
        });
    });
    return promise;
};

Server.prototype.finish = function (status, headers, req, res, promise, callback) {
    var result = {
        status:  status,
        headers: headers,
        message: http.STATUS_CODES[status]
    };

    headers['server'] = this.serverInfo;

    if (!status || status >= 400) {
        if (callback) {
            callback(result);
        } else {
            if (promise.listeners('error').length > 0) {
                promise.emit('error', result);
            }
            else {
              res.writeHead(status, headers);
              res.end();
            }
        }
    } else {
        // Don't end the request here, if we're streaming;
        // it's taken care of in `prototype.stream`.
        if (status !== 200 || req.method !== 'GET') {
            res.writeHead(status, headers);
            res.end();
        }
        callback && callback(null, result);
        promise.emit('success', result);
    }
};

Server.prototype.servePath = function (pathname, status, headers, req, res, pathValidator, finish) {
    var that = this,
        promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    // Make sure we're not trying to access a
    // file outside of the root.
    if (pathname.indexOf(that.root) === 0) {
        if (pathValidator && !pathValidator(pathname)) {
            // Forbidden
            finish(403, {});
            return promise;
        }
        fs.stat(pathname, function (e, stat) {
            if (e) {
                finish(404, {});
            } else if (stat.isFile()) {      // Stream a single file.
                that.respond(null, status, headers, [pathname], stat, req, res, finish);
            } else if (stat.isDirectory()) { // Stream a directory of files.
                that.serveDir(pathname, req, res, finish);
            } else {
                finish(400, {});
            }
        });
    } else {
        // Forbidden
        finish(403, {});
    }
    return promise;
};

Server.prototype.resolve = function (pathname) {
    return path.resolve(path.join(this.root, pathname));
};

Server.prototype.serve = function (req, res, options, callback) {
    if (typeof options === 'function') {
        callback = options;
        options = null;
    }
    var that    = this,
        promise = new(events.EventEmitter),
        pathname;

    var finish = function (status, headers) {
        that.finish(status, headers, req, res, promise, callback);
    };

    try {
        pathname = decodeURI(url.parse(req.url).pathname);
    }
    catch(e) {
        return process.nextTick(function() {
            return finish(400, {});
        });
    }

    process.nextTick(function () {
        that.servePath(pathname, 200, {}, req, res, options && options.validatePath, finish).on('success', function (result) {
            promise.emit('success', result);
        }).on('error', function (err) {
            promise.emit('error');
        });
    });
    if (! callback) { return promise }
};

/* Check if we should consider sending a gzip version of the file based on the
 * file content type and client's Accept-Encoding header value.
 */
Server.prototype.gzipOk = function(req, contentType) {
    var enable = this.options.gzip;
    if(enable &&
        (typeof enable === 'boolean' ||
            (contentType && (enable instanceof RegExp) && enable.test(contentType)))) {
        var acceptEncoding = req.headers['accept-encoding'];
        return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0;
    }
    return false;
}

/* Send a gzipped version of the file if the options and the client indicate gzip is enabled and
 * we find a .gz file mathing the static resource requested.
 */
Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) {
    var that = this;
    if(files.length == 1 && this.gzipOk(req, contentType)) {
        var gzFile = files[0] + ".gz";
        fs.stat(gzFile, function(e, gzStat) {
            if(!e && gzStat.isFile()) {
                //console.log('Serving', gzFile, 'to gzip-capable client instead of', files[0], 'new size is', gzStat.size, 'uncompressed size', stat.size);
                var vary = _headers['Vary'];
                _headers['Vary'] = (vary && vary != 'Accept-Encoding'?vary+', ':'')+'Accept-Encoding';
                _headers['Content-Encoding'] = 'gzip';
                stat.size = gzStat.size;
                files = [gzFile];
            } else {
                //console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile());
            }
            that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
        });
    } else {
        // Client doesn't want gzip or we're sending multiple files
        that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
    }
}

Server.prototype.parseByteRange = function(req, stat) {
    var byteRange = {
      from: 0,
      to: 0,
      valid: false
    }

    var rangeHeader = req.headers['range'];
    var flavor = 'bytes=';

    if (rangeHeader) {
        if (rangeHeader.indexOf(flavor) == 0 && rangeHeader.indexOf(',') == -1) {
            /* Parse */
            rangeHeader = rangeHeader.substr(flavor.length).split('-');
            byteRange.from = parseInt(rangeHeader[0]);
            byteRange.to = parseInt(rangeHeader[1]);

            /* Replace empty fields of differential requests by absolute values */
            if (isNaN(byteRange.from) && !isNaN(byteRange.to)) {
                byteRange.from = stat.size - byteRange.to;
                byteRange.to = stat.size ? stat.size - 1 : 0;
            } else if (!isNaN(byteRange.from) && isNaN(byteRange.to)) {
                byteRange.to = stat.size ? stat.size - 1 : 0;
            }

            /* General byte range validation */
            if (!isNaN(byteRange.from) && !!byteRange.to && 0 <= byteRange.from < byteRange.to) {
                byteRange.valid = true;
            } else {
                console.warn("Request contains invalid range header: ", rangeHeader);
            }
        } else {
            console.warn("Request contains unsupported range header: ", rangeHeader);
        }
    }
    return byteRange;
}

Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) {
    var mtime           = Date.parse(stat.mtime),
        key             = pathname || files[0],
        headers         = {},
        clientETag      = req.headers['if-none-match'],
        clientMTime     = Date.parse(req.headers['if-modified-since']),
        startByte       = 0,
        length          = stat.size,
        byteRange       = this.parseByteRange(req, stat);

    /* Handle byte ranges */
    if (files.length == 1 && byteRange.valid) {
        if (byteRange.to < length) {

            // Note: HTTP Range param is inclusive
            startByte = byteRange.from;
            length = byteRange.to - byteRange.from + 1;
            status = 206;

            // Set Content-Range response header (we advertise initial resource size on server here (stat.size))
            headers['Content-Range'] = 'bytes ' + byteRange.from + '-' + byteRange.to + '/' + stat.size;

        } else {
            byteRange.valid = false;
            console.warn("Range request exceeds file '" + key + "' boundaries, goes until byte no", byteRange.to, "against file size of", length, "bytes");
        }
    }

    /* In any case, check for unhandled byte range headers */
    if (!byteRange.valid && req.headers['range']) {
        console.error(new Error("Range request present but invalid, might serve whole file instead"));
    }

    // Copy default headers
    for (var k in this.options.headers) {  headers[k] = this.options.headers[k] }
    // Copy custom headers
    for (var k in _headers) { headers[k] = _headers[k] }

    headers['Etag']          = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
    headers['Date']          = new(Date)().toUTCString();
    headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
    headers['Content-Type']   = contentType;
    headers['Content-Length'] = length;

    for (var k in _headers) { headers[k] = _headers[k] }

    // Conditional GET
    // If the "If-Modified-Since" or "If-None-Match" headers
    // match the conditions, send a 304 Not Modified.
    if ((clientMTime  || clientETag) &&
        (!clientETag  || clientETag === headers['Etag']) &&
        (!clientMTime || clientMTime >= mtime)) {
        // 304 response should not contain entity headers
        ['Content-Encoding',
         'Content-Language',
         'Content-Length',
         'Content-Location',
         'Content-MD5',
         'Content-Range',
         'Content-Type',
         'Expires',
         'Last-Modified'].forEach(function(entityHeader) {
            delete headers[entityHeader];
        });
        finish(304, headers);
    } else {

        res.writeHead(status, headers);

        this.stream(key, files, new(buffer.Buffer)(length), startByte, res, function (e, buffer) {
            if (e) { return finish(500, {}) }
            finish(status, headers);
        });
    }
};

Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
    var contentType = _headers['Content-Type'] ||
                      mime.lookup(files[0]) + (this.options.charset ? '; charset=' + this.options.charset : '') ||
                      'application/octet-stream';

    if(this.options.gzip) {
        this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
    } else {
        this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
    }
}

Server.prototype.stream = function (pathname, files, buffer, startByte, res, callback) {

    (function streamFile(files, offset) {
        var file = files.shift();

        if (file) {
            file = path.resolve(file) === path.normalize(file)  ? file : path.join(pathname || '.', file);

            // Stream the file to the client
            fs.createReadStream(file, {
                flags: 'r',
                mode: 0666,
                start: startByte,
                end: startByte + (buffer.length ? buffer.length - 1 : 0)
            }).on('data', function (chunk) {
                // Bounds check the incoming chunk and offset, as copying
                // a buffer from an invalid offset will throw an error and crash
                if (chunk.length && offset < buffer.length && offset >= 0) {
                    chunk.copy(buffer, offset);
                    offset += chunk.length;
                }
            }).on('close', function () {
                streamFile(files, offset);
            }).on('error', function (err) {
                callback(err);
                console.error(err);
            }).pipe(res, { end: false });
        } else {
            res.end();
            callback(null, buffer, offset);
        }
    })(files.slice(0), 0);
};

// Exports
exports.Server       = Server;
exports.version      = version;
exports.mime         = mime;

<commit_msg>Update usage of mime<commit_after>var fs     = require('fs')
  , events = require('events')
  , buffer = require('buffer')
  , http   = require('http')
  , url    = require('url')
  , path   = require('path')
  , mime   = require('mime')
  , util   = require('./node-static/util');

// Current version
var version = [0, 7, 7];

var Server = function (root, options) {
    if (root && (typeof(root) === 'object')) { options = root; root = null }

    // resolve() doesn't normalize (to lowercase) drive letters on Windows
    this.root    = path.normalize(path.resolve(root || '.'));
    this.options = options || {};
    this.cache   = 3600;

    this.defaultHeaders  = {};
    this.options.headers = this.options.headers || {};

    this.options.indexFile = this.options.indexFile || "index.html";

    if ('cache' in this.options) {
        if (typeof(this.options.cache) === 'number') {
            this.cache = this.options.cache;
        } else if (! this.options.cache) {
            this.cache = false;
        }
    }

    if ('serverInfo' in this.options) {
        this.serverInfo = this.options.serverInfo.toString();
    } else {
        this.serverInfo = 'node-static/' + version.join('.');
    }

    this.defaultHeaders['server'] = this.serverInfo;

    if (this.cache !== false) {
        this.defaultHeaders['cache-control'] = 'max-age=' + this.cache;
    }

    for (var k in this.defaultHeaders) {
        this.options.headers[k] = this.options.headers[k] ||
                                  this.defaultHeaders[k];
    }
};

Server.prototype.serveDir = function (pathname, req, res, finish) {
    var htmlIndex = path.join(pathname, this.options.indexFile),
        that = this;

    fs.stat(htmlIndex, function (e, stat) {
        if (!e) {
            var status = 200;
            var headers = {};
            var originalPathname = decodeURI(url.parse(req.url).pathname);
            if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') {
                return finish(301, { 'Location': originalPathname + '/' });
            } else {
                that.respond(null, status, headers, [htmlIndex], stat, req, res, finish);
            }
        } else {
            // Stream a directory of files as a single file.
            fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
                if (e) { return finish(404, {}) }
                var index = JSON.parse(contents);
                streamFiles(index.files);
            });
        }
    });
    function streamFiles(files) {
        util.mstat(pathname, files, function (e, stat) {
            if (e) { return finish(404, {}) }
            that.respond(pathname, 200, {}, files, stat, req, res, finish);
        });
    }
};

Server.prototype.serveFile = function (pathname, status, headers, req, res) {
    var that = this;
    var promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    fs.stat(pathname, function (e, stat) {
        if (e) {
            return promise.emit('error', e);
        }
        that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
            that.finish(status, headers, req, res, promise);
        });
    });
    return promise;
};

Server.prototype.finish = function (status, headers, req, res, promise, callback) {
    var result = {
        status:  status,
        headers: headers,
        message: http.STATUS_CODES[status]
    };

    headers['server'] = this.serverInfo;

    if (!status || status >= 400) {
        if (callback) {
            callback(result);
        } else {
            if (promise.listeners('error').length > 0) {
                promise.emit('error', result);
            }
            else {
              res.writeHead(status, headers);
              res.end();
            }
        }
    } else {
        // Don't end the request here, if we're streaming;
        // it's taken care of in `prototype.stream`.
        if (status !== 200 || req.method !== 'GET') {
            res.writeHead(status, headers);
            res.end();
        }
        callback && callback(null, result);
        promise.emit('success', result);
    }
};

Server.prototype.servePath = function (pathname, status, headers, req, res, pathValidator, finish) {
    var that = this,
        promise = new(events.EventEmitter);

    pathname = this.resolve(pathname);

    // Make sure we're not trying to access a
    // file outside of the root.
    if (pathname.indexOf(that.root) === 0) {
        if (pathValidator && !pathValidator(pathname)) {
            // Forbidden
            finish(403, {});
            return promise;
        }
        fs.stat(pathname, function (e, stat) {
            if (e) {
                finish(404, {});
            } else if (stat.isFile()) {      // Stream a single file.
                that.respond(null, status, headers, [pathname], stat, req, res, finish);
            } else if (stat.isDirectory()) { // Stream a directory of files.
                that.serveDir(pathname, req, res, finish);
            } else {
                finish(400, {});
            }
        });
    } else {
        // Forbidden
        finish(403, {});
    }
    return promise;
};

Server.prototype.resolve = function (pathname) {
    return path.resolve(path.join(this.root, pathname));
};

Server.prototype.serve = function (req, res, options, callback) {
    if (typeof options === 'function') {
        callback = options;
        options = null;
    }
    var that    = this,
        promise = new(events.EventEmitter),
        pathname;

    var finish = function (status, headers) {
        that.finish(status, headers, req, res, promise, callback);
    };

    try {
        pathname = decodeURI(url.parse(req.url).pathname);
    }
    catch(e) {
        return process.nextTick(function() {
            return finish(400, {});
        });
    }

    process.nextTick(function () {
        that.servePath(pathname, 200, {}, req, res, options && options.validatePath, finish).on('success', function (result) {
            promise.emit('success', result);
        }).on('error', function (err) {
            promise.emit('error');
        });
    });
    if (! callback) { return promise }
};

/* Check if we should consider sending a gzip version of the file based on the
 * file content type and client's Accept-Encoding header value.
 */
Server.prototype.gzipOk = function(req, contentType) {
    var enable = this.options.gzip;
    if(enable &&
        (typeof enable === 'boolean' ||
            (contentType && (enable instanceof RegExp) && enable.test(contentType)))) {
        var acceptEncoding = req.headers['accept-encoding'];
        return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0;
    }
    return false;
}

/* Send a gzipped version of the file if the options and the client indicate gzip is enabled and
 * we find a .gz file mathing the static resource requested.
 */
Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) {
    var that = this;
    if(files.length == 1 && this.gzipOk(req, contentType)) {
        var gzFile = files[0] + ".gz";
        fs.stat(gzFile, function(e, gzStat) {
            if(!e && gzStat.isFile()) {
                //console.log('Serving', gzFile, 'to gzip-capable client instead of', files[0], 'new size is', gzStat.size, 'uncompressed size', stat.size);
                var vary = _headers['Vary'];
                _headers['Vary'] = (vary && vary != 'Accept-Encoding'?vary+', ':'')+'Accept-Encoding';
                _headers['Content-Encoding'] = 'gzip';
                stat.size = gzStat.size;
                files = [gzFile];
            } else {
                //console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile());
            }
            that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
        });
    } else {
        // Client doesn't want gzip or we're sending multiple files
        that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
    }
}

Server.prototype.parseByteRange = function(req, stat) {
    var byteRange = {
      from: 0,
      to: 0,
      valid: false
    }

    var rangeHeader = req.headers['range'];
    var flavor = 'bytes=';

    if (rangeHeader) {
        if (rangeHeader.indexOf(flavor) == 0 && rangeHeader.indexOf(',') == -1) {
            /* Parse */
            rangeHeader = rangeHeader.substr(flavor.length).split('-');
            byteRange.from = parseInt(rangeHeader[0]);
            byteRange.to = parseInt(rangeHeader[1]);

            /* Replace empty fields of differential requests by absolute values */
            if (isNaN(byteRange.from) && !isNaN(byteRange.to)) {
                byteRange.from = stat.size - byteRange.to;
                byteRange.to = stat.size ? stat.size - 1 : 0;
            } else if (!isNaN(byteRange.from) && isNaN(byteRange.to)) {
                byteRange.to = stat.size ? stat.size - 1 : 0;
            }

            /* General byte range validation */
            if (!isNaN(byteRange.from) && !!byteRange.to && 0 <= byteRange.from < byteRange.to) {
                byteRange.valid = true;
            } else {
                console.warn("Request contains invalid range header: ", rangeHeader);
            }
        } else {
            console.warn("Request contains unsupported range header: ", rangeHeader);
        }
    }
    return byteRange;
}

Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) {
    var mtime           = Date.parse(stat.mtime),
        key             = pathname || files[0],
        headers         = {},
        clientETag      = req.headers['if-none-match'],
        clientMTime     = Date.parse(req.headers['if-modified-since']),
        startByte       = 0,
        length          = stat.size,
        byteRange       = this.parseByteRange(req, stat);

    /* Handle byte ranges */
    if (files.length == 1 && byteRange.valid) {
        if (byteRange.to < length) {

            // Note: HTTP Range param is inclusive
            startByte = byteRange.from;
            length = byteRange.to - byteRange.from + 1;
            status = 206;

            // Set Content-Range response header (we advertise initial resource size on server here (stat.size))
            headers['Content-Range'] = 'bytes ' + byteRange.from + '-' + byteRange.to + '/' + stat.size;

        } else {
            byteRange.valid = false;
            console.warn("Range request exceeds file '" + key + "' boundaries, goes until byte no", byteRange.to, "against file size of", length, "bytes");
        }
    }

    /* In any case, check for unhandled byte range headers */
    if (!byteRange.valid && req.headers['range']) {
        console.error(new Error("Range request present but invalid, might serve whole file instead"));
    }

    // Copy default headers
    for (var k in this.options.headers) {  headers[k] = this.options.headers[k] }
    // Copy custom headers
    for (var k in _headers) { headers[k] = _headers[k] }

    headers['Etag']          = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
    headers['Date']          = new(Date)().toUTCString();
    headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
    headers['Content-Type']   = contentType;
    headers['Content-Length'] = length;

    for (var k in _headers) { headers[k] = _headers[k] }

    // Conditional GET
    // If the "If-Modified-Since" or "If-None-Match" headers
    // match the conditions, send a 304 Not Modified.
    if ((clientMTime  || clientETag) &&
        (!clientETag  || clientETag === headers['Etag']) &&
        (!clientMTime || clientMTime >= mtime)) {
        // 304 response should not contain entity headers
        ['Content-Encoding',
         'Content-Language',
         'Content-Length',
         'Content-Location',
         'Content-MD5',
         'Content-Range',
         'Content-Type',
         'Expires',
         'Last-Modified'].forEach(function(entityHeader) {
            delete headers[entityHeader];
        });
        finish(304, headers);
    } else {

        res.writeHead(status, headers);

        this.stream(key, files, new(buffer.Buffer)(length), startByte, res, function (e, buffer) {
            if (e) { return finish(500, {}) }
            finish(status, headers);
        });
    }
};

Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
    var contentType = _headers['Content-Type'] ||
                      mime.getType(files[0]) + (this.options.charset ? '; charset=' + this.options.charset : '') ||
                      'application/octet-stream';

    if(this.options.gzip) {
        this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
    } else {
        this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
    }
}

Server.prototype.stream = function (pathname, files, buffer, startByte, res, callback) {

    (function streamFile(files, offset) {
        var file = files.shift();

        if (file) {
            file = path.resolve(file) === path.normalize(file)  ? file : path.join(pathname || '.', file);

            // Stream the file to the client
            fs.createReadStream(file, {
                flags: 'r',
                mode: 0666,
                start: startByte,
                end: startByte + (buffer.length ? buffer.length - 1 : 0)
            }).on('data', function (chunk) {
                // Bounds check the incoming chunk and offset, as copying
                // a buffer from an invalid offset will throw an error and crash
                if (chunk.length && offset < buffer.length && offset >= 0) {
                    chunk.copy(buffer, offset);
                    offset += chunk.length;
                }
            }).on('close', function () {
                streamFile(files, offset);
            }).on('error', function (err) {
                callback(err);
                console.error(err);
            }).pipe(res, { end: false });
        } else {
            res.end();
            callback(null, buffer, offset);
        }
    })(files.slice(0), 0);
};

// Exports
exports.Server       = Server;
exports.version      = version;
exports.mime         = mime;

<|endoftext|>


<commit_before>"use babel";
import {CompositeDisposable} from "atom";
import http from "http";
import querystring from "querystring";

export default {
  config: {
    port: {
      type: "integer",
      default: 8090,
      minimum: 1,
    },
  },

  subscriptions: null,
  packageName: "notify-atom",

  activate (state) {
    this.subscriptions = new CompositeDisposable();

    const configPort = "notify-atom.port";

    const startServer = (wasRestart) => {
      if (this.server) {
        this.server.close();
      }
      this.port = atom.config.get(configPort);
      this.startListening(this.port, wasRestart);
    };

    startServer();

    atom.config.onDidChange(configPort, ({newValue, oldValue}) => {
      if (this.restartTimeout) {
        window.clearTimeout(this.restartTimeout);
      }

      this.restartTimeout = window.setTimeout(() => {
        this.restartTimeout = null;
        startServer(true);
      }, 1000);
    });
  },

  startListening (port, wasRestart) {
    this.server = http.createServer((request, response) => {
      if (request.method.toUpperCase() === "POST") {
        let body = "";

        request.on("data", (data) => {
          body += data;
          if (body.length > 1e6) {
            request.connection.destroy();
          }
        });

        request.on("end", () => {
          let requestParams = {};
          try {
            requestParams = JSON.parse(body);
          } catch (e) {
            requestParams = querystring.parse(body);
          }

          this.handleRequest(requestParams);
        });
      }
      response.writeHead(200);
      response.end();
    });

    this.server.on("error", (err) => {
      if (err.code.indexOf("EADDRINUSE") !== -1) {
        atom.notifications.addWarning(`Failed to start ${this.packageName}`, {
          description: `Unable to bind to port (${this.port}). This port is already in use by another service. Please change your port number in the package settings.`,  // eslint-disable-line
        });
      } else {
        atom.notifications.addError(`Failed to start ${this.packageName}`, {
          description: `Unable to bind to port (${this.port}) for an unknown reason. Please report this to the package maintainer:<br/><br/>${err}` // eslint-disable-line
        });
      }
    });

    this.server.listen(port, () => {
      if (wasRestart) {
        atom.notifications.addSuccess(`${this.packageName} restarted`, {
          description: `Now listening on port ${this.port}`,
        });
      }
    });
  },

  handleRequest (r) {
    const handlers = {
      success: atom.notifications.addSuccess.bind(atom.notifications),
      info: atom.notifications.addInfo.bind(atom.notifications),
      warning: atom.notifications.addWarning.bind(atom.notifications),
      error: atom.notifications.addError.bind(atom.notifications),
      fatalerror: atom.notifications.addFatalError.bind(atom.notifications),
    };

    if (!r.type || !handlers[r.type] || !r.message) {
      return;
    }

    handlers[r.type](r.message);
  },

  deactivate () {
    this.subscriptions.dispose();
  },

  serialize () {
    return {
    };
  },
};
<commit_msg>Add comment about timeout<commit_after>"use babel";
import {CompositeDisposable} from "atom";
import http from "http";
import querystring from "querystring";

export default {
  config: {
    port: {
      type: "integer",
      default: 8090,
      minimum: 1,
    },
  },

  subscriptions: null,
  packageName: "notify-atom",

  activate (state) {
    this.subscriptions = new CompositeDisposable();

    const configPort = "notify-atom.port";

    const startServer = (wasRestart) => {
      if (this.server) {
        this.server.close();
      }
      this.port = atom.config.get(configPort);
      this.startListening(this.port, wasRestart);
    };

    startServer();

    atom.config.onDidChange(configPort, ({newValue, oldValue}) => {
      // Use a timeout here to avoid spamming the user with restart-messages
      // for every single keypress they make in the config field
      if (this.restartTimeout) {
        window.clearTimeout(this.restartTimeout);
      }

      this.restartTimeout = window.setTimeout(() => {
        this.restartTimeout = null;
        startServer(true);
      }, 1000);
    });
  },

  startListening (port, wasRestart) {
    this.server = http.createServer((request, response) => {
      if (request.method.toUpperCase() === "POST") {
        let body = "";

        request.on("data", (data) => {
          body += data;
          if (body.length > 1e6) {
            request.connection.destroy();
          }
        });

        request.on("end", () => {
          let requestParams = {};
          try {
            requestParams = JSON.parse(body);
          } catch (e) {
            requestParams = querystring.parse(body);
          }

          this.handleRequest(requestParams);
        });
      }
      response.writeHead(200);
      response.end();
    });

    this.server.on("error", (err) => {
      if (err.code.indexOf("EADDRINUSE") !== -1) {
        atom.notifications.addWarning(`Failed to start ${this.packageName}`, {
          description: `Unable to bind to port (${this.port}). This port is already in use by another service. Please change your port number in the package settings.`,  // eslint-disable-line
        });
      } else {
        atom.notifications.addError(`Failed to start ${this.packageName}`, {
          description: `Unable to bind to port (${this.port}) for an unknown reason. Please report this to the package maintainer:<br/><br/>${err}` // eslint-disable-line
        });
      }
    });

    this.server.listen(port, () => {
      if (wasRestart) {
        atom.notifications.addSuccess(`${this.packageName} restarted`, {
          description: `Now listening on port ${this.port}`,
        });
      }
    });
  },

  handleRequest (r) {
    const handlers = {
      success: atom.notifications.addSuccess.bind(atom.notifications),
      info: atom.notifications.addInfo.bind(atom.notifications),
      warning: atom.notifications.addWarning.bind(atom.notifications),
      error: atom.notifications.addError.bind(atom.notifications),
      fatalerror: atom.notifications.addFatalError.bind(atom.notifications),
    };

    if (!r.type || !handlers[r.type] || !r.message) {
      return;
    }

    handlers[r.type](r.message);
  },

  deactivate () {
    this.subscriptions.dispose();
  },

  serialize () {
    return {
    };
  },
};
<|endoftext|>


<commit_before>module.exports = function () {
    var handler = new Handler;

    return function (name, res) {
        if (handler[name]) {
            handler[name](res);
        }
        else {
            console.error('\r\nUnknown event: ' + name);
        }
    };
};

function stringify (obj) {
    return JSON.stringify(obj);
}

function Handler () {
    this.counts = {};
    this.fails = {};
}

Handler.prototype.testBegin = function () {};
Handler.prototype.testEnd = function () {};

Handler.prototype.log = function (res) {
    this.fails[res.browser].push([ 'log', res ]);
};

Handler.prototype.visit = function (key) {
    this.fails[key] = [];
    this.write('\r' + key + '    ');
};

Handler.prototype.launched = function (key) {
    this.fails[key] = [];
    this.writeBrowser(key);
};

Handler.prototype.assert = function (res) {
    var key = res.browser;
    var id = res.testId || 'init';

    var counts = this.counts;
    var fails = this.fails;

    if (!counts[key]) counts[key] = { inits : { pass : 0, fail : 0 } };
    if (!counts[key][id]) counts[key][id] = { pass : 0, fail : 0 };
    var count = counts[key][id];

    if (res.ok) count.pass ++;
    else {
        count.fail ++;
        fails[key].push([ 'assert', res ]);
    }
    if (count.plan) this.writeBrowser(key);
};

Handler.prototype.plan = function (res) {
    var key = res.browser;
    var id = res.testId || 'init';
    var counts = this.counts;

    if (key) {
        if (!counts[key]) counts[key] = { inits : { pass : 0, fail : 0 } };
        if (!counts[key][id]) counts[key][id] = { pass : 0, fail : 0 };
        var count = counts[key][id];

        if (res.n) count.plan = res.n;
    }
};

Handler.prototype.end = function (res) {
    var self = this;

    var key = res.browser;
    var id = res.testId || 'init';

    this.writeBrowser(key);
    this.write('\r\n');

    this.fails[key].forEach(function (xs) {
        function str (x) {
            return JSON.stringify(x);
        }

        var name = xs[0];
        if (name === 'log') {
            self.write('  Log: ' + xs[1].message);
        }
        else if (name === 'assert') {
            var fail = xs[1];
            self.write('  ' + fail.type + ' error: ');
            if (fail.type === 'equal') {
                self.write(str(fail.wanted) + ' == ' + str(fail.found));
            }
            else if (fail.type === 'notEqual') {
                self.write(str(fail.wanted) + ' != ' + str(fail.found));
            }
            else if (fail.type === 'strictEqual') {
                self.write(str(fail.wanted) + ' === ' + str(fail.found));
            }
            else if (fail.type === 'strictNotEqual') {
                self.write(str(fail.wanted) + ' !== ' + str(fail.found));
            }
            else if (fail.type === 'ok') {
                self.write('ok(' + str(fail.found) + ')');
            }
            else if (fail.type === 'notOk') {
                self.write('notOk(' + str(fail.found) + ')');
            }
            else {
                self.write('  wanted: ' + str(fail.wanted) + ', '
                    + 'found: ' + str(fail.found) + '\r\n');
            }

            self.write('\r\n');
            if (fail.stack) {
                self.write(fail.stack + '\r\n');
            }
        }
    });
};

Handler.prototype.finished = function () {
    var self = this;
    var total = { pass : 0, fail : 0 };
    var counts = this.counts;

    Object.keys(counts).forEach(function (key) {
        Object.keys(counts[key]).forEach(function (id) {
            total.pass += counts[key][id].pass;
            total.fail += counts[key][id].fail;
        });
    });

    counts.total = { total : total };

    this.write('\r\n');
    this.writeBrowser('total');

    process.nextTick(function () {
        self.write('\r\n');
    });
};

Handler.prototype.error = function (res) {
    if (res.browser && this.fails[res.browser]) {
        if (counts[res.browser]) {
            counts[res.browser][res.testId || 'init'].fail ++;
        }
        this.fails[res.browser].push([ 'error', res ]);
    }
    else {
        this.write([
            '',
            'Unexpected error: ' + JSON.stringify(res),
            '',
            ''
        ].join('\r\n'));
    }
};

Handler.prototype.write = function (msg) {
    process.stdout.write(msg);
};

Handler.prototype.writeBrowser = function (key) {
    var counts = this.counts;
    var fails = this.fails;

    if (!counts[key]) counts[key] = { init : { pass : 0, fail : 0 } };

    var count = Object.keys(counts[key]).reduce(function (acc, id) {
        acc.pass += counts[key][id].pass;
        acc.fail += counts[key][id].fail;
        return acc;
    }, { pass : 0, fail : 0 });

    var percent = count.pass + count.fail <= 0
        ? '0'
        : Math.floor(100 * count.pass / (count.pass + count.fail))
    ;

    function padRight (n, s) {
        s = s.toString();
        return s + Array(Math.max(0, n + 1 - s.length)).join(' ');
    }

    function padLeft (n, s) {
        s = s.toString();
        return Array(Math.max(0, n - s.length) + 1).join(' ') + s;
    }

    this.write('\r'
        + padRight(24, key)
        + '  '
        + padLeft(9, count.pass + '/' + (count.pass + count.fail))
        + '  '
        + padLeft(3, percent) + ' % ok'
    );
};
<commit_msg>show source snippets from the failing assertions<commit_after>var fs = require('fs');

module.exports = function () {
    var handler = new Handler;

    return function (name, res) {
        if (handler[name]) {
            handler[name](res);
        }
        else {
            console.error('\r\nUnknown event: ' + name);
        }
    };
};

function stringify (obj) {
    return JSON.stringify(obj);
}

function Handler () {
    this.counts = {};
    this.fails = {};
}

Handler.prototype.testBegin = function () {};
Handler.prototype.testEnd = function () {};

Handler.prototype.log = function (res) {
    this.fails[res.browser].push([ 'log', res ]);
};

Handler.prototype.visit = function (key) {
    this.fails[key] = [];
    this.write('\r' + key + '    ');
};

Handler.prototype.launched = function (key) {
    this.fails[key] = [];
    this.writeBrowser(key);
};

Handler.prototype.assert = function (res) {
    var key = res.browser;
    var id = res.testId || 'init';

    var counts = this.counts;
    var fails = this.fails;

    if (!counts[key]) counts[key] = { inits : { pass : 0, fail : 0 } };
    if (!counts[key][id]) counts[key][id] = { pass : 0, fail : 0 };
    var count = counts[key][id];

    if (res.ok) count.pass ++;
    else {
        count.fail ++;
        fails[key].push([ 'assert', res ]);
    }
    if (count.plan) this.writeBrowser(key);
};

Handler.prototype.plan = function (res) {
    var key = res.browser;
    var id = res.testId || 'init';
    var counts = this.counts;

    if (key) {
        if (!counts[key]) counts[key] = { inits : { pass : 0, fail : 0 } };
        if (!counts[key][id]) counts[key][id] = { pass : 0, fail : 0 };
        var count = counts[key][id];

        if (res.n) count.plan = res.n;
    }
};

Handler.prototype.end = function (res) {
    var self = this;

    var key = res.browser;
    var id = res.testId || 'init';

    this.writeBrowser(key);
    this.write('\r\n');

    this.fails[key].forEach(function (xs) {
        function str (x) {
            return JSON.stringify(x);
        }

        var name = xs[0];
        if (name === 'log') {
            self.write('  Log: ' + xs[1].message);
        }
        else if (name === 'assert') {
            var fail = xs[1];
            self.write('  Error in ' + fail.type + '(): ');
            if (fail.type === 'fail') {
                self.write(
                    typeof fail.found === 'string'
                        ? fail.found : str(fail.found)
                );
            }
            else if (fail.type === 'equal') {
                self.write(str(fail.wanted) + ' == ' + str(fail.found));
            }
            else if (fail.type === 'notEqual') {
                self.write(str(fail.wanted) + ' != ' + str(fail.found));
            }
            else if (fail.type === 'strictEqual') {
                self.write(str(fail.wanted) + ' === ' + str(fail.found));
            }
            else if (fail.type === 'strictNotEqual') {
                self.write(str(fail.wanted) + ' !== ' + str(fail.found));
            }
            else if (fail.type === 'ok') {
                self.write('ok(' + str(fail.found) + ')');
            }
            else if (fail.type === 'notOk') {
                self.write('notOk(' + str(fail.found) + ')');
            }
            else {
                self.write('  wanted: ' + str(fail.wanted) + ', '
                    + 'found: ' + str(fail.found) + '\r\n');
            }

            self.write('\r\n');
            if (fail.stack) {
                self.write(fail.stack + '\r\n');
                var m = fail.stack.match(/^\s*at (\/[^:]+):(\d+):(\d+)/);
                if (m) {
                    var line = m[2], col = m[3];
                    var lines = fs.readFileSync(m[1], 'utf8').split('\n');
                    var s = lines[line - 1].trim();
                    self.write('\r\n  > ' + s + '\r\n');
                }
            }
        }
    });
};

Handler.prototype.finished = function () {
    var self = this;
    var total = { pass : 0, fail : 0 };
    var counts = this.counts;

    Object.keys(counts).forEach(function (key) {
        Object.keys(counts[key]).forEach(function (id) {
            total.pass += counts[key][id].pass;
            total.fail += counts[key][id].fail;
        });
    });

    counts.total = { total : total };

    this.write('\r\n');
    this.writeBrowser('total');

    process.nextTick(function () {
        self.write('\r\n');
    });
};

Handler.prototype.error = function (res) {
    if (res.browser && this.fails[res.browser]) {
        if (counts[res.browser]) {
            counts[res.browser][res.testId || 'init'].fail ++;
        }
        this.fails[res.browser].push([ 'error', res ]);
    }
    else {
        this.write([
            '',
            'Unexpected error: ' + JSON.stringify(res),
            '',
            ''
        ].join('\r\n'));
    }
};

Handler.prototype.write = function (msg) {
    process.stdout.write(msg);
};

Handler.prototype.writeBrowser = function (key) {
    var counts = this.counts;
    var fails = this.fails;

    if (!counts[key]) counts[key] = { init : { pass : 0, fail : 0 } };

    var count = Object.keys(counts[key]).reduce(function (acc, id) {
        acc.pass += counts[key][id].pass;
        acc.fail += counts[key][id].fail;
        return acc;
    }, { pass : 0, fail : 0 });

    var percent = count.pass + count.fail <= 0
        ? '0'
        : Math.floor(100 * count.pass / (count.pass + count.fail))
    ;

    function padRight (n, s) {
        s = s.toString();
        return s + Array(Math.max(0, n + 1 - s.length)).join(' ');
    }

    function padLeft (n, s) {
        s = s.toString();
        return Array(Math.max(0, n - s.length) + 1).join(' ') + s;
    }

    this.write('\r'
        + padRight(24, key)
        + '  '
        + padLeft(9, count.pass + '/' + (count.pass + count.fail))
        + '  '
        + padLeft(3, percent) + ' % ok'
    );
};
<|endoftext|>


<commit_before>const cheerio = require('cheerio')
const urls = require('../urls')
const util = require('./util')

const getAttr = util.getAttr
const getText = util.getText
const getData = util.getData
const toNum = util.toNum

module.exports = {
    parseUserDetail,
    parseUserProfile,
    parseUserFollows
}

function parseUserDetail(html) {
    var $ = cheerio.load(html)
    var profileMainEle = $('.zm-profile-header-main')
    var profileInfoEle = $('.zm-profile-header-info')
    var profileOpeEle = $('.zm-profile-header-operation')
    var profileNavEle = $('.profile-navbar')
    var followEle = $('.zm-profile-side-following')

    var nameEle = $('.name', profileMainEle)
    var bioEle = $('.bio', profileMainEle)
    var weiboEle = $('.zm-profile-header-user-weibo', profileMainEle)
    var avatarEle = $('.Avatar', profileMainEle)

    var locationEle = $('.item.location', profileInfoEle)
    var businessEle = $('.item.business', profileInfoEle)
    var genderEle = $('.item.gender .icon', profileInfoEle)
    var companyEle = $('.item.employment', profileInfoEle)
    var positionEle = $('.item.position', profileInfoEle)
    var schoolEle = $('.item.education', profileInfoEle)
    var majorEle = $('.item.education-extra', profileInfoEle)
    var descriptionEle = $('.description.unfold-item > .content', profileInfoEle)

    var agreesEle = $('.zm-profile-header-user-agree > strong', profileOpeEle)
    var thanksEle = $('.zm-profile-header-user-thanks > strong', profileOpeEle)
    var hashEle = $('.zm-rich-follow-btn', profileOpeEle)

    var navEles = $('.item', profileNavEle)
    var asksEle = $('.num', navEles[1])
    var answersEle = $('.num', navEles[2])
    var postsEle = $('.num', navEles[3])
    var collectionsEle = $('.num', navEles[4])
    var logsEle = $('.num', navEles[5])

    var followeeEle = $('.item:first-child > strong', followEle)
    var followerEle = $('.item:last-child > strong', followEle)

    var link = getAttr(nameEle, 'href')

    var profile = {
        name: getText(nameEle),
        uname: link.substring('/people/'.length),
        link: urls.full(link),
        biology: getAttr(bioEle, 'title'),
        weibo: getAttr(weiboEle, 'href'),
        avatar: getAttr(avatarEle, 'src'),
        location: getAttr(locationEle, 'title'),
        business: getAttr(businessEle, 'title'),
        gender: getAttr(genderEle, 'class').substring('icon icon-profile-'.length),
        company: getAttr(companyEle, 'title'),
        position: getAttr(positionEle, 'title'),
        school: getAttr(schoolEle, 'title'),
        major: getAttr(majorEle, 'title'),
        description: getText(descriptionEle).trim(),
        hash: getData(hashEle, 'id'),
        agrees: toNum(getText(agreesEle)),
        thanks: toNum(getText(thanksEle)),
        asks: toNum(getText(asksEle)),
        answers: toNum(getText(answersEle)),
        posts: toNum(getText(postsEle)),
        collections: toNum(getText(collectionsEle)),
        logs: toNum(getText(logsEle)),
        followees: toNum(getText(followeeEle)),
        followers: toNum(getText(followerEle)),
        crawltime: Date.now()
    }

    return profile
}

function parseUserProfile(html) {
    var $ = cheerio.load(html)
    var upperEle = $('.upper')
    var lowerEle = $('.lower')

    var linkEle = $('.avatar-link', upperEle)
    var avatarEle = $('.Avatar', upperEle)
    var nameEle = $('.name', upperEle)
    var genderEle = $('i.icon', upperEle)
    var bioEle = $('.tagline', upperEle)

    var metaEles = $('.meta .item', lowerEle)
    var answersEle = $('.value', metaEles[0])
    var postsEle = $('.value', metaEles[1])
    var followerEle = $('.value', metaEles[2])
    var hashEle = $('.zm-rich-follow-btn', lowerEle)

    var link = getAttr(linkEle, 'href')

    var profile = {
        name: getText(nameEle),
        uname: link.substring('/people/'.length),
        link: urls.full(link),
        avatar: getAttr(avatarEle, 'src'),
        gender: getAttr(genderEle, 'class').substring('icon icon-profile-'.length),
        biology: getText(bioEle),
        answers: getText(answersEle),
        posts: getText(postsEle),
        followers: getText(followerEle),
        hash: getData(hashEle, 'id'),
        crawltime: Date.now()
    }

    return profile
}

function parseUserFollows(htmls) {
    var follows = htmls.map(html => {
        var $ = cheerio.load(html)
        var hashEle = $('.zm-rich-follow-btn')
        var linkEle = $('.zm-item-link-avatar')

        var link = getAttr(linkEle, 'href')

        var profile = {
            name: getAttr(linkEle, 'title'),
            uname: link.substring('/people/'.length),
            link: urls.full(link),
            hash: getData(hashEle, 'id')
        }

        return profile
    })

    return follows
}
<commit_msg>fix bug when parse gender of user profile<commit_after>const cheerio = require('cheerio')
const urls = require('../urls')
const util = require('./util')

const getAttr = util.getAttr
const getText = util.getText
const getData = util.getData
const toNum = util.toNum

module.exports = {
    parseUserDetail,
    parseUserProfile,
    parseUserFollows
}

function parseUserDetail(html) {
    var $ = cheerio.load(html)
    var profileMainEle = $('.zm-profile-header-main')
    var profileInfoEle = $('.zm-profile-header-info')
    var profileOpeEle = $('.zm-profile-header-operation')
    var profileNavEle = $('.profile-navbar')
    var followEle = $('.zm-profile-side-following')

    var nameEle = $('.name', profileMainEle)
    var bioEle = $('.bio', profileMainEle)
    var weiboEle = $('.zm-profile-header-user-weibo', profileMainEle)
    var avatarEle = $('.Avatar', profileMainEle)

    var locationEle = $('.item.location', profileInfoEle)
    var businessEle = $('.item.business', profileInfoEle)
    var genderEle = $('.item.gender .icon', profileInfoEle)
    var companyEle = $('.item.employment', profileInfoEle)
    var positionEle = $('.item.position', profileInfoEle)
    var schoolEle = $('.item.education', profileInfoEle)
    var majorEle = $('.item.education-extra', profileInfoEle)
    var descriptionEle = $('.description.unfold-item > .content', profileInfoEle)

    var agreesEle = $('.zm-profile-header-user-agree > strong', profileOpeEle)
    var thanksEle = $('.zm-profile-header-user-thanks > strong', profileOpeEle)
    var hashEle = $('.zm-rich-follow-btn', profileOpeEle)

    var navEles = $('.item', profileNavEle)
    var asksEle = $('.num', navEles[1])
    var answersEle = $('.num', navEles[2])
    var postsEle = $('.num', navEles[3])
    var collectionsEle = $('.num', navEles[4])
    var logsEle = $('.num', navEles[5])

    var followeeEle = $('.item:first-child > strong', followEle)
    var followerEle = $('.item:last-child > strong', followEle)

    var link = getAttr(nameEle, 'href')

    var profile = {
        name: getText(nameEle),
        uname: link.substring('/people/'.length),
        link: urls.full(link),
        biology: getAttr(bioEle, 'title'),
        weibo: getAttr(weiboEle, 'href'),
        avatar: getAttr(avatarEle, 'src'),
        location: getAttr(locationEle, 'title'),
        business: getAttr(businessEle, 'title'),
        gender: getAttr(genderEle, 'class').substring('icon icon-profile-'.length),
        company: getAttr(companyEle, 'title'),
        position: getAttr(positionEle, 'title'),
        school: getAttr(schoolEle, 'title'),
        major: getAttr(majorEle, 'title'),
        description: getText(descriptionEle).trim(),
        hash: getData(hashEle, 'id'),
        agrees: toNum(getText(agreesEle)),
        thanks: toNum(getText(thanksEle)),
        asks: toNum(getText(asksEle)),
        answers: toNum(getText(answersEle)),
        posts: toNum(getText(postsEle)),
        collections: toNum(getText(collectionsEle)),
        logs: toNum(getText(logsEle)),
        followees: toNum(getText(followeeEle)),
        followers: toNum(getText(followerEle)),
        crawltime: Date.now()
    }

    return profile
}

function parseUserProfile(html) {
    var $ = cheerio.load(html)
    var upperEle = $('.upper')
    var lowerEle = $('.lower')

    var linkEle = $('.avatar-link', upperEle)
    var avatarEle = $('.Avatar', upperEle)
    var nameEle = $('.name', upperEle)
    var genderEle = $('i.icon-profile-male', upperEle) || $('i.icon-profile-female', upperEle)
    var bioEle = $('.tagline', upperEle)

    var metaEles = $('.meta .item', lowerEle)
    var answersEle = $('.value', metaEles[0])
    var postsEle = $('.value', metaEles[1])
    var followerEle = $('.value', metaEles[2])
    var hashEle = $('.zm-rich-follow-btn', lowerEle)

    var link = getAttr(linkEle, 'href')

    var profile = {
        name: getText(nameEle),
        uname: link.substring('/people/'.length),
        link: urls.full(link),
        avatar: getAttr(avatarEle, 'src'),
        gender: getAttr(genderEle, 'class').substring('icon icon-profile-'.length),
        biology: getText(bioEle),
        answers: getText(answersEle),
        posts: getText(postsEle),
        followers: getText(followerEle),
        hash: getData(hashEle, 'id'),
        crawltime: Date.now()
    }

    return profile
}

function parseUserFollows(htmls) {
    var follows = htmls.map(html => {
        var $ = cheerio.load(html)
        var hashEle = $('.zm-rich-follow-btn')
        var linkEle = $('.zm-item-link-avatar')

        var link = getAttr(linkEle, 'href')

        var profile = {
            name: getAttr(linkEle, 'title'),
            uname: link.substring('/people/'.length),
            link: urls.full(link),
            hash: getData(hashEle, 'id')
        }

        return profile
    })

    return follows
}
<|endoftext|>


<commit_before>"use babel"
import { EventEmitter } from "events"
import minimatch from "minimatch"
import { Directory, File } from "atom"
import { union } from "./utils"
import { globifyPath, globifyDirectory, globifyGitIgnoreFile } from "./path-utils"
import glob from "fast-glob"

export default class PathsCache extends EventEmitter {
  constructor() {
    super()

    const rebuildCacheBound = this.rebuildCache.bind(this)
    this._projectChangeWatcher = atom.project.onDidChangePaths(rebuildCacheBound)

    const _onDidChangeFilesBound = this._onDidChangeFiles.bind(this)
    this._projectWatcher = atom.project.onDidChangeFiles(_onDidChangeFilesBound)

    this._repositories = []
    this._filePathsByProjectDirectory = new Map()
    this._filePathsByDirectory = new Map()
    this._fileWatchersByDirectory = new Map()

    this.updateConfig()
  }

  updateConfig() {
    this.config = {
      excludeVcsIgnoredPaths: atom.config.get("core.excludeVcsIgnoredPaths"),
      ignoreSubmodules: atom.config.get("autocomplete-paths.ignoreSubmodules"),
      shouldIgnoredNames: atom.config.get("autocomplete-paths.ignoredNames"),
      ignoredNames: atom.config.get("core.ignoredNames"),
      ignoredPatterns: atom.config.get("autocomplete-paths.ignoredPatterns"),
      maxFileCount: atom.config.get("autocomplete-paths.maxFileCount"),
    }
  }

  /**
   * Checks if the given path is ignored
   * @param  {String}  path
   * @return {Boolean}
   * @private
   */
  _isPathIgnored(path) {
    let ignored = false
    if (this.config.excludeVcsIgnoredPaths) {
      this._repositories.forEach((repository) => {
        if (ignored) return
        const isIgnoredSubmodule = this.config.ignoreSubmodules && repository.isSubmodule(path)
        if (repository.isPathIgnored(path) || isIgnoredSubmodule) {
          ignored = true
        }
      })
    }

    if (this.config.shouldIgnoredNames) {
      this.config.ignoredNames.forEach((ignoredName) => {
        if (ignored) return
        ignored = ignored || minimatch(path, ignoredName, { matchBase: true, dot: true })
      })
    }

    if (this.config.ignoredPatterns) {
      this.config.ignoredPatterns.forEach((ignoredPattern) => {
        if (ignored) return
        ignored = ignored || minimatch(path, ignoredPattern, { dot: true })
      })
    }

    return ignored
  }

  /**
   * Caches the project paths and repositories
   * @return {Promise}
   * @private
   */
  async _cacheProjectPathsAndRepositories() {
    this._projectDirectories = atom.project.getDirectories()

    // get the repositories asynchronously
    const projectNum = this._projectDirectories.length
    let repositoriesP = new Array(projectNum)
    for (let i = 0; i < projectNum; i++) {
      repositoriesP[i] = atom.project.repositoryForDirectory(this._projectDirectories[i])
    }
    const repositories = await Promise.all(repositoriesP)
    this._repositories = repositories.filter((r) => r !== null) // filter out non-repository directories
  }

  /**
   * Invoked when the content of the given `directory` has changed
   * @param  {Directory} projectDirectory
   * @param  {Directory} directory
   * @private
   */
  _onDirectoryChanged(projectDirectory, directory) {
    this._removeFilePathsForDirectory(projectDirectory, directory)
    this._cleanWatchersForDirectory(directory)
    this._cacheDirectoryFilePaths(projectDirectory, directory)
  }

  /**
   * Removes all watchers inside the given directory
   * @param  {Directory} directory
   * @private
   */
  _cleanWatchersForDirectory(directory) {
    this._fileWatchersByDirectory.forEach((watcher, otherDirectory) => {
      if (directory.contains(otherDirectory.path)) {
        watcher.dispose()
        this._fileWatchersByDirectory.delete(otherDirectory)
      }
    })
  }

  /**
   * Removes all cached file paths in the given directory
   * @param  {Directory} projectDirectory
   * @param  {Directory} directory
   * @private
   */
  _removeFilePathsForDirectory(projectDirectory, directory) {
    let filePaths = this._filePathsByProjectDirectory.get(projectDirectory.path)
    if (!filePaths) return

    filePaths = filePaths.filter((path) => !directory.contains(path))
    this._filePathsByProjectDirectory.set(projectDirectory.path, filePaths)

    this._filePathsByDirectory.delete(directory.path)
  }

  /**
   * Caches file paths for the given directory
   * @param  {Directory} projectDirectory
   * @param  {Directory} directory
   * @return {Promise}
   * @private
   */
  async _cacheDirectoryFilePaths(projectDirectory, directory) {
    if (this._cancelled) return []

    if (process.platform !== "win32") {
      let watcher = this._fileWatchersByDirectory.get(directory)
      if (!watcher) {
        watcher = directory.onDidChange(() => this._onDirectoryChanged(projectDirectory, directory))
        this._fileWatchersByDirectory.set(directory, watcher)
      }
    }
    const entries = await this._getDirectoryEntries(directory)
    if (this._cancelled) return []

    // Filter: Files and Directories that are not ignored
    let filePaths = []
    let directories = []
    for (let i = 0, len = entries.length; i < len; i++) {
      const entry = entries[i]
      if (entry instanceof File && !this._isPathIgnored(entry.path)) {
        filePaths.push(entry.path)
      } else if (entry instanceof Directory && !this._isPathIgnored(entry.path)) {
        directories.push(entry)
      }
    }

    // Merge file paths into existing array (which contains *all* file paths)
    let filePathsArray = this._filePathsByProjectDirectory.get(projectDirectory.path) || []
    const newPathsCount = filePathsArray.length + filePaths.length

    if (newPathsCount > this.config.maxFileCount && !this._cancelled) {
      atom.notifications.addError("autocomplete-paths", {
        description: `Maximum file count of ${this.config.maxFileCount} has been exceeded. Path autocompletion will not work in this project.

Click here to learn more.`,
        dismissable: true,
      })

      this._filePathsByProjectDirectory.clear()
      this._filePathsByDirectory.clear()
      this._cancelled = true
      this.emit("rebuild-cache-done")
      return
    }

    this._filePathsByProjectDirectory.set(projectDirectory.path, union(filePathsArray, filePaths))

    // Merge file paths into existing array (which contains file paths for a specific directory)
    filePathsArray = this._filePathsByDirectory.get(directory.path) || []
    this._filePathsByDirectory.set(directory.path, union(filePathsArray, filePaths))

    return Promise.all(directories.map((directory) => this._cacheDirectoryFilePaths(projectDirectory, directory)))
  }

  /**
   * Promisified version of Directory#getEntries
   * @param  {Directory} directory
   * @return {Promise}
   * @private
   */
  _getDirectoryEntries(directory) {
    return new Promise((resolve, reject) => {
      directory.getEntries((err, entries) => {
        if (err) return reject(err)
        resolve(entries)
      })
    })
  }

  /**
   * Rebuilds the paths cache
   */
  rebuildCache() {
    this.dispose()

    this._cancelled = false
    this.emit("rebuild-cache")

    return this._buildInitialCacheWithGlob().catch((e) => {
      console.error(e)
      return this._buildInitialCacheWithAtom()
    })
  }

  /**
   * Builds the initial file cache using Atom
   * @return {Promise}
   * @private
   */
  async _buildInitialCacheWithAtom() {
    await this._cacheProjectPathsAndRepositories()
    const result = await Promise.all(
      this._projectDirectories.map((projectDirectory) => {
        return this._cacheDirectoryFilePaths(projectDirectory, projectDirectory)
      })
    )
    this.emit("rebuild-cache-done")
    return result
  }

  /**
   * Returns the file paths for the given project directory with the given (optional) relative path
   * @param  {Directory} projectDirectory
   * @param  {String} [relativeToPath=null]
   * @return {String[]}
   */
  getFilePathsForProjectDirectory(projectDirectory, relativeToPath = null) {
    let filePaths = this._filePathsByProjectDirectory.get(projectDirectory.path) || []
    if (relativeToPath) {
      return filePaths.filter((filePath) => filePath.indexOf(relativeToPath) === 0)
    }
    return filePaths
  }

  /**
   * Disposes this PathsCache
   */
  dispose(isPackageDispose) {
    this._fileWatchersByDirectory.forEach((watcher) => {
      watcher.dispose()
    })
    this._fileWatchersByDirectory = new Map()
    this._filePathsByProjectDirectory = new Map()
    this._filePathsByDirectory = new Map()
    this._repositories = []
    if (this._projectWatcher) {
      this._projectWatcher.dispose()
      this._projectWatcher = null
    }
    if (isPackageDispose && this._projectChangeWatcher) {
      this._projectChangeWatcher.dispose()
      this._projectChangeWatcher = null
    }
  }

  //
  // Cache with `find`
  //

  /**
   * Builds the initial file cache with `find`
   * @return {Promise}
   * @private
   */
  async _buildInitialCacheWithGlob() {
    await this._cacheProjectPathsAndRepositories()
    const result = await Promise.all(this._projectDirectories.map((d) => this._populateCacheFor(d)))

    this.emit("rebuild-cache-done")
    return result
  }

  _onDidChangeFiles(events) {
    events
      .filter((event) => event.action !== "modified")
      .forEach((event) => {
        if (!this._projectDirectories) {
          return
        }

        const { action, path, oldPath } = event

        const projectDirectory = this._projectDirectories.find(
          (projectDirectory) => path.indexOf(projectDirectory.path) === 0
        )

        if (!projectDirectory) {
          return
        }
        const directoryPath = projectDirectory.path
        const ignored = this._isPathIgnored(path)

        if (ignored) {
          return
        }

        const files = this._filePathsByProjectDirectory.get(directoryPath) || []

        switch (action) {
          case "created":
            files.push(path)
            break

          case "deleted": {
            const i = files.indexOf(path)
            if (i > -1) {
              files.splice(i, 1)
            }
            break
          }
          case "renamed": {
            const j = files.indexOf(oldPath)
            if (j > -1) {
              files[j] = path
            }
            break
          }
        }

        if (!this._filePathsByProjectDirectory.has(directoryPath)) {
          this._filePathsByProjectDirectory.set(directoryPath, files)
        }
      })
  }

  /**
   * Returns a list of ignore patterns for a directory
   * @param  {String} directoryPath
   * @returns {Array<string>} an array of glob patterns
   * @private
   */
  _getIgnoredPatternsGlob(directoryPath) {
    const patterns = []

    if (this.config.shouldIgnoredNames) {
      patterns.push(...this.config.ignoredNames)
    }

    if (this.config.ignoredPatterns) {
      patterns.push(...this.config.ignoredPatterns)
    }

    const patternsNum = patterns.length

    let globEntries = new Array(patternsNum)

    for (let iEntry = 0; iEntry < patternsNum; iEntry++) {
      const globifyOutput = globifyPath(patterns[iEntry], directoryPath)

      // Check if `globifyPath` returns a pair or a string
      if (typeof globifyOutput === "string") {
        // string
        globEntries[iEntry] = globifyOutput // Place the entry in the output array
      } else {
        // pair
        globEntries[iEntry] = globifyOutput[0] // Place the entry in the output array
        globEntries.push(globifyOutput[1]) // Push the additional entry
      }
    }
    return globEntries
  }

  /**
   * Returns the glob pattern of a gitignore of a directory
   * @param  {String} directoryPath
   * @returns {Promise<Array<string>>} an array of glob patterns
   * @private
   */
  async _getGitIgnoreGlob(directoryPath) {
    if (this.config.excludeVcsIgnoredPaths) {
      try {
        return await globifyGitIgnoreFile(directoryPath)
      } catch (err) {
        // .gitignore does not exist for this directory, ignoring
      }
    }
    return []
  }

  /**
   * Populates cache for a project directory
   * @param  {Directory} projectDirectory
   * @return {Promise}
   * @private
   */
  async _populateCacheFor(projectDirectory) {
    const directoryPath = projectDirectory.path

    const directoryGlob = globifyDirectory(directoryPath)
    const gitignoreGlob = await this._getGitIgnoreGlob(directoryPath)
    const ignoredPatternsGlob = this._getIgnoredPatternsGlob(directoryPath)

    const files = await glob(
      [directoryGlob, ...gitignoreGlob, ...ignoredPatternsGlob],
      // glob options
      {
        dot: true,
        cwd: directoryPath,
        onlyFiles: true,
      }
    )
    this._filePathsByProjectDirectory.set(directoryPath, files)
    return files
  }
}
<commit_msg>chore: make PatchCache.rebuildCache async<commit_after>"use babel"
import { EventEmitter } from "events"
import minimatch from "minimatch"
import { Directory, File } from "atom"
import { union } from "./utils"
import { globifyPath, globifyDirectory, globifyGitIgnoreFile } from "./path-utils"
import glob from "fast-glob"

export default class PathsCache extends EventEmitter {
  constructor() {
    super()

    const rebuildCacheBound = this.rebuildCache.bind(this)
    this._projectChangeWatcher = atom.project.onDidChangePaths(rebuildCacheBound)

    const _onDidChangeFilesBound = this._onDidChangeFiles.bind(this)
    this._projectWatcher = atom.project.onDidChangeFiles(_onDidChangeFilesBound)

    this._repositories = []
    this._filePathsByProjectDirectory = new Map()
    this._filePathsByDirectory = new Map()
    this._fileWatchersByDirectory = new Map()

    this.updateConfig()
  }

  updateConfig() {
    this.config = {
      excludeVcsIgnoredPaths: atom.config.get("core.excludeVcsIgnoredPaths"),
      ignoreSubmodules: atom.config.get("autocomplete-paths.ignoreSubmodules"),
      shouldIgnoredNames: atom.config.get("autocomplete-paths.ignoredNames"),
      ignoredNames: atom.config.get("core.ignoredNames"),
      ignoredPatterns: atom.config.get("autocomplete-paths.ignoredPatterns"),
      maxFileCount: atom.config.get("autocomplete-paths.maxFileCount"),
    }
  }

  /**
   * Checks if the given path is ignored
   * @param  {String}  path
   * @return {Boolean}
   * @private
   */
  _isPathIgnored(path) {
    let ignored = false
    if (this.config.excludeVcsIgnoredPaths) {
      this._repositories.forEach((repository) => {
        if (ignored) return
        const isIgnoredSubmodule = this.config.ignoreSubmodules && repository.isSubmodule(path)
        if (repository.isPathIgnored(path) || isIgnoredSubmodule) {
          ignored = true
        }
      })
    }

    if (this.config.shouldIgnoredNames) {
      this.config.ignoredNames.forEach((ignoredName) => {
        if (ignored) return
        ignored = ignored || minimatch(path, ignoredName, { matchBase: true, dot: true })
      })
    }

    if (this.config.ignoredPatterns) {
      this.config.ignoredPatterns.forEach((ignoredPattern) => {
        if (ignored) return
        ignored = ignored || minimatch(path, ignoredPattern, { dot: true })
      })
    }

    return ignored
  }

  /**
   * Caches the project paths and repositories
   * @return {Promise}
   * @private
   */
  async _cacheProjectPathsAndRepositories() {
    this._projectDirectories = atom.project.getDirectories()

    // get the repositories asynchronously
    const projectNum = this._projectDirectories.length
    let repositoriesP = new Array(projectNum)
    for (let i = 0; i < projectNum; i++) {
      repositoriesP[i] = atom.project.repositoryForDirectory(this._projectDirectories[i])
    }
    const repositories = await Promise.all(repositoriesP)
    this._repositories = repositories.filter((r) => r !== null) // filter out non-repository directories
  }

  /**
   * Invoked when the content of the given `directory` has changed
   * @param  {Directory} projectDirectory
   * @param  {Directory} directory
   * @private
   */
  _onDirectoryChanged(projectDirectory, directory) {
    this._removeFilePathsForDirectory(projectDirectory, directory)
    this._cleanWatchersForDirectory(directory)
    this._cacheDirectoryFilePaths(projectDirectory, directory)
  }

  /**
   * Removes all watchers inside the given directory
   * @param  {Directory} directory
   * @private
   */
  _cleanWatchersForDirectory(directory) {
    this._fileWatchersByDirectory.forEach((watcher, otherDirectory) => {
      if (directory.contains(otherDirectory.path)) {
        watcher.dispose()
        this._fileWatchersByDirectory.delete(otherDirectory)
      }
    })
  }

  /**
   * Removes all cached file paths in the given directory
   * @param  {Directory} projectDirectory
   * @param  {Directory} directory
   * @private
   */
  _removeFilePathsForDirectory(projectDirectory, directory) {
    let filePaths = this._filePathsByProjectDirectory.get(projectDirectory.path)
    if (!filePaths) return

    filePaths = filePaths.filter((path) => !directory.contains(path))
    this._filePathsByProjectDirectory.set(projectDirectory.path, filePaths)

    this._filePathsByDirectory.delete(directory.path)
  }

  /**
   * Caches file paths for the given directory
   * @param  {Directory} projectDirectory
   * @param  {Directory} directory
   * @return {Promise}
   * @private
   */
  async _cacheDirectoryFilePaths(projectDirectory, directory) {
    if (this._cancelled) return []

    if (process.platform !== "win32") {
      let watcher = this._fileWatchersByDirectory.get(directory)
      if (!watcher) {
        watcher = directory.onDidChange(() => this._onDirectoryChanged(projectDirectory, directory))
        this._fileWatchersByDirectory.set(directory, watcher)
      }
    }
    const entries = await this._getDirectoryEntries(directory)
    if (this._cancelled) return []

    // Filter: Files and Directories that are not ignored
    let filePaths = []
    let directories = []
    for (let i = 0, len = entries.length; i < len; i++) {
      const entry = entries[i]
      if (entry instanceof File && !this._isPathIgnored(entry.path)) {
        filePaths.push(entry.path)
      } else if (entry instanceof Directory && !this._isPathIgnored(entry.path)) {
        directories.push(entry)
      }
    }

    // Merge file paths into existing array (which contains *all* file paths)
    let filePathsArray = this._filePathsByProjectDirectory.get(projectDirectory.path) || []
    const newPathsCount = filePathsArray.length + filePaths.length

    if (newPathsCount > this.config.maxFileCount && !this._cancelled) {
      atom.notifications.addError("autocomplete-paths", {
        description: `Maximum file count of ${this.config.maxFileCount} has been exceeded. Path autocompletion will not work in this project.

Click here to learn more.`,
        dismissable: true,
      })

      this._filePathsByProjectDirectory.clear()
      this._filePathsByDirectory.clear()
      this._cancelled = true
      this.emit("rebuild-cache-done")
      return
    }

    this._filePathsByProjectDirectory.set(projectDirectory.path, union(filePathsArray, filePaths))

    // Merge file paths into existing array (which contains file paths for a specific directory)
    filePathsArray = this._filePathsByDirectory.get(directory.path) || []
    this._filePathsByDirectory.set(directory.path, union(filePathsArray, filePaths))

    return Promise.all(directories.map((directory) => this._cacheDirectoryFilePaths(projectDirectory, directory)))
  }

  /**
   * Promisified version of Directory#getEntries
   * @param  {Directory} directory
   * @return {Promise}
   * @private
   */
  _getDirectoryEntries(directory) {
    return new Promise((resolve, reject) => {
      directory.getEntries((err, entries) => {
        if (err) return reject(err)
        resolve(entries)
      })
    })
  }

  /**
   * Rebuilds the paths cache
   */
  async rebuildCache() {
    this.dispose()

    this._cancelled = false
    this.emit("rebuild-cache")

    try {
      return await this._buildInitialCacheWithGlob()
    } catch (e) {
      console.error(e)
      return await this._buildInitialCacheWithAtom()
    }
  }

  /**
   * Builds the initial file cache using Atom
   * @return {Promise}
   * @private
   */
  async _buildInitialCacheWithAtom() {
    await this._cacheProjectPathsAndRepositories()
    const result = await Promise.all(
      this._projectDirectories.map((projectDirectory) => {
        return this._cacheDirectoryFilePaths(projectDirectory, projectDirectory)
      })
    )
    this.emit("rebuild-cache-done")
    return result
  }

  /**
   * Returns the file paths for the given project directory with the given (optional) relative path
   * @param  {Directory} projectDirectory
   * @param  {String} [relativeToPath=null]
   * @return {String[]}
   */
  getFilePathsForProjectDirectory(projectDirectory, relativeToPath = null) {
    let filePaths = this._filePathsByProjectDirectory.get(projectDirectory.path) || []
    if (relativeToPath) {
      return filePaths.filter((filePath) => filePath.indexOf(relativeToPath) === 0)
    }
    return filePaths
  }

  /**
   * Disposes this PathsCache
   */
  dispose(isPackageDispose) {
    this._fileWatchersByDirectory.forEach((watcher) => {
      watcher.dispose()
    })
    this._fileWatchersByDirectory = new Map()
    this._filePathsByProjectDirectory = new Map()
    this._filePathsByDirectory = new Map()
    this._repositories = []
    if (this._projectWatcher) {
      this._projectWatcher.dispose()
      this._projectWatcher = null
    }
    if (isPackageDispose && this._projectChangeWatcher) {
      this._projectChangeWatcher.dispose()
      this._projectChangeWatcher = null
    }
  }

  //
  // Cache with `find`
  //

  /**
   * Builds the initial file cache with `find`
   * @return {Promise}
   * @private
   */
  async _buildInitialCacheWithGlob() {
    await this._cacheProjectPathsAndRepositories()
    const result = await Promise.all(this._projectDirectories.map((d) => this._populateCacheFor(d)))

    this.emit("rebuild-cache-done")
    return result
  }

  _onDidChangeFiles(events) {
    events
      .filter((event) => event.action !== "modified")
      .forEach((event) => {
        if (!this._projectDirectories) {
          return
        }

        const { action, path, oldPath } = event

        const projectDirectory = this._projectDirectories.find(
          (projectDirectory) => path.indexOf(projectDirectory.path) === 0
        )

        if (!projectDirectory) {
          return
        }
        const directoryPath = projectDirectory.path
        const ignored = this._isPathIgnored(path)

        if (ignored) {
          return
        }

        const files = this._filePathsByProjectDirectory.get(directoryPath) || []

        switch (action) {
          case "created":
            files.push(path)
            break

          case "deleted": {
            const i = files.indexOf(path)
            if (i > -1) {
              files.splice(i, 1)
            }
            break
          }
          case "renamed": {
            const j = files.indexOf(oldPath)
            if (j > -1) {
              files[j] = path
            }
            break
          }
        }

        if (!this._filePathsByProjectDirectory.has(directoryPath)) {
          this._filePathsByProjectDirectory.set(directoryPath, files)
        }
      })
  }

  /**
   * Returns a list of ignore patterns for a directory
   * @param  {String} directoryPath
   * @returns {Array<string>} an array of glob patterns
   * @private
   */
  _getIgnoredPatternsGlob(directoryPath) {
    const patterns = []

    if (this.config.shouldIgnoredNames) {
      patterns.push(...this.config.ignoredNames)
    }

    if (this.config.ignoredPatterns) {
      patterns.push(...this.config.ignoredPatterns)
    }

    const patternsNum = patterns.length

    let globEntries = new Array(patternsNum)

    for (let iEntry = 0; iEntry < patternsNum; iEntry++) {
      const globifyOutput = globifyPath(patterns[iEntry], directoryPath)

      // Check if `globifyPath` returns a pair or a string
      if (typeof globifyOutput === "string") {
        // string
        globEntries[iEntry] = globifyOutput // Place the entry in the output array
      } else {
        // pair
        globEntries[iEntry] = globifyOutput[0] // Place the entry in the output array
        globEntries.push(globifyOutput[1]) // Push the additional entry
      }
    }
    return globEntries
  }

  /**
   * Returns the glob pattern of a gitignore of a directory
   * @param  {String} directoryPath
   * @returns {Promise<Array<string>>} an array of glob patterns
   * @private
   */
  async _getGitIgnoreGlob(directoryPath) {
    if (this.config.excludeVcsIgnoredPaths) {
      try {
        return await globifyGitIgnoreFile(directoryPath)
      } catch (err) {
        // .gitignore does not exist for this directory, ignoring
      }
    }
    return []
  }

  /**
   * Populates cache for a project directory
   * @param  {Directory} projectDirectory
   * @return {Promise}
   * @private
   */
  async _populateCacheFor(projectDirectory) {
    const directoryPath = projectDirectory.path

    const directoryGlob = globifyDirectory(directoryPath)
    const gitignoreGlob = await this._getGitIgnoreGlob(directoryPath)
    const ignoredPatternsGlob = this._getIgnoredPatternsGlob(directoryPath)

    const files = await glob(
      [directoryGlob, ...gitignoreGlob, ...ignoredPatternsGlob],
      // glob options
      {
        dot: true,
        cwd: directoryPath,
        onlyFiles: true,
      }
    )
    this._filePathsByProjectDirectory.set(directoryPath, files)
    return files
  }
}
<|endoftext|>