<commit_before>var Download = require('./download'),
    Package = require('./package'),
    UrlCheck = require('./urlcheck'),
    debug = require('debug')('npm-mirror:SyncManager'),
    fs = require('graceful-fs'),
    maybeMkdir = require('./maybemkdir'),
    path = require('path'),
    url = require('url');

/**
 * @constructor
 *
 * @param {string} master npm registry to get packages from.
 * @param {Object} packageToVersions map from package name to object with
 *     key list of versions we need for the package.
 * @param {string} hostname new host that will serve mirrored packages.
 * @param {string} root path for mirrored packages.
 */
function SyncManager(master, packageToVersions, hostname, root) {
  this.master = master;
  this.packageToVersions = packageToVersions;
  this.hostname = hostname;
  this.root = root;
}
module.exports = SyncManager;

SyncManager.prototype = {
  /**
   * @type {string}
   */
  master: null,

  /**
   * @type {Object}
   */
  packageToVersions: null,

  /**
   * @type {string}
   */
  hostname: null,

  /**
   * @type {string}
   */
  root: null,

  /**
   * Sync the packages we're watching from the master registry.
   *
   * 1. Use semver to resolve all loose dependency declarations.
   * 2. Find all of our package version dependencies recursively.
   * 3. Make sure that we have directories for each package version.
   * 4. Download the world.
   *
   * @param {Function} callback invoke when done.
   */
  sync: function(callback) {
    debug('sync');

    // Step 1: Resolve loose package versions.
    Package.versions(
      this.master, this.packageToVersions, function(e, packageToVersions) {
      if (e) {
        return callback && callback(e);
      }

      this.packageToVersions = packageToVersions;

      // Step 2: Dependency search.
      debug('dependency search');
      this.dependencySearch(this.packageToVersions, function(e) {
        if (e) {
          return callback && callback(e);
        }

        // Step 3: Make sure we have package and package version dirs.
        this.makedirs(function(e) {
          if (e) {
            return callback && callback(e);
          }

          // Step 4: Download the world.
          this.download(callback);
        }.bind(this));
      }.bind(this));
    }.bind(this));
  },

  /**
   * Check each package version we're syncing for the master's
   * package version object. Add any new package version dependencies
   * to the in-memory map we're building. Then recursively perform the
   * procedure for all new package version dependencies.
   *
   * @param {Object} packageToVersions map from package name to object with key
   *     list of versions we need for the package.
   * @param {Function} callback invoke when done.
   */
  dependencySearch: function(packageToVersions, callback) {
    var count = Package.versionCount(packageToVersions);

    // Base case.
    if (count === 0) {
      return callback && callback();
    }

    // For each package version, check master package version object.
    // Issue a recursive dependencySearch with all of the "new" deps we find.
    var packages = Object.keys(packageToVersions);
    packages.forEach(function(package) {
      var versions = Object.keys(packageToVersions[package]);
      versions.forEach(function(version) {
        function process() {
          if (--count <= 0) {
            return callback && callback();
          }
        }

        // Careful! The version could still be a git/tarball url...
        // Or version could be null if semver couldn't find a match.
        if (!version || version === 'null' || UrlCheck.isWebUrl(version) ||
            UrlCheck.isGitUrl(version)) {
          // TODO(gaye): Eh? What to do?
          delete this.packageToVersions[package][version];
          return process();
        }

        var packageVersionUrl = Package.url(this.master, package, version);
        Download.inst.download(
          packageVersionUrl, function(e, packageVersionData) {
          if (e) {
            // If we choke on a package, it's probably something wacky or
            // no longer exists. Let's ignore it for now, but log it just
            // in case.
            debug('error downloading package ' + package + ': ' + e);
            delete this.packageToVersions[package][version];
            return process();
          }

          var packageVersion = JSON.parse(packageVersionData);
          var deps = Package.dependencies(
            packageVersion, ['dependencies', 'peerDependencies'] /* types */);
          Package.versions(this.master, deps, function(e, depToVersions) {
            if (e) {
              return callback && callback(e);
            }

            var newDeps =
              this.addDependencies(packageVersion.name, depToVersions);
            this.dependencySearch(newDeps, function(e) {
              if (e) {
                return callback && callback(e);
              }

              return process();
            });
          }.bind(this));
        }.bind(this));
      }.bind(this));
    }.bind(this));
  },

  /**
   * Add dependencies to our in-memory map from packages to versions.
   *
   * @param {string} parent package that needs these dependencies.
   * @param {Object} depToVersions map from package name to object with key list
   *     of versions we need for the dep package.
   * @return {Object} new deps.
   */
  addDependencies: function(parent, depToVersions) {
    var newDeps = {};
    var packages = Object.keys(depToVersions);
    packages.forEach(function(package) {
      var currVersions = this.packageToVersions[package];
      var depVersions = depToVersions[package];
      newDeps[package] = {};

      // Check if we are watching this package.
      if (!currVersions) {
        // Add all of the versions.
        debug(parent + ' needs ' + package + '@' +
              Object.keys(depVersions).join(', '));
        this.packageToVersions[package] = depVersions;
        newDeps[package] = depVersions;
        return;
      }

      // Check which versions we are watching.
      var versions = Object.keys(depVersions);
      versions
        .filter(function(version) {
          return !(version in currVersions);
        })
        .forEach(function(version) {
          // Add this version.
          debug(parent + ' needs ' + package + '@' + version);
          this.packageToVersions[package][version] = true;
          newDeps[package][version] = true;
        }.bind(this));
    }.bind(this));

    return newDeps;
  },

  /**
   * Make directories for all of the packages and package versions.
   *
   * @param {Function} callback [err] invoke when done.
   */
  makedirs: function(callback) {
    var dirs = [];
    for (var package in this.packageToVersions) {
      var packagePath = path.resolve(this.root, package);
      dirs.push(packagePath);

      var versions = this.packageToVersions[package];
      for (var version in versions) {
        var packageVersionPath = path.resolve(this.root, package, version);
        dirs.push(packageVersionPath);
      }
    }

    var count = dirs.length;
    debug('maybe make ' + count + ' dirs');
    dirs.forEach(function(dir) {
      maybeMkdir(dir, function onMaybeMkdir(e) {
        if (e) {
          // TODO(gaye): Eventually we should give up.
          debug('error maybemkdir, will try again ' + dir + ': ' + e);
          return maybeMkdir(dir, onMaybeMkdir);
        }

        if (--count <= 0) {
          return callback && callback();
        }
      });
    });
  },

  /**
   * Download all the package root objects, package version objects,
   * and tarballs.
   *
   * @param {Function} callback invoke when done.
   */
  download: function(callback) {
    this.downloadPackageRootObjects(function(e) {
      if (e) {
        return callback && callback(e);
      }

      this.downloadPackageVersions(callback);
    }.bind(this));
  },

  /**
   * Download a package root object for each package.
   *
   * @param {Function} callback invoke when done.
   */
  downloadPackageRootObjects: function(callback) {
    var packages = Object.keys(this.packageToVersions);
    var count = packages.length;
    function process() {
      if (--count <= 0) {
        return callback && callback();
      }
    }

    packages.forEach(function(package) {
      var packageRootUrl = url.resolve(this.master, package);
      // TODO(gaye): Use etags here so that we don't download again
      //     if, for instance, no new packages have been published.
      Download.inst.download(packageRootUrl, function(e, packageRootData) {
        if (e) {
          debug('error downloading package ' + package + ': ' + e);
          return process();
        }

        var packageRoot = JSON.parse(packageRootData);
        var copy = {};
        copy.name = packageRoot.name;
        copy._id = packageRoot.name;
        copy.versions = {};
        var versions = Object.keys(this.packageToVersions[package]);
        versions.forEach(function(version) {
          copy.versions[version] = Package.url(this.hostname, package, version);
        }.bind(this));

        copy['dist-tags'] = {};
        var sorted = versions.sort();
        copy['dist-tags'].latest = sorted[sorted.length - 1];

        var dest = path.resolve(this.root, package, 'index.json');
        fs.writeFile(dest, JSON.stringify(copy), function(e) {
          if (e) {
            return callback && callback(e);
          }

          return process();
        });
      }.bind(this));
    }.bind(this));
  },

  /**
   * Download a package version object and tarball for each package version.
   *
   * @param {Function} callback invoke when done.
   */
  downloadPackageVersions: function(callback) {
    var count = Package.versionCount(this.packageToVersions);
    function process() {
      if (--count <= 0) {
        return callback && callback();
      }
    }

    var packages = Object.keys(this.packageToVersions);
    packages.forEach(function(package) {
      var versions = this.packageToVersions[package];

      var onExists = function(exists) {
        // TODO(gaye): Use etags here to prevent the cases where packages
        //     are modified or deleted after being synced.
        if (exists) {
          return process();
        }

        var packageVersionUrl = Package.url(this.master, package, version);
        Download.inst.download(
          packageVersionUrl, function(e, packageVersionData) {
          if (e) {
            debug('error downloading package ' + package + ': ' + e);
            return process();
          }

          var packageVersion = JSON.parse(packageVersionData);
          var copy = {};
          copy.name = packageVersion.name;
          copy.version = packageVersion.version;
          copy.dependencies = packageVersion.dependencies;
          copy.devDependencies = packageVersion.devDependencies;
          copy.peerDependencies = packageVersion.peerDependencies;
          copy.dist = {};
          copy.dist.tarball =
            Package.tarballUrl(this.master, package, version);

          fs.writeFile(dest, JSON.stringify(copy), function() {
            var tarball = packageVersion.dist.tarball;
            var dest = path.resolve(
              this.root, package, version,
              package + '-' + version + '.tgz');
            Download.inst.downloadToDisk(tarball, dest, function(e) {
              if (e) {
                return callback && callback(e);
              }

              process();
            });
          }.bind(this));
        }.bind(this));
      }.bind(this);

      for (var version in versions) {
        var dest = path.resolve(this.root, package, version, 'index.json');
        fs.exists(dest, onExists);
      }
    }.bind(this));
  }
};
<commit_msg>Bug 934608 - npm-mirror doesn't always download all versions when it needs multiple<commit_after>var Download = require('./download'),
    Package = require('./package'),
    UrlCheck = require('./urlcheck'),
    debug = require('debug')('npm-mirror:SyncManager'),
    fs = require('graceful-fs'),
    maybeMkdir = require('./maybemkdir'),
    path = require('path'),
    url = require('url');

/**
 * @constructor
 *
 * @param {string} master npm registry to get packages from.
 * @param {Object} packageToVersions map from package name to object with
 *     key list of versions we need for the package.
 * @param {string} hostname new host that will serve mirrored packages.
 * @param {string} root path for mirrored packages.
 */
function SyncManager(master, packageToVersions, hostname, root) {
  this.master = master;
  this.packageToVersions = packageToVersions;
  this.hostname = hostname;
  this.root = root;
}
module.exports = SyncManager;

SyncManager.prototype = {
  /**
   * @type {string}
   */
  master: null,

  /**
   * @type {Object}
   */
  packageToVersions: null,

  /**
   * @type {string}
   */
  hostname: null,

  /**
   * @type {string}
   */
  root: null,

  /**
   * Sync the packages we're watching from the master registry.
   *
   * 1. Use semver to resolve all loose dependency declarations.
   * 2. Find all of our package version dependencies recursively.
   * 3. Make sure that we have directories for each package version.
   * 4. Download the world.
   *
   * @param {Function} callback invoke when done.
   */
  sync: function(callback) {
    debug('sync');

    // Step 1: Resolve loose package versions.
    Package.versions(
      this.master, this.packageToVersions, function(e, packageToVersions) {
      if (e) {
        return callback && callback(e);
      }

      this.packageToVersions = packageToVersions;

      // Step 2: Dependency search.
      debug('dependency search');
      this.dependencySearch(this.packageToVersions, function(e) {
        if (e) {
          return callback && callback(e);
        }

        // Step 3: Make sure we have package and package version dirs.
        this.makedirs(function(e) {
          if (e) {
            return callback && callback(e);
          }

          // Step 4: Download the world.
          this.download(callback);
        }.bind(this));
      }.bind(this));
    }.bind(this));
  },

  /**
   * Check each package version we're syncing for the master's
   * package version object. Add any new package version dependencies
   * to the in-memory map we're building. Then recursively perform the
   * procedure for all new package version dependencies.
   *
   * @param {Object} packageToVersions map from package name to object with key
   *     list of versions we need for the package.
   * @param {Function} callback invoke when done.
   */
  dependencySearch: function(packageToVersions, callback) {
    var count = Package.versionCount(packageToVersions);

    // Base case.
    if (count === 0) {
      return callback && callback();
    }

    // For each package version, check master package version object.
    // Issue a recursive dependencySearch with all of the "new" deps we find.
    var packages = Object.keys(packageToVersions);
    packages.forEach(function(package) {
      var versions = Object.keys(packageToVersions[package]);
      versions.forEach(function(version) {
        function process() {
          if (--count <= 0) {
            return callback && callback();
          }
        }

        // Careful! The version could still be a git/tarball url...
        // Or version could be null if semver couldn't find a match.
        if (!version || version === 'null' || UrlCheck.isWebUrl(version) ||
            UrlCheck.isGitUrl(version)) {
          // TODO(gaye): Eh? What to do?
          delete this.packageToVersions[package][version];
          return process();
        }

        var packageVersionUrl = Package.url(this.master, package, version);
        Download.inst.download(
          packageVersionUrl, function(e, packageVersionData) {
          if (e) {
            // If we choke on a package, it's probably something wacky or
            // no longer exists. Let's ignore it for now, but log it just
            // in case.
            debug('error downloading package ' + package + ': ' + e);
            delete this.packageToVersions[package][version];
            return process();
          }

          var packageVersion = JSON.parse(packageVersionData);
          var deps = Package.dependencies(
            packageVersion, ['dependencies', 'peerDependencies'] /* types */);
          Package.versions(this.master, deps, function(e, depToVersions) {
            if (e) {
              return callback && callback(e);
            }

            var newDeps =
              this.addDependencies(packageVersion.name, depToVersions);
            this.dependencySearch(newDeps, function(e) {
              if (e) {
                return callback && callback(e);
              }

              return process();
            });
          }.bind(this));
        }.bind(this));
      }.bind(this));
    }.bind(this));
  },

  /**
   * Add dependencies to our in-memory map from packages to versions.
   *
   * @param {string} parent package that needs these dependencies.
   * @param {Object} depToVersions map from package name to object with key list
   *     of versions we need for the dep package.
   * @return {Object} new deps.
   */
  addDependencies: function(parent, depToVersions) {
    var newDeps = {};
    var packages = Object.keys(depToVersions);
    packages.forEach(function(package) {
      var currVersions = this.packageToVersions[package];
      var depVersions = depToVersions[package];
      newDeps[package] = {};

      // Check if we are watching this package.
      if (!currVersions) {
        // Add all of the versions.
        debug(parent + ' needs ' + package + '@' +
              Object.keys(depVersions).join(', '));
        this.packageToVersions[package] = depVersions;
        newDeps[package] = depVersions;
        return;
      }

      // Check which versions we are watching.
      var versions = Object.keys(depVersions);
      versions
        .filter(function(version) {
          return !(version in currVersions);
        })
        .forEach(function(version) {
          // Add this version.
          debug(parent + ' needs ' + package + '@' + version);
          this.packageToVersions[package][version] = true;
          newDeps[package][version] = true;
        }.bind(this));
    }.bind(this));

    return newDeps;
  },

  /**
   * Make directories for all of the packages and package versions.
   *
   * @param {Function} callback [err] invoke when done.
   */
  makedirs: function(callback) {
    var dirs = [];
    for (var package in this.packageToVersions) {
      var packagePath = path.resolve(this.root, package);
      dirs.push(packagePath);

      var versions = this.packageToVersions[package];
      for (var version in versions) {
        var packageVersionPath = path.resolve(this.root, package, version);
        dirs.push(packageVersionPath);
      }
    }

    var count = dirs.length;
    debug('maybe make ' + count + ' dirs');
    dirs.forEach(function(dir) {
      maybeMkdir(dir, function onMaybeMkdir(e) {
        if (e) {
          // TODO(gaye): Eventually we should give up.
          debug('error maybemkdir, will try again ' + dir + ': ' + e);
          return maybeMkdir(dir, onMaybeMkdir);
        }

        if (--count <= 0) {
          return callback && callback();
        }
      });
    });
  },

  /**
   * Download all the package root objects, package version objects,
   * and tarballs.
   *
   * @param {Function} callback invoke when done.
   */
  download: function(callback) {
    this.downloadPackageRootObjects(function(e) {
      if (e) {
        return callback && callback(e);
      }

      this.downloadPackageVersions(callback);
    }.bind(this));
  },

  /**
   * Download a package root object for each package.
   *
   * @param {Function} callback invoke when done.
   */
  downloadPackageRootObjects: function(callback) {
    var packages = Object.keys(this.packageToVersions);
    var count = packages.length;
    function process() {
      if (--count <= 0) {
        return callback && callback();
      }
    }

    packages.forEach(function(package) {
      var packageRootUrl = url.resolve(this.master, package);
      // TODO(gaye): Use etags here so that we don't download again
      //     if, for instance, no new packages have been published.
      Download.inst.download(packageRootUrl, function(e, packageRootData) {
        if (e) {
          debug('error downloading package ' + package + ': ' + e);
          return process();
        }

        var packageRoot = JSON.parse(packageRootData);
        var copy = {};
        copy.name = packageRoot.name;
        copy._id = packageRoot.name;
        copy.versions = {};
        var versions = Object.keys(this.packageToVersions[package]);
        versions.forEach(function(version) {
          copy.versions[version] = Package.url(this.hostname, package, version);
        }.bind(this));

        copy['dist-tags'] = {};
        var sorted = versions.sort();
        copy['dist-tags'].latest = sorted[sorted.length - 1];

        var dest = path.resolve(this.root, package, 'index.json');
        fs.writeFile(dest, JSON.stringify(copy), function(e) {
          if (e) {
            return callback && callback(e);
          }

          return process();
        });
      }.bind(this));
    }.bind(this));
  },

  /**
   * Download a package version object and tarball for each package version.
   *
   * @param {Function} callback invoke when done.
   */
  downloadPackageVersions: function(callback) {
    var count = Package.versionCount(this.packageToVersions);
    function process() {
      if (--count <= 0) {
        return callback && callback();
      }
    }

    var packages = Object.keys(this.packageToVersions);
    packages.forEach(function(package) {
      var versions = Object.keys(this.packageToVersions[package]);
      versions.forEach(function(version) {
         var dest = path.resolve(this.root, package, version, 'index.json');
         fs.exists(dest, function onExists(exists) {
          // TODO(gaye): Use etags here to prevent the cases where packages
          //     are modified or deleted after being synced.
          if (exists) {
            return process();
          }

          // Now we know that the package version has not been synced,
          // so we should go ahead and download it.
          var packageVersionUrl = Package.url(this.master, package, version);
          Download.inst.download(
            packageVersionUrl, function(e, packageVersionData) {
            if (e) {
              debug('error downloading package ' + package + ': ' + e);
              return process();
            }

            var packageVersion = JSON.parse(packageVersionData);
            var copy = {};
            copy.name = packageVersion.name;
            copy.version = packageVersion.version;
            copy.dependencies = packageVersion.dependencies;
            copy.devDependencies = packageVersion.devDependencies;
            copy.peerDependencies = packageVersion.peerDependencies;
            copy.dist = {};
            copy.dist.tarball =
              Package.tarballUrl(this.master, package, version);

            fs.writeFile(dest, JSON.stringify(copy), function() {
              var tarball = packageVersion.dist.tarball;
              var dest = path.resolve(
                this.root, package, version,
                package + '-' + version + '.tgz');
              Download.inst.downloadToDisk(tarball, dest, function(e) {
                if (e) {
                  return callback && callback(e);
                }

                process();
              });
            }.bind(this));
          }.bind(this));
         }.bind(this));
      }.bind(this));
    }.bind(this));
  }
};
<|endoftext|>


<commit_before>/*
 * syndicate-g
 * https://github.com/petarov/syndicate-g
 *
 * The MIT License (MIT)
 * Copyright (c) 2013 Petar Petrov
 */
"use strict";

var config;
var _ = require('underscore')._;
var request = require('request');
var RSS = require('rss');
var Persistence = require('./persistence')(config);

/**
 * Generate ATOM feed from G+ json data
 * @param  {String} G+ profile json
 * @param  {String} G+ posts json
 * @return {String} xml
 */
function generateRSS(profileRAW, postsRAW) {
    var json = JSON.parse(postsRAW);
    var jsonProfile = JSON.parse(profileRAW);

    var feed = new RSS({
        title: jsonProfile.displayName + ' Google Plus Feed',
        description: json.title,
        feed_url: config.server.url + '/' + jsonProfile.id,
        site_url: jsonProfile.url,
        image_url: jsonProfile.image ? jsonProfile.image.url || '' : '',
        author: jsonProfile.displayName,
        copyright: jsonProfile.displayName,
        language: 'en',
        categories: ['public'],
        pubDate: json.updated,
        ttl: '60'
    });

    for (var i = 0; i < json.items.length; i++) {
        var item = json.items[i];
        feed.item({
            title: item.title,
            description: item.content,
            url: item.url,
            guid: item.id,
            categories: ['public'],
            author: item.actor.displayName,
            date: item.updated, // item.published
        });
    };

    // cache the xml to send to clients
    return feed.xml();
}

function req(res, profile, options, callback) {
    callback = callback ? callback : options;

    var url = 'https://www.googleapis.com/plus/v1/people/' + profile + res;
    url += '?key=' + config.gplus.apiKey;

    for (var key in options) {
        if (options.hasOwnProperty(key)) {
            url += '&' + key + '=' + options[key];
        }
    }

    if (config.isDebug)
        console.log(url);

    request(url, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            callback && callback(null, body);
        } else {
            if (config.isDebug)
                console.log(body);

            var parsedBody = JSON.parse(body);
            callback && callback(error || {message: parsedBody.error.message, code: parsedBody.error.code}, body);
        }
    });
}

function SyndicateG() {
    var self = this;
    this.persistence = Persistence.create();
    this.persistence.open(config.db.path);

    this.fetchCache = function(profile, callback) {
        self.persistence.get('feed_' + profile, function(err, value) {
            if (err && err.notFound) {
                callback && callback(err);
                return;
            }
            //TODO: handle other (non-notFound) errs

            console.log('cache hit for %s', profile);
            callback && callback(null, value);
        });
    }
}
_.extend(SyndicateG.prototype, {

    fetch: function(profile, options, callback) {
        callback = callback ? callback : options;
        var self = this;

        this.fetchCache(profile, function(err, xml) {
            if (err) {
                // fetch from Google+
                req('/activities/public', profile, {'maxResults': options.maxResults || 20}, function(error, body) {
                    if (error) {
                        callback && callback(error);
                        return;
                    }

                    var _rawPosts = body;

                    req('', profile, function(error, body) {
                        if (error) {
                            callback && callback(error);
                            return;
                        }

                        var data = generateRSS(body, _rawPosts);
                        // cache generated feed (async => skip callback)
                        self.persistence.put('feed_' + profile, data);
                        // tell caller we're done
                        callback && callback(error, data);
                    });
                });

            } else {
                callback && callback(null, xml);
            }
        });
    },

    deleteCache: function(profile, callback) {
        this.persistence.delete('feed_' + profile, function(err) {
            if (err) {
                // TODO: fund out what exactly the err obj from levelup returns here
                callback && callback({code: 500, message: 'Error removing profile data. Profile not found!?'});
            } else {
                callback && callback();
            }

        });
    },

    clearCache: function() {
        //TODO
    }

});

/**
 * Exports
 */
module.exports = function(_config) {
    config = _config;
    return {
        create: function() {
            return new SyndicateG();
        }
    };
}
<commit_msg>Added feed TTL and update functionality<commit_after>/*
 * syndicate-g
 * https://github.com/petarov/syndicate-g
 *
 * The MIT License (MIT)
 * Copyright (c) 2013 Petar Petrov
 */
"use strict";

var config;
var _ = require('underscore')._;
var request = require('request');
var RSS = require('rss');
var Persistence = require('./persistence')(config);

/**
 * Generate ATOM feed from G+ json data
 * @param  {String} G+ profile json
 * @param  {String} G+ posts json
 * @return {String} xml
 */
function generateRSS(profileRAW, postsRAW) {
    var json = JSON.parse(postsRAW);
    var jsonProfile = JSON.parse(profileRAW);

    var feed = new RSS({
        title: jsonProfile.displayName + ' Google Plus Feed',
        description: json.title,
        feed_url: config.server.url + '/' + jsonProfile.id,
        site_url: jsonProfile.url,
        image_url: jsonProfile.image ? jsonProfile.image.url || '' : '',
        author: jsonProfile.displayName,
        copyright: jsonProfile.displayName,
        language: 'en',
        categories: ['public'],
        pubDate: json.updated,
        ttl: '60' // number of minutes the feed can stay cached before refreshing it from the source.
    });

    for (var i = 0; i < json.items.length; i++) {
        var item = json.items[i];
        feed.item({
            title: item.title,
            description: item.content,
            url: item.url,
            guid: item.id,
            categories: ['public'],
            author: item.actor.displayName,
            date: item.updated, // item.published
        });
    };

    // cache the xml to send to clients
    return feed.xml();
}

function req(res, profile, options, callback) {
    callback = callback ? callback : options;

    var url = 'https://www.googleapis.com/plus/v1/people/' + profile + res;
    url += '?key=' + config.gplus.apiKey;

    for (var key in options) {
        if (options.hasOwnProperty(key)) {
            url += '&' + key + '=' + options[key];
        }
    }

    if (config.isDebug)
        console.log(url);

    request(url, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            callback && callback(null, body);
        } else {
            if (config.isDebug)
                console.log(body);

            var parsedBody = JSON.parse(body);
            callback && callback(error || {message: parsedBody.error.message, code: parsedBody.error.code}, body);
        }
    });
}

function SyndicateG() {
    var self = this;
    this.persistence = Persistence.create();
    this.persistence.open(config.db.path);

    this.fetchCachedFeed = function(profile, options, callback) {
        self.persistence.get('feed_' + profile, function(err, value) {
            if (err && err.notFound) {
                callback && callback(err);
                return;
            }
            //TODO: handle other (non-notFound) errs

            console.log('cache hit for %s', profile);
            callback && callback(null, value);

            // fetch feed again if expired
            self.persistence.get('updated_' + profile, function(err, value) {
                if (err && err.notFound) {
                    // we don't care
                    return;
                }

                var now = Date.now();
                var expires = Math.floor(value) + 10*1000;
                console.log('exp: %d', expires);
                console.log('now: %d', now);
                if (expires < now) {
                    console.log(profile + ' data expired. fetching ...')
                    self.fetchFeed(profile, options);
                }
            });
        });
    }
    this.fetchFeed = function(profile, options, callback) {
        req('/activities/public', profile, {'maxResults': options.maxResults || 20}, function(error, body) {
            if (error) {
                callback && callback(error);
                return;
            }

            var _rawPosts = body;

            req('', profile, function(error, body) {
                if (error) {
                    callback && callback(error);
                    return;
                }

                var updated = Date.now();
                var xml = generateRSS(body, _rawPosts);
                // cache generated feed (async => skip callback)
                self.persistence.put('feed_' + profile, xml);
                self.persistence.put('updated_' + profile, updated);
                // tell caller we're done
                callback && callback(error, xml);
            });
        });
    }
}
_.extend(SyndicateG.prototype, {

    fetch: function(profile, options, callback) {
        callback = callback ? callback : options;
        var self = this;

        this.fetchCachedFeed(profile, options, function(err, xml) {
            if (err) {
                // fetch from Google+
                self.fetchFeed(profile, options, function(error, xml) {
                    callback && callback(error, xml);
                });
            } else {
                callback && callback(null, xml);
            }
        });
    },

    deleteCache: function(profile, callback) {
        this.persistence.delete('feed_' + profile, function(err) {
            if (err) {
                // TODO: fund out what exactly the err obj from levelup returns here
                callback && callback({code: 500, message: 'Error removing profile data. Profile not found!?'});
            } else {
                callback && callback();
            }

        });
    },

    clearCache: function() {
        //TODO: clear everything
    }

});

/**
 * Exports
 */
module.exports = function(_config) {
    config = _config;
    return {
        create: function() {
            return new SyndicateG();
        }
    };
}
<|endoftext|>


<commit_before><commit_msg>Update xdNoPC1.js<commit_after><|endoftext|>


<commit_before>/**
 *  class Wire
 **/

(function (root) {
  'use strict';

  //////////////////////////////////////////////////////////////////////////////

  function noop() {}

  //
  // Helpers for cross-browser compatibility
  //

  var isString = function isString(obj) {
    return Object.prototype.toString.call(obj) === '[object String]';
  };

  var isArray = Array.isArray || function isArray(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
  };

  var isFunction = function isFunction(obj) {
    return Object.prototype.toString.call(obj) === '[object Function]';
  };

  //
  // Simplified stable sort implementation from Lo-Dash (http://lodash.com/)
  //

  function compareAscending(a, b) {
    var ai = a.index,
        bi = b.index;

    a = a.criteria;
    b = b.criteria;

    // ensure a stable sort in V8 and other engines
    // http://code.google.com/p/v8/issues/detail?id=90
    if (a !== b) {
      if (a > b || typeof a === 'undefined') {
        return 1;
      }
      if (a < b || typeof b === 'undefined') {
        return -1;
      }
    }

    return ai < bi ? -1 : 1;
  }

  function stableSort(collection, property) {
    var index
      , length = collection.length
      , result = new Array(length);

    for (index = 0; index < length; index += 1) {
      result[index] = {
        criteria: property ? collection[index][property] : collection[index]
      , index: index
      , value: collection[index]
      };
    }

    result.sort(compareAscending);

    for (index = 0; index < length; index += 1) {
      result[index] = result[index].value;
    }

    return result;
  }

  //////////////////////////////////////////////////////////////////////////////

  // Structure to hold handler data
  function WireHandler(channel, options, func) {
    if (channel.indexOf('*') !== -1 &&
        channel.indexOf('*') !== channel.length - 1) {
      throw 'Bad channel name \'' + channel + '\'. Broadcast symbol (*) must ' +
            'be the last character.';
    }

    this.channel   = channel;
    this.func      = func;
    this.name      = func.name || '<anonymous>';
    this.sync      = 0 === func.length || 1 === func.length;
    this.once      = Boolean(options.once);
    this.ensure    = Boolean(options.ensure);
    this.priority  = Number(options.priority || 0);
    this.ncalled   = 0;

    // Use channel name of this handler only as a prefix to match or not.
    this.isBroadcast = ('*' === channel.charAt(channel.length - 1));

    // Used to match this handler.
    this.lookupString = this.isBroadcast ? channel.slice(0, -1) : channel;
  }

  //////////////////////////////////////////////////////////////////////////////

  function Wire() {
    this.__handlers__       = [];
    this.__sortedCache__    = [];
    this.__knownChannels__  = {};
    this.__skips__          = {};
  }

  // Returns true if `handlerName` must be skipped on `channelName`.
  //
  Wire.prototype.checkSkip = function (handlerName, channelName) {
    var index, length, skip, skipBroadcast;
    var skipList = this.__skips__[handlerName] || [];

    for (index = 0, length = skipList.length; index < length; index += 1) {
      skip = skipList[index];
      skipBroadcast = ('*' === skip.charAt(skip.length - 1));

      if (!skipBroadcast && (channelName === skip)) {
        return true;
      }

      if (skipBroadcast && (0 === channelName.indexOf(skip.slice(0, -1)))) {
        return true;
      }
    }

    return false;
  };

  Wire.prototype.getHandlers = function (channel) {
    var self = this, result;

    if (!this.__sortedCache__[channel]) {
      result = [];

      this.__handlers__.forEach(function (handler) {

        // Respect skips.
        if (self.checkSkip(handler.name, channel)) {
          return;
        }

        // Non-broadcast handler - do exact match.
        if (!handler.isBroadcast && (channel !== handler.lookupString)) {
          return;
        }

        // Broadcast handler - match as a prefix.
        if (handler.isBroadcast && (0 !== channel.indexOf(handler.lookupString))) {
          return;
        }

        result.push(handler);
      });

      // We must use stable sort here to be sure, that handlers in the resulting
      // list will be placed exactly in the order they are declared.
      this.__sortedCache__[channel] = stableSort(result, 'priority');
    }

    return this.__sortedCache__[channel];
  };

  // Internal helper that runs handlers for a single channel
  function emitSingle(self, channel, params, callback) {
    var stash = self.getHandlers(channel).slice()
      , wh, fn, _err;

    // iterates through handlers of stash
    function walk(err) {

      // chain finished - exit
      if (!stash.length) {
        callback(err);
        return;
      }

      // Get next element
      wh = stash.shift();
      fn = wh.func;

      // if error - skip all handlers except 'ehshured'
      if (err && !wh.ensure) {
        walk(err);
        return;
      }

      wh.ncalled++;

      if (wh.once) {
        self.off(wh.channel, fn);
      }

      // Call handler, but protect err from overrite,
      // if already exists
      if (!wh.sync) {
        fn(params, function (_err) {
          walk(err || _err);
        });
      } else {
        _err = fn(params);
        walk(err || _err);
      }

      return;
    }

    // start stash walker
    walk();
  }

  /**
   *  Wire#emit(channels, params[, callback]) -> Void
   *  - channels (String|Array):
   *  - params (Mixed):
   *  - callback (Function):
   *
   *  Sends message with `params` into the `channel`. Once all sync and ascync
   *  handlers finished, optional `callback(err)` (if specified) fired.
   **/
  Wire.prototype.emit = function (channels, params, callback) {
    var self = this, _chs, chan;

    callback = callback || noop;

    // slightly optimize regular calls, with single channel
    //
    if (!isArray(channels)) {
      emitSingle(self, channels, params, callback);
      return;
    }

    // Lot of channel - do chaining
    //
    _chs = channels.slice();

    function walk(err) {
      if (err || !_chs.length) {
        callback(err);
        return;
      }

      chan = _chs.shift();
      emitSingle(self, chan, params, walk);
    }

    walk();
  };

  /**
   *  Wire#on(channels[, options], handler) -> Void
   *  - channels (String | Array):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Registers `handler` to be executed upon messages in the a single channel
   *  or a sequence of channels stored in `channels` parameter. Handler can be
   *  either sync function:
   *
   *      wire.on('foobar', function () {
   *        // do stuff here
   *      });
   *
   *      wire.on('foobar', function (params) {
   *        // do stuff here
   *      });
   *
   *  Or it might be an async function with `callback(err)` second argument:
   *
   *      wire.on('foobar', function (params, callback) {
   *        // do stuff here
   *        callback(null);
   *      });
   *
   *
   *  ##### Options
   *
   *  - `priority` (Number, Default: 0)
   *  - `ensure` (Boolean, Default: false)
   *    If `true`, will run handler even if one of previous fired error.
   **/
  Wire.prototype.on = function (channels, options, handler) {
    if (!channels) {
      throw 'Channel name required. Use "**" if you want "any channel".';
    }

    if (!isArray(channels)) {
      channels = [ channels ];
    }

    if (!handler) {
      handler = options;
      options = null;
    }

    options = options || {};

    if (!isFunction(handler)) {
      throw 'Not a function';
    }

    if (0 !== handler.length && 1 !== handler.length && 2 !== handler.length) {
      throw 'Function must accept exactly 0 (sync), 1 (sync), or 2 (async) arguments';
    }

    var index, length, channelName, wh;

    for (index = 0, length = channels.length; index < length; index += 1) {
      channelName = channels[index];
      wh = new WireHandler(channelName, options, handler);

      // Count main channel handler (no wildcards, zero-priority)
      if (0 === wh.priority) {
        this.__knownChannels__[channelName] = (this.__knownChannels__[channelName] || 0) + 1;
      }

      this.__handlers__.push(wh);
    }

    // TODO: Move to separate method
    this.__sortedCache__ = [];
  };

  /**
   *  Wire#once(channel[, options], handler) -> Void
   *  - channel (String):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Same as [[Wire#on]] but runs handler one time only.
   **/
  Wire.prototype.once = function (channel, options, handler) {
    if (!handler) {
      handler = options;
      options = {};
    }

    options = options || {};
    options.once = true;

    this.on(channel, options, handler);
  };

  /**
   *  Wire#before(channel[, options], handler) -> Void
   *  - channel (String):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Same as [[Wire#on]] but with 'fixed' priority of `-10`
   **/
  Wire.prototype.before = function (channel, options, handler) {
    if (!handler) {
      handler = options;
      options = {};
    }

    options = options || {};
    options.priority = options.priority || -10;

    if (0 <= options.priority) {
      throw 'before() requires priority lower than 0';
    }

    return this.on(channel, options, handler);
  };

  /**
   *  Wire#after(channel[, options], handler) -> Void
   *  - channel (String):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Same as [[Wire#on]] but with default priority of `10`
   **/
  Wire.prototype.after = function (channel, options, handler) {
    if (!handler) {
      handler = options;
      options = {};
    }

    options = options || {};
    options.priority = options.priority || 10;

    if (0 >= options.priority) {
      throw 'after() requires priority greater than 0';
    }

    return this.on(channel, options, handler);
  };

  /**
   *  Wire#off(channel[, handler]) -> Void
   *  - channel (String):
   *  - handler (Function):
   *
   *  Removes `handler` of a channel, or removes ALL handlers of a channel if
   *  `handler` is not given.
   **/
  Wire.prototype.off = function (channel, handler) {
    var self = this;

    this.__handlers__.forEach(function (wh) {
      if (channel !== wh.channel) {
        return; // continue
      }

      if (handler && (handler !== wh.func)) {
        return; // continue
      }

      // Unkount back zero-priority handler
      if (wh.priority === 0) {
        self.__knownChannels__[channel]--;
      }
      // Just replace with dummy call, to keep cache lists intact
      wh.sync = true;
      wh.func = noop;
    });
  };

  /**
   *  Wire#skip(channel, skipList) -> Void
   *  - channel (String):
   *  - skipList (Array):
   *
   *  Exclude calling list of named handlers for given chennel:
   *
   *      wire.skip('server:static', [
   *        session_start,
   *        cookies_start
   *      ]);
   *
   **/
  Wire.prototype.skip = function (channel, skipList) {
    var self = this;

    if (channel.indexOf('*') !== -1 &&
        channel.indexOf('*') !== channel.length - 1) {
      throw 'Bad channel name \'' + channel + '\'. Broadcast symbol (*) must ' +
            'be the last character.';
    }

    if (isString(skipList)) {
      skipList = [ skipList ];
    }

    if (!isArray(skipList)) {
      throw 'skipList must be String or Array of Strings';
    }

    this.__skips__[channel] = this.__skips__[channel] || {};

    skipList.forEach(function (name) {
      if (!self.__skips__.hasOwnProperty(name)) {
        self.__skips__[name] = [];
      }

      // Add channel name only if it not exists.
      if (-1 === self.__skips__[name].indexOf(channel)) {
        self.__skips__[name].push(channel);
      }
    });

    // TODO: Move to separate method
    this.__sortedCache__ = [];
  };

  /**
   *  Wire#has(channel) -> Boolean
   *  - channel (String):
   *
   *  Returns if `channel` has at least one subscriber
   *  with zero priority (main handler)
   **/
  Wire.prototype.has = function (channel) {
    return Boolean(this.__knownChannels__[channel]);
  };

  /**
   *  Wire#stat() -> Object
   *
   *  Returns full statictics about all channels. Only channels without wildcards
   *  are displayed. Each channel has following structures:
   *
   *  ```
   *  {
   *    name: channnelName,
   *    listeners: Array[handlerStat]
   *  }
   *  ```
   **/
  Wire.prototype.stat = function () {
    var self = this
      , result = []
      , known = [];

    // Scan all unique channels, ignore priorities
    self.__handlers__.forEach(function (wh) {
      if (-1 === known.indexOf(wh.channel)) {
        known.push(wh.channel);
      }
    });
    known = known.sort();

    // Extract info
    known.forEach(function (name) {
      result.push({ name : name, listeners: self.getHandlers(name) });
    });

    return result;
  };

  //////////////////////////////////////////////////////////////////////////////

  // Node.JS
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = Wire;
  // Browser
  } else {
    root.Wire = Wire;
  }
}(this));
<commit_msg>wire style cleanup<commit_after>/**
 *  class Wire
 **/

(function (root) {
  'use strict';

  //////////////////////////////////////////////////////////////////////////////
  // Helpers

  function noop() {}

  function _class(obj) { return Object.prototype.toString.call(obj); }

  function isString(obj) { return _class(obj) === '[object String]'; }
  function isFunction(obj) { return _class(obj) === '[object Function]'; }

  //
  // Simplified stable sort implementation from Lo-Dash (http://lodash.com/)
  //

  function compareAscending(a, b) {
    var ai = a.index,
        bi = b.index;

    a = a.criteria;
    b = b.criteria;

    // ensure a stable sort in V8 and other engines
    // http://code.google.com/p/v8/issues/detail?id=90
    if (a !== b) {
      if (a > b || typeof a === 'undefined') {
        return 1;
      }
      if (a < b || typeof b === 'undefined') {
        return -1;
      }
    }

    return ai < bi ? -1 : 1;
  }

  function stableSort(collection, property) {
    var index
      , length = collection.length
      , result = new Array(length);

    for (index = 0; index < length; index += 1) {
      result[index] = {
        criteria: property ? collection[index][property] : collection[index]
      , index: index
      , value: collection[index]
      };
    }

    result.sort(compareAscending);

    for (index = 0; index < length; index += 1) {
      result[index] = result[index].value;
    }

    return result;
  }

  //////////////////////////////////////////////////////////////////////////////

  // Structure to hold handler data
  function WireHandler(channel, options, func) {
    if (channel.indexOf('*') !== -1 &&
        channel.indexOf('*') !== channel.length - 1) {
      throw 'Bad channel name \'' + channel + '\'. Broadcast symbol (*) must ' +
            'be the last character.';
    }

    this.channel   = channel;
    this.func      = func;
    this.name      = func.name || '<anonymous>';
    this.sync      = 0 === func.length || 1 === func.length;
    this.once      = Boolean(options.once);
    this.ensure    = Boolean(options.ensure);
    this.priority  = Number(options.priority || 0);
    this.ncalled   = 0;

    // Use channel name of this handler only as a prefix to match or not.
    this.isBroadcast = ('*' === channel.charAt(channel.length - 1));

    // Used to match this handler.
    this.lookupString = this.isBroadcast ? channel.slice(0, -1) : channel;
  }

  //////////////////////////////////////////////////////////////////////////////

  function Wire() {
    this.__handlers__       = [];
    this.__sortedCache__    = [];
    this.__knownChannels__  = {};
    this.__skips__          = {};
  }

  // Returns true if `handlerName` must be skipped on `channelName`.
  //
  Wire.prototype.checkSkip = function (handlerName, channelName) {
    var index, length, skip, skipBroadcast;
    var skipList = this.__skips__[handlerName] || [];

    for (index = 0, length = skipList.length; index < length; index += 1) {
      skip = skipList[index];
      skipBroadcast = ('*' === skip.charAt(skip.length - 1));

      if (!skipBroadcast && (channelName === skip)) {
        return true;
      }

      if (skipBroadcast && (0 === channelName.indexOf(skip.slice(0, -1)))) {
        return true;
      }
    }

    return false;
  };

  Wire.prototype.getHandlers = function (channel) {
    var self = this, result;

    if (!this.__sortedCache__[channel]) {
      result = [];

      this.__handlers__.forEach(function (handler) {

        // Respect skips.
        if (self.checkSkip(handler.name, channel)) {
          return;
        }

        // Non-broadcast handler - do exact match.
        if (!handler.isBroadcast && (channel !== handler.lookupString)) {
          return;
        }

        // Broadcast handler - match as a prefix.
        if (handler.isBroadcast && (0 !== channel.indexOf(handler.lookupString))) {
          return;
        }

        result.push(handler);
      });

      // We must use stable sort here to be sure, that handlers in the resulting
      // list will be placed exactly in the order they are declared.
      this.__sortedCache__[channel] = stableSort(result, 'priority');
    }

    return this.__sortedCache__[channel];
  };

  // Internal helper that runs handlers for a single channel
  function emitSingle(self, channel, params, callback) {
    var stash = self.getHandlers(channel).slice(),
        wh, fn, _err;

    // iterates through handlers of stash
    function walk(err) {

      // chain finished - exit
      if (!stash.length) {
        callback(err);
        return;
      }

      // Get next element
      wh = stash.shift();
      fn = wh.func;

      // if error - skip all handlers except 'ehshured'
      if (err && !wh.ensure) {
        walk(err);
        return;
      }

      wh.ncalled++;

      if (wh.once) {
        self.off(wh.channel, fn);
      }

      // Call handler, but protect err from overrite,
      // if already exists
      if (!wh.sync) {
        fn(params, function (_err) {
          walk(err || _err);
        });
      } else {
        _err = fn(params);
        walk(err || _err);
      }

      return;
    }

    // start stash walker
    walk();
  }

  /**
   *  Wire#emit(channels, params[, callback]) -> Void
   *  - channels (String|Array):
   *  - params (Mixed):
   *  - callback (Function):
   *
   *  Sends message with `params` into the `channel`. Once all sync and ascync
   *  handlers finished, optional `callback(err)` (if specified) fired.
   **/
  Wire.prototype.emit = function (channels, params, callback) {
    var self = this, _chs, chan;

    callback = callback || noop;

    // slightly optimize regular calls, with single channel
    if (!Array.isArray(channels)) {
      emitSingle(self, channels, params, callback);
      return;
    }

    // Lot of channel - do chaining
    _chs = channels.slice();

    function walk(err) {
      if (err || !_chs.length) {
        callback(err);
        return;
      }

      chan = _chs.shift();
      emitSingle(self, chan, params, walk);
    }

    walk();
  };

  /**
   *  Wire#on(channels[, options], handler) -> Void
   *  - channels (String | Array):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Registers `handler` to be executed upon messages in the a single channel
   *  or a sequence of channels stored in `channels` parameter. Handler can be
   *  either sync function:
   *
   *      wire.on('foobar', function () {
   *        // do stuff here
   *      });
   *
   *      wire.on('foobar', function (params) {
   *        // do stuff here
   *      });
   *
   *  Or it might be an async function with `callback(err)` second argument:
   *
   *      wire.on('foobar', function (params, callback) {
   *        // do stuff here
   *        callback(null);
   *      });
   *
   *
   *  ##### Options
   *
   *  - `priority` (Number, Default: 0)
   *  - `ensure` (Boolean, Default: false)
   *    If `true`, will run handler even if one of previous fired error.
   **/
  Wire.prototype.on = function (channels, options, handler) {
    if (!channels) {
      throw 'Channel name required. Use "**" if you want "any channel".';
    }

    if (!Array.isArray(channels)) {
      channels = [ channels ];
    }

    if (!handler) {
      handler = options;
      options = null;
    }

    options = options || {};

    if (!isFunction(handler)) {
      throw 'Listener should be the function';
    }

    if (0 !== handler.length && 1 !== handler.length && 2 !== handler.length) {
      throw 'Function must accept exactly 0 (sync), 1 (sync), or 2 (async) arguments';
    }

    channels.forEach(function (channelName) {
      var wh = new WireHandler(channelName, options, handler);

      // Count main channel handler (no wildcards, zero-priority)
      if (0 === wh.priority) {
        this.__knownChannels__[channelName] = (this.__knownChannels__[channelName] || 0) + 1;
      }

      this.__handlers__.push(wh);
    }, this);

    // TODO: Move to separate method
    this.__sortedCache__ = [];
  };

  /**
   *  Wire#once(channel[, options], handler) -> Void
   *  - channel (String):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Same as [[Wire#on]] but runs handler one time only.
   **/
  Wire.prototype.once = function (channel, options, handler) {
    if (!handler) {
      handler = options;
      options = {};
    }

    options = options || {};
    options.once = true;

    this.on(channel, options, handler);
  };

  /**
   *  Wire#before(channel[, options], handler) -> Void
   *  - channel (String):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Same as [[Wire#on]] but with 'fixed' priority of `-10`
   **/
  Wire.prototype.before = function (channel, options, handler) {
    if (!handler) {
      handler = options;
      options = {};
    }

    options = options || {};
    options.priority = options.priority || -10;

    if (0 <= options.priority) {
      throw 'before() requires priority lower than 0';
    }

    return this.on(channel, options, handler);
  };

  /**
   *  Wire#after(channel[, options], handler) -> Void
   *  - channel (String):
   *  - options (Object):
   *  - handler (Function):
   *
   *  Same as [[Wire#on]] but with default priority of `10`
   **/
  Wire.prototype.after = function (channel, options, handler) {
    if (!handler) {
      handler = options;
      options = {};
    }

    options = options || {};
    options.priority = options.priority || 10;

    if (0 >= options.priority) {
      throw 'after() requires priority greater than 0';
    }

    return this.on(channel, options, handler);
  };

  /**
   *  Wire#off(channel[, handler]) -> Void
   *  - channel (String):
   *  - handler (Function):
   *
   *  Removes `handler` of a channel, or removes ALL handlers of a channel if
   *  `handler` is not given.
   **/
  Wire.prototype.off = function (channel, handler) {

    this.__handlers__.forEach(function (wh) {
      if (channel !== wh.channel) {
        return; // continue
      }

      if (handler && (handler !== wh.func)) {
        return; // continue
      }

      // Unkount back zero-priority handler
      if (wh.priority === 0) {
        this.__knownChannels__[channel]--;
      }
      // Just replace with dummy call, to keep cache lists intact
      wh.sync = true;
      wh.func = noop;
    }, this);
  };

  /**
   *  Wire#skip(channel, skipList) -> Void
   *  - channel (String):
   *  - skipList (Array):
   *
   *  Exclude calling list of named handlers for given chennel:
   *
   *      wire.skip('server:static', [
   *        session_start,
   *        cookies_start
   *      ]);
   *
   **/
  Wire.prototype.skip = function (channel, skipList) {

    if (channel.indexOf('*') !== -1 &&
        channel.indexOf('*') !== channel.length - 1) {
      throw 'Bad channel name \'' + channel + '\'. Broadcast symbol (*) must ' +
            'be the last character.';
    }

    if (isString(skipList)) {
      skipList = [ skipList ];
    }

    if (!Array.isArray(skipList)) {
      throw 'skipList must be String or Array of Strings';
    }

    this.__skips__[channel] = this.__skips__[channel] || {};

    skipList.forEach(function (name) {
      if (!this.__skips__.hasOwnProperty(name)) {
        this.__skips__[name] = [];
      }

      // Add channel name only if it not exists.
      if (-1 === this.__skips__[name].indexOf(channel)) {
        this.__skips__[name].push(channel);
      }
    }, this);

    // TODO: Move to separate method
    this.__sortedCache__ = [];
  };

  /**
   *  Wire#has(channel) -> Boolean
   *  - channel (String):
   *
   *  Returns if `channel` has at least one subscriber
   *  with zero priority (main handler)
   **/
  Wire.prototype.has = function (channel) {
    return Boolean(this.__knownChannels__[channel]);
  };

  /**
   *  Wire#stat() -> Object
   *
   *  Returns full statictics about all channels. Only channels without wildcards
   *  are displayed. Each channel has following structures:
   *
   *  ```
   *  {
   *    name: channnelName,
   *    listeners: Array[handlerStat]
   *  }
   *  ```
   **/
  Wire.prototype.stat = function () {
    var result = [],
        known = [];

    // Scan all unique channels, ignore priorities
    this.__handlers__.forEach(function (wh) {
      if (-1 === known.indexOf(wh.channel)) {
        known.push(wh.channel);
      }
    });
    known = known.sort();

    // Extract info
    known.forEach(function (name) {
      result.push({ name : name, listeners: this.getHandlers(name) });
    }, this);

    return result;
  };

  //////////////////////////////////////////////////////////////////////////////

  // Node.JS
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = Wire;
  // Browser
  } else {
    root.Wire = Wire;
  }
}(this));
<|endoftext|>


<commit_before>var system = require('system'),
    fs = require('fs');

// check for required arguments
if (system.args.length < 2) {
    console.log('Usage: casperjs test-runner.js <casperPath> --url=<base-url> --suite=<test-suite-path> --xunit=<path-to-xml>');
    phantom.exit(64);
}

// set casperjs path
phantom.casperPath = 'vendor/casperjs';

// the first argument ist the path to casper
// hopefully this will work on windows too
if (system.args[1]) {
    phantom.casperPath = system.args[1];
}

// inject casperjs
phantom.injectJs(phantom.casperPath + '/bin/bootstrap.js');

if (!phantom.casperLoaded) {
    console.log('This script must be invoked using the casperjs executable');
    phantom.exit(1);
}

// initialize casperjs and casperjs's utils
var utils = require('utils'),
    colorizer = require('colorizer'),
    fs = require('fs'),
    f = utils.format,
    includes = [],
    tests = [],
    casper = require('casper').create({
        exitOnError: false
    });

// local utils
function checkIncludeFile(include) {
    var absInclude = fs.absolute(include.trim());
    if (!fs.exists(absInclude)) {
        casper.warn("%s file not found, can't be included", absInclude);
        return;
    }
    if (!utils.isJsFile(absInclude)) {
        casper.warn("%s is not a supported file type, can't be included", absInclude);
        return;
    }
    if (fs.isDirectory(absInclude)) {
        casper.warn("%s is a directory, can't be included", absInclude);
        return;
    }
    if (tests.indexOf(include) > -1 || tests.indexOf(absInclude) > -1) {
        casper.warn("%s is a test file, can't be included", absInclude);
        return;
    }
    return absInclude;
}

// parse some options from cli
casper.options.verbose = casper.cli.get('direct') || false;
casper.options.logLevel = casper.cli.get('log-level') || "error";
if (casper.cli.get('no-colors')) {
    var cls = 'Dummy';
    casper.options.colorizerType = cls;
    casper.colorizer = colorizer.create(cls);
}
var test = [];
if (casper.cli.get('suite')) {
    tests.push(casper.cli.get('suite'));
} else {
    casper.echo('No test path passed, exiting.', 'RED_BAR', 80);
    casper.exit(1);
}

// includes handling
if (casper.cli.has('includes')) {
    includes = casper.cli.get('includes').split(',').map(function(include) {
        // we can't use filter() directly because of abspath transformation
        return checkIncludeFile(include);
    }).filter(function(include) {
        return utils.isString(include);
    });
    casper.test.includes = utils.unique(includes);
}

// set base url for usage in tests
casper.test.baseUrl = casper.cli.get('url');

// test suites completion listener
casper.test.on('tests.complete', function() {
    this.renderResults(true, undefined, casper.cli.get('xunit') || undefined);
});

var shoot = function (count, result) {
    result = (result) ? 'success' : 'fail';
    console.log(casper.cli.get('screens') + '/screen_' + count + '_' + result + '.png');
    casper.capture(casper.cli.get('screens') + '/screen_' + count + '_' + result + '.png', {
        top: 0,
        left: 0,
        width: 1200,
        height: 800
    });
};

if (casper.cli.has('screens')) {
    var count = 1;
    casper.test.on('success', function() {
        shoot(count, true);
        count++;
    });
    casper.test.on('fail', function() {
        shoot(count, false);
        count++;
    });
}

// run all the suites
casper.test.runSuites.apply(casper.test, tests);
<commit_msg>adde screenshot.do event<commit_after>var system = require('system'),
    fs = require('fs');

// check for required arguments
if (system.args.length < 2) {
    console.log('Usage: casperjs test-runner.js <casperPath> --url=<base-url> --suite=<test-suite-path> --xunit=<path-to-xml>');
    phantom.exit(64);
}

// set casperjs path
phantom.casperPath = 'vendor/casperjs';

// the first argument ist the path to casper
// hopefully this will work on windows too
if (system.args[1]) {
    phantom.casperPath = system.args[1];
}

// inject casperjs
phantom.injectJs(phantom.casperPath + '/bin/bootstrap.js');

if (!phantom.casperLoaded) {
    console.log('This script must be invoked using the casperjs executable');
    phantom.exit(1);
}

// initialize casperjs and casperjs's utils
var utils = require('utils'),
    colorizer = require('colorizer'),
    fs = require('fs'),
    f = utils.format,
    includes = [],
    tests = [],
    casper = require('casper').create({
        exitOnError: false
    });

// local utils
function checkIncludeFile(include) {
    var absInclude = fs.absolute(include.trim());
    if (!fs.exists(absInclude)) {
        casper.warn("%s file not found, can't be included", absInclude);
        return;
    }
    if (!utils.isJsFile(absInclude)) {
        casper.warn("%s is not a supported file type, can't be included", absInclude);
        return;
    }
    if (fs.isDirectory(absInclude)) {
        casper.warn("%s is a directory, can't be included", absInclude);
        return;
    }
    if (tests.indexOf(include) > -1 || tests.indexOf(absInclude) > -1) {
        casper.warn("%s is a test file, can't be included", absInclude);
        return;
    }
    return absInclude;
}

// parse some options from cli
casper.options.verbose = casper.cli.get('direct') || false;
casper.options.logLevel = casper.cli.get('log-level') || "error";
// set higher timeouts
casper.options.stepTimeout = 10000;
casper.options.timeout = 10000;
casper.defaultWaitForTimeout = 10000;

if (casper.cli.get('no-colors')) {
    var cls = 'Dummy';
    casper.options.colorizerType = cls;
    casper.colorizer = colorizer.create(cls);
}
var test = [];
if (casper.cli.get('suite')) {
    tests.push(casper.cli.get('suite'));
} else {
    casper.echo('No test path passed, exiting.', 'RED_BAR', 80);
    casper.exit(1);
}

// includes handling
if (casper.cli.has('includes')) {
    includes = casper.cli.get('includes').split(',').map(function(include) {
        // we can't use filter() directly because of abspath transformation
        return checkIncludeFile(include);
    }).filter(function(include) {
        return utils.isString(include);
    });
    casper.test.includes = utils.unique(includes);
}

// set base url for usage in tests
casper.test.baseUrl = casper.cli.get('url');

// test suites completion listener
casper.test.on('tests.complete', function() {
    this.renderResults(true, undefined, casper.cli.get('xunit') || undefined);
});

var shoot = function (count, result) {
    casper.capture(casper.cli.get('screens') + '/screen_' + count + '_' + result + '.png', {
        top: 0,
        left: 0,
        width: 1200,
        height: 800
    });
};

if (casper.cli.has('screens')) {
    var count = 1;
    casper.test.on('success', function() {
        shoot(count, 'success');
        count++;
    });
    casper.test.on('fail', function() {
        shoot(count, 'fail');
        count++;
    });
    casper.on('screenshot.do', function() {
        shoot(count, 'user');
        count++;
    });
}

// run all the suites
casper.test.runSuites.apply(casper.test, tests);
<|endoftext|>


<commit_before>var parinfer = require("../parinfer.js");
var assert = require("assert");

describe("Respecting CRLF line-endings", function(){
  it("indent mode", function(){
    var textIn = "(foo\r\nbar";
    var textExpected = "(foo)\r\nbar";
    var textOut = parinfer.indentMode(textIn).text;
    assert.strictEqual(textOut, textExpected);
  });
  it("paren mode", function(){
    var textIn = "(foo\r\nbar)";
    var textExpected = "(foo\r\n bar)";
    var textOut = parinfer.parenMode(textIn).text;
    assert.strictEqual(textOut, textExpected);
  });
});

function testErrorName(mode, textIn, errorName, lineNo, x) {
  var result = mode(textIn);
  assert.strictEqual(result.success, false);
  assert.strictEqual(result.error.name, errorName);
  assert.strictEqual(result.error.lineNo, lineNo);
  assert.strictEqual(result.error.x, x);
}

describe("Returning appropriate errors", function(){
  it("unclosed-quote", function(){
    testErrorName(parinfer.indentMode, '(foo"', "unclosed-quote", 0, 4);
    testErrorName(parinfer.parenMode, '(foo"', "unclosed-quote", 0, 4);
  });
  it("unclosed-paren", function(){
    testErrorName(parinfer.parenMode, '(foo', "unclosed-paren", 0, 0);
  });
  it("quote-danger", function(){
    testErrorName(parinfer.parenMode, '; "foo', "quote-danger", 0, 2);
  });
  it("eol-backslash", function(){
    testErrorName(parinfer.parenMode, '(foo \\', "eol-backslash", 0, 5);
  });
});

describe("Returning changed lines", function(){
  it("indent mode: single change", function(){
    var textIn = "(foo\nbar";
    var actual = parinfer.indentMode(textIn).changedLines;
    var expected = [
      { lineNo: 0, line: "(foo)" }
    ];
    assert.deepStrictEqual(actual, expected);
  });
  it("indent mode: multiple changes", function(){
    var textIn = "(foo\nbar)";
    var actual = parinfer.indentMode(textIn).changedLines;
    var expected = [
      { lineNo: 0, line: "(foo)" },
      { lineNo: 1, line: "bar" },
    ];
    assert.deepStrictEqual(actual, expected);
  });
  it("paren mode: single change", function(){
    var textIn = "(foo\nbar)";
    var actual = parinfer.parenMode(textIn).changedLines;
    var expected = [
      { lineNo: 1, line: " bar)" },
    ];
    assert.deepStrictEqual(actual, expected);
  });
  it("paren mode: multiple changes", function(){
    var textIn = "(foo]\nbar)";
    var actual = parinfer.parenMode(textIn).changedLines;
    var expected = [
      { lineNo: 0, line: "(foo" },
      { lineNo: 1, line: " bar)" },
    ];
    assert.deepStrictEqual(actual, expected);
  });
});
<commit_msg>test: reformat extra test cases for readability<commit_after>var parinfer = require("../parinfer.js");
var assert = require("assert");

var modeFn = {
  indent: parinfer.indentMode,
  paren: parinfer.parenMode
};

function checkMode(mode, textIn, textExpected) {
  var textOut = modeFn[mode](textIn).text;
  assert.strictEqual(textOut, textExpected);
}

describe("Respecting CRLF line-endings", function(){
  it("indent mode", function(){ checkMode('indent', "(foo\r\nbar", "(foo)\r\nbar"); });
  it("paren mode",  function(){ checkMode('paren',  "(foo\r\nbar)", "(foo\r\n bar)"); });
});

function checkError(mode, textIn, errorName, lineNo, x) {
  var result = modeFn[mode](textIn);
  assert.strictEqual(result.success, false);
  assert.strictEqual(result.error.name, errorName);
  assert.strictEqual(result.error.lineNo, lineNo);
  assert.strictEqual(result.error.x, x);
}

describe("Returning appropriate errors", function(){
  it("unclosed-quote", function(){ checkError('indent', '(foo"',   "unclosed-quote", 0, 4);
                                   checkError('paren',  '(foo"',   "unclosed-quote", 0, 4); });
  it("unclosed-paren", function(){ checkError('paren',  '(foo',    "unclosed-paren", 0, 0); });
  it("quote-danger",   function(){ checkError('paren',  '; "foo',  "quote-danger",   0, 2); });
  it("eol-backslash",  function(){ checkError('paren',  '(foo \\', "eol-backslash",  0, 5); });
});

function checkLines(mode, text, changedLines) {
  var result = modeFn[mode](text);
  assert.deepStrictEqual(result.changedLines, changedLines);
}

describe("Returning changed lines", function(){
  it("indent mode: single change",    function(){ checkLines('indent', "(foo\nbar",   [{lineNo: 0, line: "(foo)"}]); });
  it("indent mode: multiple changes", function(){ checkLines('indent', "(foo\nbar)",  [{lineNo: 0, line: "(foo)"},
                                                                                {lineNo: 1, line: "bar"}]); });
  it("paren mode: single change",     function(){ checkLines('paren',  "(foo\nbar)",  [{lineNo: 1, line: " bar)"}]); });
  it("paren mode: multiple changes",  function(){ checkLines('paren',  "(foo]\nbar)", [{lineNo: 0, line: "(foo"},
                                                                                {lineNo: 1, line: " bar)"}]); });
});
<|endoftext|>


<commit_before>/**
 * @fileoverview A parser for command line options.
 */

require("core/string");

/**
 * Create a new command line option parser.
 */
exports.Parser = function() {
    var options = [];

    /**
     * Add an option to the parser.
     * @param shortName the short option name (without leading hyphen)
     * @param longName the long option name (without leading hyphens)
     * @param argument the name of the argument if the option requires one, or null
     * @param helpText the help text to display for the option
     */
    this.addOption = function(shortName, longName, argument, helpText) {
        if (shortName && shortName.length != 1) {
            throw new Error("Short option must be a string of length 1");
        }
        argument = argument || "";
        options.push([shortName, longName, argument, helpText]);
    };

    /**
     * Get help text for the parser's options suitable for display in command line scripts.
     */
    this.help = function() {
        var help = [];
        for each (var opt in options) {
            var line = " -" + opt[0] +  " --" + opt[1];
            if (opt[2]) {
                line += " " + opt[2];
            }
            help.push([line, opt[3]]);
        }
        var maxlength = help.map(function(s) { return s[0].length; }).max();
        return help.map(function(s) {
            var padding = " ".repeat(2 + maxlength - s[0].length);
            return s[0] + padding + s[1];
        }).join('\n');
    };

    /**
     * Parse an arguments array into an option object.
     * @param args the argument array. Matching options are removed.
     * @param result optional result object. If undefined, a new Object is created
     * @return the result object
     */
    this.parse = function(args, result) {
        result = result || {};
        while (args.length > 0) {
            var option = args[0];
            if (!option.startsWith("-")) {
                break;
            }
            if (option.startsWith("--")) {
                parseLongOption(option.substring(2), args, result);
            } else {
                parseShortOption(option.substring(1), args, result);
            }
        }
        return result;
    };

    function parseShortOption(opt, args, result) {
        var length = opt.length;
        var consumedNext = false;
        for (var i = 0; i < length; i++) {
            var def = null;
            var c = opt.charAt(i);
            for each (var d in options) {
                if (d[0] == c) {
                    def = d;
                    break;
                }
            }
            if (def == null) {
                unknownOptionError("-" + c);
            }
            var optarg = null;
            if (def[2]) {
                if (i == length - 1) {
                    if (args.length <= 1) {
                        missingValueError("-" + def[0]);
                    }
                    optarg = args[1];
                    consumedNext = true;
                } else {
                    optarg = opt.substring(i + 1);
                    if (optarg.length == 0) {
                        missingValueError("-" + def[0]);
                    }
                }
                i = length;
            }
            result[def[1]] = optarg || true;
        }
        args.splice(0, consumedNext ? 2 : 1);
    }

    function parseLongOption(opt, args, result) {
        var def = null;
        for each (var d in options) {
            if (opt == d[1] || (opt.startsWith(d[1])
                    && opt.charAt(d[1].length) == '=')) {
                def = d;
                break;
            }
        }
        if (def == null) {
            unknownOptionError("--" + opt);
        }
        var optarg = null;
        var consumedNext = false;
        if (def[2]) {
            if (opt == def[1]) {
                if (args.length <= 1) {
                    missingValueError("--" + def[1]);
                }
                optarg = args[1];
                consumedNext = true;
            } else {
                var length = def[1].length;
                if (opt.charAt(length) != '=') {
                    missingValueError("--" + def[1]);
                }
                optarg = opt.substring(length + 1);
            }
        }
        result[def[1]] = optarg || true;
        args.splice(0, consumedNext ? 2 : 1);
    }
};

function missingValueError(option) {
    throw new Error(option + " option requires a value.", -1);
}

function unknownOptionError(option) {
    throw new Error("Unknown option: " + option, -1);
}
<commit_msg>Add missing import statement for core/array<commit_after>/**
 * @fileoverview A parser for command line options.
 */

require("core/array");
require("core/string");

/**
 * Create a new command line option parser.
 */
exports.Parser = function() {
    var options = [];

    /**
     * Add an option to the parser.
     * @param shortName the short option name (without leading hyphen)
     * @param longName the long option name (without leading hyphens)
     * @param argument the name of the argument if the option requires one, or null
     * @param helpText the help text to display for the option
     */
    this.addOption = function(shortName, longName, argument, helpText) {
        if (shortName && shortName.length != 1) {
            throw new Error("Short option must be a string of length 1");
        }
        argument = argument || "";
        options.push([shortName, longName, argument, helpText]);
    };

    /**
     * Get help text for the parser's options suitable for display in command line scripts.
     */
    this.help = function() {
        var help = [];
        for each (var opt in options) {
            var line = " -" + opt[0] +  " --" + opt[1];
            if (opt[2]) {
                line += " " + opt[2];
            }
            help.push([line, opt[3]]);
        }
        var maxlength = help.map(function(s) { return s[0].length; }).max();
        return help.map(function(s) {
            var padding = " ".repeat(2 + maxlength - s[0].length);
            return s[0] + padding + s[1];
        }).join('\n');
    };

    /**
     * Parse an arguments array into an option object.
     * @param args the argument array. Matching options are removed.
     * @param result optional result object. If undefined, a new Object is created
     * @return the result object
     */
    this.parse = function(args, result) {
        result = result || {};
        while (args.length > 0) {
            var option = args[0];
            if (!option.startsWith("-")) {
                break;
            }
            if (option.startsWith("--")) {
                parseLongOption(option.substring(2), args, result);
            } else {
                parseShortOption(option.substring(1), args, result);
            }
        }
        return result;
    };

    function parseShortOption(opt, args, result) {
        var length = opt.length;
        var consumedNext = false;
        for (var i = 0; i < length; i++) {
            var def = null;
            var c = opt.charAt(i);
            for each (var d in options) {
                if (d[0] == c) {
                    def = d;
                    break;
                }
            }
            if (def == null) {
                unknownOptionError("-" + c);
            }
            var optarg = null;
            if (def[2]) {
                if (i == length - 1) {
                    if (args.length <= 1) {
                        missingValueError("-" + def[0]);
                    }
                    optarg = args[1];
                    consumedNext = true;
                } else {
                    optarg = opt.substring(i + 1);
                    if (optarg.length == 0) {
                        missingValueError("-" + def[0]);
                    }
                }
                i = length;
            }
            result[def[1]] = optarg || true;
        }
        args.splice(0, consumedNext ? 2 : 1);
    }

    function parseLongOption(opt, args, result) {
        var def = null;
        for each (var d in options) {
            if (opt == d[1] || (opt.startsWith(d[1])
                    && opt.charAt(d[1].length) == '=')) {
                def = d;
                break;
            }
        }
        if (def == null) {
            unknownOptionError("--" + opt);
        }
        var optarg = null;
        var consumedNext = false;
        if (def[2]) {
            if (opt == def[1]) {
                if (args.length <= 1) {
                    missingValueError("--" + def[1]);
                }
                optarg = args[1];
                consumedNext = true;
            } else {
                var length = def[1].length;
                if (opt.charAt(length) != '=') {
                    missingValueError("--" + def[1]);
                }
                optarg = opt.substring(length + 1);
            }
        }
        result[def[1]] = optarg || true;
        args.splice(0, consumedNext ? 2 : 1);
    }
};

function missingValueError(option) {
    throw new Error(option + " option requires a value.", -1);
}

function unknownOptionError(option) {
    throw new Error("Unknown option: " + option, -1);
}
<|endoftext|>


<commit_before>import Vue from 'vue'
import VueGtag from 'vue-gtag'

export default ({ $config, app }, inject) => {
  const options = {
    config: {
      id: $config.googleAnalyticsTrackingId,
      params: {
        // cookieDomain: 'none',
        anonymize_ip: true
      }
    },
    enabled: false
  }
  inject('gtag', Vue.use(VueGtag, options, app.router))
}
<commit_msg>Remove unnecessary some lines<commit_after>import Vue from 'vue'
import VueGtag from 'vue-gtag'

export default ({ $config, app }, inject) => {
  const options = {
    config: {
      id: $config.googleAnalyticsTrackingId,
      params: {
        anonymize_ip: true
      }
    },
    enabled: false
  }
  inject('gtag', Vue.use(VueGtag, options, app.router))
}
<|endoftext|>


<commit_before>var exec = require('child_process').exec,
	sprintf = require('sprintf-js').sprintf,
	_ = require('lodash'),
	fs = require('fs'),
	config = require('./config').Config;

/**
 * Initialize the Thumbnailer
 */
function Thumbnailer(opts) {
	// for the benefit of testing
	// perform dependency injection.
	_.extend(this, {
		tmp: require('tmp'),
		logger: require('./logger')
	}, opts);
}

/**
 * Execute the image conversion command
 *
 * @param object description The job description
 * @param localPath The local path to the image
 * @param function onComplete The callback function
 */
Thumbnailer.prototype.execute = function(description, localPaths, onComplete) {
	var _this = this;

	// Convert single path to array
	if (!_.isArray(localPaths)) {
		localPaths = [localPaths];
	}

	// parameters for a single execution
	// of the thumbnailer.
	_.extend(this, {
		localPaths: localPaths,
		width: description.width,
		height: description.height,
		format: (description.format || 'jpg'),
		strategy: (description.strategy || 'bounded'),
		background: (description.background || 'black'),
		autoOrient: (description.autoOrient || null),
		quality: (description.quality || null),
		command: (description.command || config.get('convertCommand')),
		onComplete: onComplete,
		thumbnailTimeout: 20000
	});

	this.createConversionPath(function(err) {

		if (err) {
			_this.onComplete(err);
			return;
		}

		var strategy = _this._guessStrategy();
		if (strategy) _this[strategy]();
	});
};

/**
 * Choose an appropriate image manipulation
 * strategy, based on 'strategy' key in job.
 * If the strategy contains, %(command)s, assume
 * manual strategy:
 *
 * "%(command)s -border 0 -tile 3x1 -geometry 160x106 "%(localPaths[0])s" "%(localPaths[1])s" "%(localPaths[2])s" -quality 90 %(convertedPath)s"
 *
 * @return string strategy to execute.
 * @throw strategy not found.
 */
Thumbnailer.prototype._guessStrategy = function() {
	if (this.strategy.match(/%\(.*\)s/)) {
		return 'manual';
	} else if (!this[this.strategy]) {
		this.onComplete(Error('could not find strategy ' + this.strategy));
	} else {
		return this.strategy;
	}
};

/**
 * Create a temp file for the converted image
 *
 * @param function callback The callback function
 */
Thumbnailer.prototype.createConversionPath = function(callback) {
	var _this = this;

	this.tmp.file({dir: config.get('tmpDir'), postfix: "." + this.format}, function(err, convertedPath, fd) {
		fs.closeSync(fd); // close immediately, we do not use this file handle.
		_this.convertedPath = convertedPath;
		callback(err);
	});
};

/**
 * Execute the conversion command
 *
 * @param string command The command
 */
Thumbnailer.prototype.execCommand = function(command) {
	var _this = this;

	exec(command, {timeout: this.thumbnailTimeout}, function(err, stdout, stderr) {

		_this.logger.info('running command ', command);

		if (err) {
			_this.onComplete(err);
			return;
		}

		// make sure the conversion was successful.
		fs.stat(_this.convertedPath, function(err, stat) {
			if (err || stat.size === 0) {
				err =  'zero byte thumbnail created';
				_this.onComplete(err);
				return;
			}
			_this.onComplete(null, _this.convertedPath);
		});

	});
};

/**
 * Convert the image using the manual strategy.
 * looks for a strategy of the form:
 *
 * "%(command)s -border 0 -tile 3x1 -geometry 160x106 '%(localPath[0])s' '%(localPath[1])s' '%(localPath[2])s' -quality 90 %(convertedPath)s
 *
 * The custom strategy has access to all variables set on
 * the thumbnailer instance:
 *   * command: the conversion command to run.
 *   * localPaths: the local temp images to apply operation to.
 *   * convertedPath: path to store final thumbnail to on S3.
 */
Thumbnailer.prototype.manual = function() {
	try {
		var thumbnailCommand = sprintf(this.strategy, this);
		this.execCommand(thumbnailCommand);
	} catch (err) {
		this.onComplete(err);
	}
};

/**
 * Convert the image using the matted strategy
 */
Thumbnailer.prototype.matted = function() {
	var dimensionsString = this.width + 'X' + this.height,
	  qualityString = (this.quality ? '-quality ' + this.quality : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + autoOrient + ' -resize ' + dimensionsString + ' -size ' + dimensionsString + ' xc:' + this.background + ' +swap -gravity center' + qualityString + ' -composite ' + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

/**
 * Convert the image using the bounded strategy
 */
Thumbnailer.prototype.bounded = function() {
	var dimensionsString = this.width + 'X' + this.height,
		qualityString = (this.quality ? '-quality ' + this.quality + ' ' : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + autoOrient + ' -thumbnail ' + dimensionsString + ' ' + qualityString + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

/**
 * Convert the image using the fill strategy
 */
Thumbnailer.prototype.fill = function() {
	var dimensionsString = this.width + 'X' + this.height,
		qualityString = (this.quality ? '-quality ' + this.quality : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + autoOrient + ' -resize ' + dimensionsString + '^ -gravity center -extent ' + dimensionsString + ' ' + qualityString + ' ' + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

/**
 * Convert the image using the strict strategy
 */
Thumbnailer.prototype.strict = function() {
	var dimensionsString = this.width + 'X' + this.height,
		qualityString = (this.quality ? '-quality ' + this.quality : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + autoOrient + ' -resize ' + dimensionsString + '! ' + qualityString + ' ' + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

exports.Thumbnailer = Thumbnailer;
<commit_msg>Added -colorspace option (defaults to rgb), prevents inverting of CMYK JPEGs<commit_after>var exec = require('child_process').exec,
	sprintf = require('sprintf-js').sprintf,
	_ = require('lodash'),
	fs = require('fs'),
	config = require('./config').Config;

/**
 * Initialize the Thumbnailer
 */
function Thumbnailer(opts) {
	// for the benefit of testing
	// perform dependency injection.
	_.extend(this, {
		tmp: require('tmp'),
		logger: require('./logger')
	}, opts);
}

/**
 * Execute the image conversion command
 *
 * @param object description The job description
 * @param localPath The local path to the image
 * @param function onComplete The callback function
 */
Thumbnailer.prototype.execute = function(description, localPaths, onComplete) {
	var _this = this;

	// Convert single path to array
	if (!_.isArray(localPaths)) {
		localPaths = [localPaths];
	}

	// parameters for a single execution
	// of the thumbnailer.
	_.extend(this, {
		localPaths: localPaths,
		width: description.width,
		height: description.height,
		format: (description.format || 'jpg'),
		strategy: (description.strategy || 'bounded'),
		background: (description.background || 'black'),
		autoOrient: (description.autoOrient || null),
		colorSpace: (description.colorSpace || 'rgb'),
		quality: (description.quality || null),
		command: (description.command || config.get('convertCommand')),
		onComplete: onComplete,
		thumbnailTimeout: 20000
	});

	this.createConversionPath(function(err) {

		if (err) {
			_this.onComplete(err);
			return;
		}

		var strategy = _this._guessStrategy();
		if (strategy) _this[strategy]();
	});
};

/**
 * Choose an appropriate image manipulation
 * strategy, based on 'strategy' key in job.
 * If the strategy contains, %(command)s, assume
 * manual strategy:
 *
 * "%(command)s -border 0 -tile 3x1 -geometry 160x106 "%(localPaths[0])s" "%(localPaths[1])s" "%(localPaths[2])s" -quality 90 %(convertedPath)s"
 *
 * @return string strategy to execute.
 * @throw strategy not found.
 */
Thumbnailer.prototype._guessStrategy = function() {
	if (this.strategy.match(/%\(.*\)s/)) {
		return 'manual';
	} else if (!this[this.strategy]) {
		this.onComplete(Error('could not find strategy ' + this.strategy));
	} else {
		return this.strategy;
	}
};

/**
 * Create a temp file for the converted image
 *
 * @param function callback The callback function
 */
Thumbnailer.prototype.createConversionPath = function(callback) {
	var _this = this;

	this.tmp.file({dir: config.get('tmpDir'), postfix: "." + this.format}, function(err, convertedPath, fd) {
		fs.closeSync(fd); // close immediately, we do not use this file handle.
		_this.convertedPath = convertedPath;
		callback(err);
	});
};

/**
 * Execute the conversion command
 *
 * @param string command The command
 */
Thumbnailer.prototype.execCommand = function(command) {
	var _this = this;

	exec(command, {timeout: this.thumbnailTimeout}, function(err, stdout, stderr) {

		_this.logger.info('running command ', command);

		if (err) {
			_this.onComplete(err);
			return;
		}

		// make sure the conversion was successful.
		fs.stat(_this.convertedPath, function(err, stat) {
			if (err || stat.size === 0) {
				err =  'zero byte thumbnail created';
				_this.onComplete(err);
				return;
			}
			_this.onComplete(null, _this.convertedPath);
		});

	});
};

/**
 * Convert the image using the manual strategy.
 * looks for a strategy of the form:
 *
 * "%(command)s -border 0 -tile 3x1 -geometry 160x106 '%(localPath[0])s' '%(localPath[1])s' '%(localPath[2])s' -quality 90 %(convertedPath)s
 *
 * The custom strategy has access to all variables set on
 * the thumbnailer instance:
 *   * command: the conversion command to run.
 *   * localPaths: the local temp images to apply operation to.
 *   * convertedPath: path to store final thumbnail to on S3.
 */
Thumbnailer.prototype.manual = function() {
	try {
		var thumbnailCommand = sprintf(this.strategy, this);
		this.execCommand(thumbnailCommand);
	} catch (err) {
		this.onComplete(err);
	}
};

/**
 * Convert the image using the matted strategy
 */
Thumbnailer.prototype.matted = function() {
	var dimensionsString = this.width + 'X' + this.height,
	  qualityString = (this.quality ? '-quality ' + this.quality : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		colorSpace = (this.colorSpace ? '-colorspace ' + this.colorSpace : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + colorSpace + ' ' + autoOrient + ' -resize ' + dimensionsString + ' -size ' + dimensionsString + ' xc:' + this.background + ' +swap -gravity center' + qualityString + ' -composite ' + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

/**
 * Convert the image using the bounded strategy
 */
Thumbnailer.prototype.bounded = function() {
	var dimensionsString = this.width + 'X' + this.height,
		qualityString = (this.quality ? '-quality ' + this.quality + ' ' : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		colorSpace = (this.colorSpace ? '-colorspace ' + this.colorSpace : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + colorSpace + ' ' + autoOrient + ' -thumbnail ' + dimensionsString + ' ' + qualityString + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

/**
 * Convert the image using the fill strategy
 */
Thumbnailer.prototype.fill = function() {
	var dimensionsString = this.width + 'X' + this.height,
		qualityString = (this.quality ? '-quality ' + this.quality : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		colorSpace = (this.colorSpace ? '-colorspace ' + this.colorSpace : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + colorSpace + ' ' + autoOrient + ' -resize ' + dimensionsString + '^ -gravity center -extent ' + dimensionsString + ' ' + qualityString + ' ' + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

/**
 * Convert the image using the strict strategy
 */
Thumbnailer.prototype.strict = function() {
	var dimensionsString = this.width + 'X' + this.height,
		qualityString = (this.quality ? '-quality ' + this.quality : ''),
		autoOrient = (this.autoOrient ? '-auto-orient' : ''),
		colorSpace = (this.colorSpace ? '-colorspace ' + this.colorSpace : ''),
		thumbnailCommand = config.get('convertCommand') + ' "' + this.localPaths[0] + '[0]" ' + colorSpace + ' ' + autoOrient + ' -resize ' + dimensionsString + '! ' + qualityString + ' ' + this.convertedPath;

	this.execCommand(thumbnailCommand);
};

exports.Thumbnailer = Thumbnailer;
<|endoftext|>