injoin/plook

View on GitHub
lib/plook.js

Summary

Maintainability
B
5 hrs
Test Coverage
"use strict";

// Core modules
var https = require( "https" );
var url = require( "url" );

// Dependencies
var bower = require( "bower" );
var Promise = require( "bluebird" );
var LRU = require( "lru-cache" );
var winston = require( "winston" );

// Locals
var utils = require( "./utils" );

// -------------------------------------------------------------------------------------------------

// Instantiate the default Plook instance
module.exports = exports = new Plook();

// Also allow access to the Plook constructor
exports.Plook = Plook;

// -------------------------------------------------------------------------------------------------

/**
 * Plook constructor
 *
 * @constructor
 * @parram      {Plook} [root]  An optional root instance to copy cache from
 */
function Plook( root ) {
    if ( !( this instanceof Plook ) ) {
        return new Plook();
    }

    // Create a custom socket for when getting files
    this._agent = new https.Agent();
    this._agent.maxSockets = 10;

    Object.defineProperty( this, "_cache", {
        value: root instanceof Plook ? root._cache : LRU({
            max: 500
        }),
        enumerable: false,
        writable: false
    });

    this.logger = new winston.Logger({
        levels: {
            debug:  0,
            server: 5,
            info:  10,
            warn:  40,
            error: 50
        },
        colors: {
            debug:  "white",
            server: "blue",
            info:   "cyan",
            warn:   "yellow",
            error:  "red"
        }
    });

    // Configure Console transport
    this.logger.add( winston.transports.Console, {
        level: "debug",
        timestamp: true,
        colorize: true
    });
}

/**
 * Lookup a package slug by its name
 *
 * @param   {String} name   The name of the package
 * @returns {Promise}
 */
Plook.prototype.lookup = function( name ) {
    var plook = this;

    return new Promise(function( resolve, reject ) {
        var cmd;

        if ( !name ) {
            plook.logger.error( "package name not provided" );
            return reject( utils.createHttpError( 400, "Package name not provided" ) );
        }

        // If this package is already cached, let's use that cached value
        if ( plook._cache.has( name ) ) {
            return resolve( plook._cache.get( name ).slug );
        }

        cmd = bower.commands.lookup( name );
        cmd.on( "end", function( pkg ) {
            var err;
            var slug = pkg ? utils.slug( pkg.url ) : null;

            if ( slug ) {
                // Put this slug in the cache object
                plook._cache.set( name, {
                    slug: slug
                });

                return resolve( slug );
            } else if ( pkg ) {
                plook.logger.error( "not a github repository: %s", name );
                err = utils.createHttpError( 412, "Not a GitHub repository" );
            } else {
                plook.logger.error( "package not found: %s", name );
                err = utils.createHttpError( 404, "Package not found" );
            }

            reject( err );
        });
    });
};

/**
 * Find possible resolution URLs for a file and a package version.
 * If version is not passed, will use "latest" as the version.
 *
 * @param   {String} name       The package name
 * @param   {String} [version]  The package version
 * @param   {String} file       The file path
 * @returns {Promise}
 */
Plook.prototype.findURLs = function( name, version, file ) {
    var latest;
    var plook = this;

    version = version.trim();

    // If a file arg is not available, let's use the version as the file and 'latest' as the version
    if ( !file ) {
        file = version;
        version = "latest";
    }

    latest = version.toLowerCase() === "latest";
    version = latest ? version : version.replace( /^v/, "" );

    return this.lookup( name ).then(function( slug ) {
        var cached = plook._cache.get( name );
        var hasVersion = function() {
            return cached.versions && !!~cached.versions.indexOf( version );
        };
        var createURLs = function() {
            return [ "v", "" ].map(function( prefix ) {
                return utils.githubUrl( slug, prefix + version, file );
            });
        };

        return new Promise(function( resolve, reject ) {
            var cmd;

            if ( !latest && hasVersion() ) {
                return resolve( createURLs() );
            }

            cmd = bower.commands.info( name );
            cmd.on( "end", function( pkg ) {
                cached.versions = pkg.versions;
                version = latest ? pkg.versions[ 0 ] : version;

                if ( !hasVersion() ) {
                    plook.logger.error( "version not found: %s#%s", name, version );
                    return reject( utils.createHttpError( 404, "Version not found" ) );
                }

                resolve( createURLs() );
            });
        });
    });
};

/**
 * Get the content of a file in a package version.
 * Optionally, an ETag can also be provided.
 *
 * @param   {String} name       The package name
 * @param   {String} [version]  The package version
 * @param   {String} file       The file path
 * @param   {String} [etag]     An optional ETag string to act as a HTTP cache header
 * @returns {Promise}
 */
Plook.prototype.get = function( name, version, file, etag ) {
    var promise;
    var plook = this;

    if ( !file ) {
        file = version;
        version = null;
    }

    promise = this.findURLs( name, version, file ).call( "map", function( reqUrl ) {
        var options = url.parse( reqUrl );
        options.agent = plook._agent;
        options.headers = {
            "if-none-match": etag
        };

        return new Promise(function( resolve, reject ) {
            https.get( options, function( response ) {
                var err;

                if ( response.statusCode >= 400 ) {
                    err = utils.createHttpError( response.statusCode, "File not found" );
                    err.url = reqUrl;

                    reject( err );
                } else {
                    plook.logger.debug( "getting: %s#%s - %s", name, version, file );

                    resolve({
                        url: reqUrl,
                        response: response
                    });
                }
            });
        });
    });

    return Promise.any( promise ).catch( Promise.AggregateError, function( err ) {
        plook.logger.error( "file not found: %s#%s - %s", name, version, file );
        throw err[ 0 ];
    });
};

/**
 * Branch the current plook instance to allow for logging an object with every log message.
 *
 * @param   {Object} [custom]   The custom meta object to use in the logs
 * @returns {Plook}             The new Plook instance
 */
Plook.prototype.branch = function( custom ) {
    var logger = this.logger;
    var branch = new Plook( this );

    branch.logger.log = function() {
        var meta, callback;
        var args = [].slice.call( arguments );

        // Handle log callback
        callback = typeof args[ args.length - 1 ] === "function" ? args.pop() : null;

        // Handle message meta
        meta = typeof args[ args.length - 1 ] === "object" ? args.pop() : {};
        utils.extend( meta, custom );

        // Readd meta object and callback as necessary
        args.push( meta );
        callback ? args.push( callback ) : null;

        return logger.log.apply( logger, args );
    };

    return branch;
};