module.exports = function(url, options) { options = options || {}; options.name = options.name || pkg.name; options.info = options.info || function(msg) { console.info('[' + new Date + '] [info] Daemon "' + options.name + '" :: ' + msg); }; options.error = options.error || function(msg) { console.error('[' + new Date + '] [error] Daemon "' + options.name + '" :: ' + msg); }; options.debug = options.debug || function(msg) { console.log('[' + new Date + '] [debug] Daemon "' + options.name + '" :: ' + msg); }; return _.through(function(source) { return source .on('data', function(data) { if (data && data.type === 'log') { var level = data.level || 'info'; if (typeof options[level] !== 'function') { options.error('Cannot log with level ' + level); return; } options[level](data.message); } }) .on('error', function(data) { options.error(JSON.stringify(data)); }); }); };
module.exports = function masseur(url, options) { options = options || {}; var couch = nano(url); var config = {}; return _.pipeline( // gather configs _.map(function(data) { if (data && data.stream === 'compile') { Object.keys(data.doc).forEach(function(key) { config[data.id + '/' + key] = data.doc[key]; }); } return data; }), // process docs _.through(function(source) { return _(function(push, done) { source.on('data', function(data) { if (data && data.stream === 'changes' && data.doc && data.db_name) { async.eachSeries(Object.keys(config), function(key, next) { try { config[key](data.doc, couch.use(data.db_name), next); } catch(e) { push(null, { type: 'log', message: 'Error running ' + key + ': ' + e }); } }, function(err, res) { push(null, { db_name: data.db_name, seq: data.seq, type: 'log', message: 'Complete: ' + data.db_name + '/' + data.doc._id }); }); } }); source.on('error', push); source.on('end', done); }); }) ); };
module.exports = function couchmagick(url, options) { options = options || {}; options.concurrency = options.concurrency || 1; options.timeout = options.timeout || 60 * 1000; // 1 minute var couch = nano(url); var configs = {}; // convert attachment // TODO: process // * configs // * attachments // * versions // in one go. Don't do the through split pipeline stuff. function convertAttachment(data, callback) { var db = couch.use(data.db_name); // get target doc db.get(data.target.id, function(err, doc) { data.target.doc = doc || { _id: data.target.id }; data.target.doc.couchmagick = data.target.doc.couchmagick || {}; data.target.doc.couchmagick[data.target.id] = data.target.doc.couchmagick[data.target.id] || {}; // do not process attachments twice, respect revpos if (data.target.doc.couchmagick[data.target.id][data.target.name] && data.target.doc.couchmagick[data.target.id][data.target.name].revpos === data.source.revpos) { return callback(null, data); } // insert couchmagick stamp data.target.doc.couchmagick[data.target.id][data.target.name] = { id: data.source.id, name: data.source.name, revpos: data.source.revpos }; // query params, doc_name is used by nano as id var params = { doc_name: data.target.id }; if (data.target.doc._rev) { params.rev = data.target.doc._rev; } // attachment multipart part var attachment = { name: data.target.name, content_type: data.target.content_type, data: [] }; // convert process var c = spawn('convert', data.args); // collect errors var cerror = []; c.stderr.on('data', function(err) { cerror.push(err); }); // convert timeout var kill = setTimeout(function() { cerror.push(new Buffer('timeout')); // send SIGTERM c.kill(); }, options.timeout); // collect output c.stdout.on('data', function(data) { attachment.data.push(data); }); // concat output c.stdout.on('end', function() { clearTimeout(kill); attachment.data = Buffer.concat(attachment.data); }); // convert finish c.on('close', function(code) { // store exit code data.code = code; data.target.doc.couchmagick[data.target.id][data.target.name].code = data.code; if (code === 0) { // no error: make multipart request return db.multipart.insert(data.target.doc, [attachment], params, function(err, response) { if (err) { return callback(err); } data.response = response; if (response.rev) { data.target.rev = response.rev; } callback(null, data); }); } // store error data.error = Buffer.concat(cerror).toString(); data.target.doc.couchmagick[data.target.id][data.target.name].error = data.error; // store document stup, discard attachment db.insert(data.target.doc, data.target.id, function(err, response) { if (err) { return callback(err); } data.response = response; if (response.rev) { data.target.rev = response.rev; } callback(null, data); }); }); // request attachment and pipe it into convert process db.attachment.get(data.source.id, data.source.name).pipe(c.stdin); }); } // processing queue var convert = async.queue(convertAttachment, options.concurrency); return _.pipeline( // gather configs _.map(function(data) { if (data.stream === 'compile') { var cfg = {}; cfg[data.id] = data.doc; _.extend(cfg, configs); } return data; }), // Decide whether a whole doc needs processing at all _.filter(function(data) { if (!data.doc) { return false; } if (!data.doc._attachments) { return false; } if (!Object.keys(data.doc._attachments).length) { return false; } return true; }), // split stream into each config // TODO: this prevents us from supporting multiple attachments per document // and therefore needs serialisation _.map(function(data) { return Object.keys(configs).map(function(config) { return { db_name: data.db_name, seq: data.seq, doc: data.doc, config: configs[config] }; }); }), _.flatten(), // Decide if couchmagick should be run on a specific attachment _.filter(function(data) { if (typeof data.config.filter === 'function' && !data.config.filter(data.doc)) { return false; } return true; }), // split stream into attachments // TODO: this prevents us from supporting multiple attachments per document // and therefore needs serialisation _.map(function(data) { return Object.keys(data.doc._attachments).map(function(name) { return { db_name: data.db_name, seq: data.seq, doc: data.doc, config: data.config, name: name }; }); }), _.flatten(), // filter attachments with builtin _.filter(function(data) { if (!data.doc) { return false; } if (!data.name) { return false; } return data.doc._attachments[data.name].content_type.match(/^image\//); }), // split stream into versions // TODO: this prevents us from supporting multiple attachments per document // and therefore needs serialisation _.map(function(data) { return Object.keys(data.config.versions).map(function(key) { var version = data.config.versions[key]; // version defaults version.id = version.id || '{id}/{version}'; version.name = version.name || '{basename}-{version}{extname}'; version.content_type = version.content_type || 'image/jpeg'; version.args = version.args || []; // first arg is input pipe if (!version.args[0] || version.args[0] !== '-') { version.args.unshift('-'); } // last arg is output pipe if (version.args.length < 2 || !version.args[version.args.length - 1].match(/^[a-z]{0,3}:-$/)) { version.args.push('jpg:-'); } // run version filter if (typeof version.filter === 'function' && !version.filter(data.doc, data.name)) { return; } // construct target doc var id = strformat(version.id, { id: data.doc._id, docuri: docuri.parse(data.doc._id), parts: data.doc._id.split('/'), version: key }); var name = strformat(version.name, { id: data.doc._id, docuri: docuri.parse(data.doc._id), parts: data.doc._id.split('/'), version: key, name: data.name, extname: path.extname(data.name), basename: path.basename(data.name, path.extname(data.name)), dirname: path.dirname(data.name) }); return { db_name: data.db_name, seq: data.seq, source: { id: data.doc._id, name: data.name, revpos: data.doc._attachments[data.name].revpos, couchmagick: data.doc.couchmagick }, args: version.args, target: { id: id, name: name, content_type: version.content_type } }; }); }), _.flatten(), // filter derived versions to prevent cascades // eg: // single-attachment/thumbnail // single-attachment/thumbnail/thumbnail // single-attachment/thumbnail/thumbnail/thumbnail _.filter(function(data) { var derivative = data.source.couchmagick && data.source.couchmagick[data.source.id] && data.source.couchmagick[data.source.id][data.source.name] && data.source.couchmagick[data.source.id][data.source.name].id; return !derivative; }), // process attachments _.through(function(source) { return _(function(push, done) { source .on('data', function(data) { convert.push(data, function(err, res) { push(err, res); }); }) .on('error', push) .on('end', done); }); }), _.map(function(data) { if (!data.response) { return data; } return { db_name: data.db_name, seq: data.seq, type: 'log', message: 'Complete: ' + JSON.stringify(data.response) }; }) ); };
module.exports = function(url, options) { options = options || {}; options.name = options.name || pkg.name; var context = { log: options.info || console.log }; function compile(str, id) { var source = '(' + str + ')'; var name = id + '.js'; var script = vm.createScript(source, name); return script.runInNewContext(context); } function compileObj(obj, id) { if (typeof obj === 'string' && obj.match(IS_FUNCTION)) { return compile(obj, id); } if (typeof obj === 'object') { Object.keys(obj).forEach(function(key) { var name = [id, key].join('/'); obj[key] = compileObj(obj[key], name); }); } return obj; } function compileDoc(data, done) { var error = null; var config = { stream: 'compile', db_name: data.db_name, id: data.id }; var doc = data.doc[options.name]; try { config.doc = compileObj(doc, data.id); } catch(e) { error = { error: 'compile_error', reason: e }; } done(error, error ? null : config); } return _.through(function(source) { var sourceEnded = false; var target = _(function(push, done) { source .on('data', function(data) { push(null, data); if (data && data.db_name && data.id && data.id.match(/^_design\//) && data.doc && data.doc[options.name]) { compileDoc(_.extend({}, data), function(err, config) { push(err, config); if (sourceEnded) { push(null, _.nil); } }); } }) .on('error', push); }); source.on('end', function() { sourceEnded = true; }); return target; }); };