UNPKG

supergiovane

Version:

website for searching through history of npm modules

473 lines (422 loc) 11.8 kB
'use strict'; /** * @file supergiovane main * @module supergiovane * @subpackage main * @version 1.7.0 * @author hex7c0 <hex7c0@gmail.com> * @copyright hex7c0 2014 * @license GPLv3 */ /* * initialize module */ var cluster = require('cluster'); var https = require('https'); var http = require('http'); var resolve = require('path').resolve; var status = http.STATUS_CODES; var express = require('express'); var logger = require('logger-request'); var semver = require('semver').valid; var setHeader = require('setheaders').setWritableHeader; var validate = require('validate-npm-package-name'); var zlib = require('zlib').unzipSync; // load var VERSION = JSON.parse(require('fs') .readFileSync(__dirname + '/package.json')); VERSION = VERSION.name + '@' + VERSION.version; var isApi = /^\/api(\/)?/i; var debug = function() { return; }; /* * functions */ /** * starting point * * @function bootstrap * @param {Object} my - user cfg * @return {Object} */ function bootstrap(my) { // setting var app = express(); app.set('env', my.env); if (my.env == 'production') { app.enable('view cache'); } else { app.disable('view cache'); } app.enable('case sensitive routing'); app.enable('trust proxy'); app.disable('x-powered-by'); // middleware if (my.logger) { app.use(logger(my.logger)); } if (my.timeout) { app.use(require('timeout-request')(my.timeout)); } if (my.signature) { app.use(require('server-signature')(my.signature)); } if (my.sitemap) { require('express-sitemap')(my.sitemap).toFile(); } if (my.mamma) { require('mamma').createClient(my.mamma, process.pid + ':supergiovane').on( 'error', function(err) { debug('cluster', { pid: process.pid, status: 'mamma', error: err.message }); }); } app.use(require('compression-zlib')()); var cache; if (my.cache) { /** * save cache * * @function caches * @param {String} bod - output body * @param {String} hash - hash * @param {String} cont - set content * @return {Object} */ cache = function(bod, hash, cont) { if (STORY[hash] === undefined) { var c = 0, old = Date.now(), last; for ( var pr in STORY) { c++; if (STORY[pr].time < old) { last = pr; old = STORY[last].time; } } if (c >= my.cache) { delete STORY[last]; } STORY[hash] = { content: cont, body: bod, time: Date.now() }; } }; } else { cache = function() { return; }; } // cfg // http.globalAgent.maxSockets = Math.pow(my.fork, 2); // by default set to Infinity on iojs 1.0 var httpsAgent = new https.Agent({ keepAlive: true }); var STORY = Object.create(null); var index = resolve(my.dir + 'index.min.html'); // routing /** * http request (no client ajax due browser security limitation) * * @function * @param {Object} req - client request * @param {Object} res - response to client * @param {next} next - next callback */ app.get('/api/:pkg/:extra?', function(req, res, next) { var isBadge = false; var version = '/'; var pkg = req.params.pkg; var extra = req.params.extra || ''; var style = req.query.style ? '?style=' + req.query.style : ''; var hash = pkg + extra + style; // checkpoint if (my.cache !== false && STORY[hash]) { // flush cache after 1 day if (Date.now() - STORY[hash].time > my.flush) { delete STORY[hash]; } return setHeader(res, 'Content-Type', STORY[hash].content) === true ? res .status(202).send(STORY[hash].body) : null; } var val = validate(pkg); // pkg name if (!val.validForNewPackages && !val.validForOldPackages) { return next(new Error(status[404])); } if (extra !== '') { // extra information if (extra === 'badge.svg') { // display badge isBadge = true; } else if (semver(extra)) { // single package version += extra; } else { // semver fail return next(new Error(status[404])); } } https.get({ host: 'registry.npmjs.org', path: '/' + pkg + version, agent: httpsAgent, headers: { 'User-Agent': VERSION, 'Accept-Encoding': 'gzip' } }, function(response) { if (response.statusCode !== 200) { return next(new Error(status[404])); } var body = new Buffer(0); response.on('data', function(chunk) { body = Buffer.concat([ body, chunk ]);// buffer }).on('end', function() { try { if (response.headers['content-encoding'] === 'gzip') { body = zlib(body); } body = JSON.parse(body); } catch (err) { return next(new Error(status[502])); } // reduce body body.readme = null; body.keywords = []; body.contributors = []; body.users = {}; // badge if (isBadge === true) { // build another request var c = Object.keys(body.versions).length; var plu = c > 1 ? 's-' : '-'; return https.get({ host: 'img.shields.io', path: '/badge/version' + plu + c + '-red.svg' + style, agent: httpsAgent, headers: { 'User-Agent': VERSION } }, function(response) { if (response.statusCode !== 200) { return next(new Error(status[404])); } var badge = new Buffer(0); response.on('data', function(chunk) { badge = Buffer.concat([ badge, chunk ]);// buffer }).on('end', function() { var content = 'image/svg+xml; charset=utf-8'; if (setHeader(res, 'Content-Type', content) === true) { res.send(badge); cache(badge, hash, content); } }).on('error', function(err) { next(new Error(status[404])); debug('client', { pid: process.pid, status: 'response', error: err.message }); }); }).on('error', function(err) { next(new Error(status[404])); debug('client', { pid: process.pid, status: 'response', error: err.message }); }); } // EOF badge var content = 'application/json; charset=utf-8'; if (setHeader(res, 'Content-Type', content) === true) { res.send(body); cache(body, hash, content); } }); }).on('error', function(err) { next(new Error(status[404])); debug('client', { pid: process.pid, status: 'request', error: err.message }); }); }); /** * index * * @param {Object} req - client request * @param {Object} res - response to client */ app.get('/', function(req, res) { res.sendFile(index); }); app.use('/static', express.static(my.dir)); /** * catch all errors returned from page * * @function * @param {Object} err - error raised * @param {Object} req - client request * @param {Object} res - response to client * @param {next} next - next callback */ app.use(function(err, req, res, next) { var code = 500; var error = err.message.toLowerCase(); // string switch (error) { case 'not found': return next(); default: debug('web', { pid: process.pid, status: 'catch', error: error }); if (my.env === 'production') { error = status[code].toLowerCase(); } break; } var out = res.status(code); if (isApi.test(req.url) === true) { out.json({ error: error }); } else { out.end(error); } }); /** * catch error 404 or if nobody cannot parse the request * * @function * @param {Object} req - client request * @param {Object} res - response to client */ app.use(function(req, res) { var code = 404; var error = status[code].toLowerCase(); var out = res.status(code); if (isApi.test(req.url) === true) { out.json({ error: error }); } else { out.end(error); } }); if (my.env != 'test') { if (my.env != 'production') { console.log(process.pid + ' | listening on: ' + my.host + ':' + my.port); } debug('web', { pid: process.pid, host: my.host, port: my.port }); var server = http.createServer(app); if (my.timeout && my.timeout.milliseconds) { server.timeout = my.timeout.milliseconds; } server.listen(my.port, my.host); } return app; } /** * option setting * * @exports supergiovane * @function supergiovane * @param {Object} opt - various options. Check README.md * @return {Object} */ module.exports = function supergiovane(opt) { var options = opt || Object.create(null); var my = { env: String(options.env || 'production'), host: String(options.host || '127.0.0.1'), port: Number(options.port) || 3000, referer: new RegExp(String(options.referer || 'http://127.0.0.1'), 'i'), dir: String(options.dir || __dirname + '/public/'), logger: options.logger === false ? false : options.logger || { filename: 'route.log', daily: true }, timeout: options.timeout === false ? false : options.timeout || {}, sitemap: options.sitemap === false ? false : options.sitemap || {}, signature: options.signature === false ? false : options.signature || false, cache: options.cache === false ? false : Number(options.cache) || 6, flush: Number(options.flush) || 86400000, fork: Number(opt.fork) < 5 ? require('os').cpus().length : Number(opt.fork) || require('os').cpus().length, max: typeof opt.max === 'string' ? opt.max : Number(opt.max) || 0, debug: options.debug === false ? false : options.debug || 'debug.log', task: Boolean(options.task) ? options.task : false, mamma: Boolean(options.mamma) ? options.mamma : false }; if (my.debug) { debug = logger({ filename: my.debug, daily: true, standalone: true, winston: { logger: '_spDebug', level: 'debug', json: false } }); } /* * father */ if (cluster.isMaster) { if (my.task) { require('task-manager')(my.task); } // no cluster if (my.env != 'production') { return bootstrap(my); } // cluster for (var i = 0; i < my.fork; i++) { cluster.fork(); } /** * work if child die * * @function * @param {Object} worker - worker child * @param {Integer} code - error code * @param {String} signal - error signal */ cluster.on('exit', function(worker, code, signal) { debug('cluster', { pid: worker.process.pid, status: code || signal, suicide: worker.suicide, max: my.max }); if (worker.suicide === true) { // task-manager kill or disconnect cluster.fork(); } else if (isNaN(my.max) === true || my.max-- > 0) { // bug restart, not too much cluster.fork(); } }); } /* * child */ process.on('uncaughtException', function(err) { debug('cluster', { pid: process.pid, status: 'uncaughtException', error: err.message, stack: err.stack }); setTimeout(function() { // wait logger write process.exit(1); }, 250); }); return bootstrap(my); };