Permalink
Browse files

added push command

  • Loading branch information...
1 parent c9189b4 commit 8c3d5e52cf67ddf5674897b04df047c22fe263e4 @caolan committed Aug 18, 2010
Showing with 398 additions and 7 deletions.
  1. +3 −0 .gitmodules
  2. +1 −0 deps/node-mime
  3. +3 −2 lib/cli.js
  4. +228 −0 lib/couchdb.js
  5. +5 −1 lib/help.js
  6. +1 −1 lib/modules.js
  7. +24 −1 lib/packages.js
  8. +69 −0 lib/push.js
  9. +2 −2 lib/utils.js
  10. +62 −0 test/test-packages.js
View
@@ -7,3 +7,6 @@
[submodule "deps/async"]
path = deps/async
url = git://github.com/caolan/async.git
+[submodule "deps/node-mime"]
+ path = deps/node-mime
+ url = git://github.com/bentomas/node-mime.git
Submodule node-mime added at 3b6031
View
@@ -24,15 +24,16 @@ process.title = 'cpm';
*/
var commands = {
- 'help': require('./help')
+ 'help': require('./help'),
+ 'push': require('./push')
};
/**
* Parse commands and turn on debugging straight away if its enabled
*/
var cmd = options.parse(process.argv.slice(2));
-if (options.debug) {
+if (cmd.options.debug) {
logger.level = 'debug';
}
View
@@ -0,0 +1,228 @@
+var url = require('url'),
+ async = require('../deps/async'),
+ mime = require('../deps/node-mime/mime'),
+ sys = require('sys'),
+ fs = require('fs'),
+ http = require('http'),
+ logger = require('./logger'),
+ querystring = require('querystring');
+
+
+
+// main couchdb function
+var exports = module.exports = function(instance){
+ var server = {};
+ var instance;
+
+ if(typeof instance == 'string') instance = url.parse(instance);
+ instance.protocol = instance.protocol || 'http';
+
+ server.db = function(name){
+ instance.db = name;
+ var db = {
+ exists: async.apply(exports.exists, instance, ''),
+ create: function(callback){
+ exports.createDB(instance, db, callback);
+ },
+ ensure: function(callback){
+ db.exists(function(err, exists){
+ if(err) return callback(err);
+ if(exists) return callback(null, db);
+ db.create(callback);
+ });
+ },
+ doc: function(id){
+ return {
+ exists: async.apply(exports.exists, instance, id),
+ save: async.apply(exports.save, instance, id),
+ attachment: function(path){
+ var p = id + '/' + path;
+ return {
+ upload: async.apply(exports.upload, instance, p),
+ exists: async.apply(exports.exists, instance, p)
+ }
+ }
+ };
+ }
+ };
+ return db;
+ }
+ return server;
+};
+
+exports.createDB = function(instance, db, callback){
+ exports.JSONClient(instance, 'PUT', '', null, function(err, data, res){
+ callback(err, db);
+ });
+};
+
+// sets up a http client for making requests to a couchdb instance
+exports.JSONClient = function(instance, method, path, data, callback){
+ if(instance.db) path = '/' + instance.db + '/' + path;
+ if(typeof data != 'string'){
+ try { data = JSON.stringify(data); }
+ catch(e) { return callback(e); }
+ }
+
+ var client = http.createClient(instance.port, instance.hostname);
+ client.on('error', callback);
+ var request = client.request(method, path, {
+ 'host': instance.hostname,
+ 'Content-Type': 'application/json'
+ });
+
+ request.on('response', function(response){
+ logger.debug('response:', {
+ headers: response.headers,
+ url: response.url,
+ method: response.method,
+ statusCode: response.statusCode
+ });
+ var buffer = [];
+ response.on('data', function(chunk){
+ buffer.push(chunk.toString());
+ });
+ response.on('end', function(){
+ var data = buffer.length ? JSON.parse(buffer.join('')): null;
+ logger.debug('data:', data);
+ if(response.statusCode >= 400 && data && data.error){
+ var err = new Error(data.reason || data.error);
+ callback(err, data, response);
+ }
+ else callback(null, data, response);
+ });
+ });
+
+ if(data) request.write(data, 'utf8');
+ request.end();
+
+ logger.debug('request:', request.output[0]);
+};
+
+// test if a doc exists in the db without fetching the whole doc
+// this should probably be added to node-couchdb
+exports.exists = function(instance, id, callback){
+ id = id || '';
+ exports.JSONClient(instance, 'HEAD', id, null, function(err, data, res){
+ res = res || {};
+ if(res.statusCode != 404 && err) return callback(err);
+ var exists = (res.statusCode == 200);
+ var etag = res.headers.etag;
+ var _rev = etag ? etag.substr(1, etag.length-2): null;
+ callback(null, exists, _rev);
+ });
+};
+
+exports.save = function(instance, id, doc, /*optional*/options, callback){
+ if(!callback){
+ callback = options;
+ options = {};
+ }
+ var method = id ? 'PUT': 'POST';
+ var path = id || '';
+
+ if(options.force){
+ // WARNING! this is a brute-force document update
+ // updates revision number to latest revision before saving
+ exports.exists(instance, id, function(err, exists, rev){
+ if(err) return callback(err);
+ if(exists) doc._rev = rev;
+ exports.JSONClient(instance, method, path, doc, function(err,data){
+ if(err) return callback(err);
+ doc._id = data.id;
+ doc._rev = data.rev;
+ callback(null, doc);
+ });
+ });
+ }
+ else exports.JSONClient(instance, method, path, data, callback);
+};
+
+exports.upload = function(instance, path, file, /*optional*/rev, callback){
+ if(!callback){
+ callback = rev;
+ rev = null;
+ }
+ if(rev){
+ path += '?' + querystring.stringify({rev: rev});
+ }
+ if(instance.db){
+ path = '/' + instance.db + '/' + path;
+ }
+ var client = http.createClient(instance.port, instance.hostname);
+ var request = client.request('PUT', path, {
+ 'host': instance.hostname,
+ 'Content-Type': mime.lookup(file)
+ });
+
+ client.on('error', function(err){
+ logger.debug('client error:', err);
+ // callback should be overridden by this point, but if its
+ // not then return the error.
+ // sometimes an error was being emitted on the client *after*
+ // the request has successfully completed and the callback executed,
+ // the error was (node v0.1.102):
+ // Error: ENOTCONN, Transport endpoint is not connected
+ callback(err);
+ });
+
+ var stream = fs.createReadStream(file);
+ var end_count = 0;
+ stream.on('end', function(){
+ logger.debug('stream event:', 'end');
+ // TODO: find the cause of this!
+ // sometimes 'end' gets emitted twice which seems to cause
+ // a bad file descriptor error (node v0.1.102)
+ end_count++;
+ //if(end_count == 2) console.log('end called twice');
+ });
+ var stream_error;
+ stream.on('error', function(err){
+ logger.debug('stream error:', err);
+ // sometimes we get bad file descriptor error after the
+ // sys.pump has completed. possibly a race condition between
+ // the server closing the request and the stream being closed by
+ // sys.pump? store the error and wait for the response. if the
+ // response contains an error, report that instead, otherwise
+ // report the stream error
+ if(end_count < 2) stream_error = err;
+ });
+
+ // wait for open event otherwise we sometimes get
+ // "TypeError: Bad argument" in fs:173
+ stream.on('open', function(fd){
+ logger.debug('stream event:', 'open');
+ sys.pump(stream, request);
+ });
+
+ logger.debug('request:', request);
+
+ request.on('response', function(response){
+ logger.debug('response:', response);
+ var buffer = [];
+ response.on('data', function(chunk){
+ buffer.push(chunk.toString());
+ });
+ response.on('end', function(){
+ var data = buffer.length ? JSON.parse(buffer.join('')): null;
+ logger.debug('data:', data);
+ if(response.statusCode >= 400 && data && data.error){
+ var err = new Error(data.reason || data.error);
+ callback(err, data, response);
+ callback = function(){};
+ }
+ else {
+ if(stream_error){
+ // a previous error occured when streaming the request
+ callback(stream_error, data, response);
+ callback = function(){};
+ }
+ else {
+ callback(null, data.rev);
+ callback = function(){};
+ }
+ }
+ });
+ });
+
+};
View
@@ -15,6 +15,10 @@ var logger = require('./logger');
*/
module.exports = function (args, options) {
- logger.info('help message');
+ logger.info('help', '\t\t\tDisplay this help message');
+ logger.info(
+ 'push url [package]',
+ '\tUpload a package to a CouchDB database'
+ );
logger.end();
};
View
@@ -9,7 +9,7 @@
*/
var path = require('path'),
- async = require('async'),
+ async = require('../deps/async'),
Script = process.binding('evals').Script;
View
@@ -11,7 +11,8 @@
var fs = require('fs'),
sys = require('sys'),
path = require('path'),
- async = require('async'),
+ async = require('../deps/async'),
+ couchdb = require('./couchdb'),
modules = require('./modules'),
utils = require('./utils');
@@ -50,6 +51,8 @@ exports.loadPackage = function (d, callback) {
// perform the list of tasks in parallel
async.parallel(tasks, function (err) {
_design.package = pkg;
+ _design.language = "javascript";
+ _design._id = pkg.name;
callback(err, pkg, _design);
});
@@ -246,3 +249,23 @@ exports.loadApp = function (packages, appname, target) {
// load a package and all its dependencies:
// exports.loadPackageFull
+
+exports.push = function (instance, packages, callback) {
+ async.forEach(Object.keys(packages), function (k, callback) {
+ var _design = packages[k];
+ var attachments = _design._attachments;
+ delete _design._attachments;
+ couchdb(instance).db(instance.db).ensure(function (err, db) {
+ if (err) return callback(err);
+ var doc = db.doc('_design/' + k);
+
+ doc.save(_design, {force: true}, function (err, d) {
+ if (err) return callback(err);
+ var keys = Object.keys(attachments);
+ async.reduce(keys, d._rev, function (rev, k, cb) {
+ doc.attachment(k).upload(attachments[k], rev, cb);
+ }, callback);
+ });
+ });
+ }, callback);
+};
View
@@ -0,0 +1,69 @@
+/*!
+ * CPM - Couch Package Manager
+ * Copyright (c) 2010 Caolan McMahon
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies
+ */
+
+var logger = require('./logger'),
+ packages = require('./packages'),
+ help = require('./help'),
+ url = require('url');
+
+
+/**
+ * Executes the push command
+ */
+
+module.exports = function (args, options) {
+ if (!args || !args.length) return help();
+ var pkg = (args.length < 2) ? '.': args[1];
+
+ logger.info('Package:', pkg);
+ try { process.chdir(pkg); }
+ catch (e) { return logger.error(e); }
+
+ var parsed = url.parse(args[0]);
+ var instance = {
+ hostname: parsed.hostname,
+ port: parsed.port,
+ db: (parsed.pathname || '').substr(1)
+ };
+
+ if (!instance.hostname) {
+ return logger.error('You must specify a hostname');
+ }
+ if (!instance.port) {
+ return logger.error('You must specify a port');
+ }
+ if (!instance.db) {
+ return logger.error('You must specify a database');
+ }
+
+ logger.info('Hostname:', instance.hostname);
+ logger.info('Port:', instance.port);
+ logger.info('DB:', instance.db);
+
+ packages.loadPackage('.', function (err, pkg, _design) {
+ if (err) return logger.error(err);
+
+ var pkgs = {};
+ pkgs[pkg.name] = _design;
+ if(pkg.app) {
+ try { packages.loadApp(pkgs, pkg.name, pkg.app); }
+ catch (e) { return logger.error(e); }
+ }
+
+ logger.debug('package.json:', pkg);
+ logger.debug('_design:', _design);
+
+ packages.push(instance, pkgs, function (err) {
+ if (err) return logger.error(err);
+ logger.end();
+ });
+ });
+};
+
Oops, something went wrong.

0 comments on commit 8c3d5e5

Please sign in to comment.