Files
deployd/bin/dpd

535 lines
12 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Dependencies
*/
var program = require('commander')
, dpd = require('../')
, fs = require('fs')
, path = require('path')
, exec = require('child_process').exec
, sh = require('shelljs')
, url = require('url')
, keygen = require('../lib/key').keygen
;
/**
* Clients for interacting with dpd over http and mongodb.
*/
var local = dpd;
var client = require('mdoq')
.use(function (req, res, next, use) {
var host = url.parse(this.url).host
, key = keys(host);
if(key) {
// finish with http
use(require('mdoq-http'));
req.headers['x-dssh-key'] = key;
next();
} else {
if(~this.url.indexOf('.deploydapp.com')) {
login(host, next);
} else {
// dont bother going to http without a key
return next({message: 'couldnt find a key for that remote'});
}
// after everyone has a chance to grab a key
use(function (req, res, next) {
key = keys(host);
req.headers['x-dssh-key'] = key;
next();
});
// finish with http
use(require('mdoq-http'));
}
})
;
/**
* Utilities
*/
function getAppName() {
return path.basename(path.resolve('.'));
}
function getStorageUrl(port) {
port = port || config('mongod').port;
return 'mongodb://localhost:' + (port || 27017) + '/' + getAppName();
}
function getMongodPort() {
var mongods = userConfig('mongods') || {}
, port = 27022
;
var localMongod = config('mongod');
if(localMongod) {
if(localMongod.port) port = localMongod.port;
} else {
while(mongods[port.toString()]) port++;
}
var mongod = mongods[port] = {
path: path.resolve('.')
};
// save global mongod config
userConfig('mongods', mongods);
// also store port for local config
mongod.port = port;
// save local config
config('mongod', mongod);
return port;
}
function startMongod(fn) {
sh.mkdir('-p', '.data');
stopMongod();
var buf
, port = getMongodPort()
, proc = exec('mongod --dbpath .data --port ' + port)
proc.stdout.on('data', function (data) {
buf += data;
if(program.debug) console.log(data);
if(~buf.indexOf('listening')) {
clearTimeout(timer);
proc.stdout.removeAllListeners('data');
fn(null, port);
}
});
var timer = setTimeout(function () {
fn(new Error('timed out while starting mongod'))
}, 2000);
return proc;
}
function stopMongod() {
if((fs.existsSync || path.existsSync)('.data/mongo.lock')) {
sh.rm('.data/mongo.lock');
}
}
function listen(port, fn) {
var key = config('key');
port = port || 2403;
startMongod(function (err, mongoPort) {
local
.storage(getStorageUrl(mongoPort))
.use('http://localhost:' + port).listen(function () {
function start(key) {
console.log('dpd is listening @', 'http://localhost:' + port);
if(program.dashboard) {
exec('open http://localhost:' + port + '/__dashboard/?key=' + encodeURIComponent(JSON.stringify(key)))
}
if(typeof fn === 'function') fn();
console.log('building cache...');
pushTo(local, function () {
});
}
var keys = local.use('/keys');
keys.get(function (err, k) {
if(k) {
start(k[0]);
} else {
keys.post(keygen(), function (err, key) {
start(key);
})
}
})
})
;
})
}
function cancel(msg) {
console.log(msg);
// process.exit(process.pid);
}
function config(key, value, path) {
path = path || '.dpd';
var config = {};
if((fs.existsSync || path.existsSync)(path)) {
try {
config = JSON.parse(fs.readFileSync(path))
} catch (e) {
return cancel('error when parsing config');
}
}
if(key) {
if(value) {
config[key] = value;
fs.writeFileSync(path, JSON.stringify(config));
} else {
return config[key];
}
}
}
function userConfig(key, value) {
return config(key, value, process.env.HOME + '/.dpd');
}
function sanitizeHost(host) {
if(~host.indexOf('://')) {
return host;
} else {
return 'http://' + host;
}
}
function pushTo(dpd, fn) {
var l = loader(fn);
files().forEach(function (file) {
if(fs.statSync(file).isDirectory()) {
l.add();
uploadDir(file, dpd, function () {
l.tic();
});
}
})
}
function files(fn) {
var files = sh.find('.').filter(function(file) { return file[0] !== '.'; });
files.push('.');
return files;
}
function dropFiles(client, fn) {
console.log('dropping files...');
files(fn);
}
function login(host, next) {
console.log('please login to deployd.com');
program.prompt('username: ', function (username) {
program.password('password: ', '*', function (password) {
require('mdoq')
.require('mdoq-http')
.use('http://deployd.com/apps/authenticate')
.get({host: host})
.post({username: username, password: password}, function (err, app) {
if(app && app.key) {
keys(host, app.key);
addRemote(host);
next();
} else {
next({message: 'login failed...'});
}
})
})
})
}
function resources(dpd, fn) {
dpd
.use('/resources')
.get(function (err, resources) {
if(err) {
console.log(err);
cancel('an error occurred while fetching resources');
} else if(!resources) {
console.log(dpd.url, 'does not have any resources');
process.exit();
} else {
resources.forEach(fn);
}
});
}
function keys(host, key) {
var keys = config('keys') || {};
if(key) {
keys[host] = key;
config('keys', keys);
} else {
return keys[host];
}
}
function addRemote(host, name) {
if(!host) return cancel('host is required');
var remotes = config('remotes') || {};
remotes[host] = name || true;
config('remotes', remotes);
}
function uploadFile(file, dpd, fn) {
var url = file;
if(url[0] === '.') url = url.slice(1);
if(url[0] !== '/') url = '/' + url;
dpd.use(url).post(fs.createReadStream(file), fn);
}
function uploadDir(dir, dpd, fn) {
var l = loader(fn);
function upload() {
fs.readdirSync(dir).forEach(function (file) {
var filePath = dir + '/' + file;
if(fs.statSync(filePath).isFile() && file[0] != '.') {
l.add();
uploadFile(filePath, dpd, l.tic);
}
})
if(!l.total) l.end();
}
var urlPath = '/' + dir;
if(dir === '.') urlPath = '/';
var resources = dpd.use('/resources');
resources.get({path: urlPath}, function (err, res) {
if(err) return cancel(err);
if(res) {
upload();
} else {
// create the resource
resources.post({path: urlPath, typeLabel: 'Files', type: 'Static'}, upload);
}
})
}
function downloadFile(host, path, output, fn) {
require('mdoq')
.require('mdoq-http')
.use(sanitizeHost(host))
.use(path)
.pipe(fs.createWriteStream(output), fn)
;
}
function downloadFiles(host, resource, fn) {
var remaining = 0;
var l = loader(fn);
client.use(sanitizeHost(host)).use(resource.path).get(function (err, files) {
if(err || !files) return cancel(err);
if(resource.path === '/') resource.path = '';
else sh.mkdir('-p', '.' + resource.path);
files.forEach(function (file) {
l.add();
downloadFile(host, resource.path + '/' + file, '.' + resource.path + '/' + file, function () {
l.tic();
});
})
})
}
/**
* Async Loader Utility
*/
var loaders = [];
var lbl = '';
function loader(fn) {
var l = {
completed: 0,
total: 0,
tic: function (force) {
l.completed++;
if(force) l.completed = l.total;
// render all loaders state...
var allTotal = 0, allCompleted = 0;
loaders.forEach(function (loader) {
allTotal += loader.total;
allCompleted += loader.completed;
});
if(allCompleted === allTotal) loaders = [];
var p = allCompleted / allTotal;
var perc = Math.round(p * 100);
var bar = '';
while(bar.length < 20) {
if(bar.length < p * 20) bar += '=';
else bar += ' ';
}
if(allTotal > 0) {
process.stdout.write('\r\033[2K' + '[' + bar + '] ' + allCompleted + '/' + allTotal + ' ');
}
if(perc === 100) console.log('');
if(l.completed === l.total) fn();
},
add: function () {
l.total++;
},
end: function () {
l.tic(true);
}
}
loaders.push(l);
return l;
}
/**
* CLI Options
*/
program
.version(JSON.parse(fs.readFileSync(__dirname + '/../package.json')).version)
.option('-d, --dashboard', 'open the dashboard when starting the server')
.option('-s, --storage', 'override the default storage path')
.option('--debug', 'show debug information')
;
/**
* CLI Commands
*/
program
.command('listen [port]')
.description(' - start an instance of deployd at the specified port (2403)')
.action(listen)
;
program
.command('remote [host] [name]')
.description(' - add a remote host for syncing files and configuration')
.action(addRemote)
;
program
.command('clone [host]')
.description(' - add a remote host and pull down all its resources and files into a new directory')
.action(function (host) {
// if(config('clone')) return cancel('cannot clone into an existing clone!');
var name = url.parse(sanitizeHost(host)).host.split('.')[0].split(':')[0];
console.log('cloning into', name + '...');
sh.mkdir('-p', name);
sh.cd(name);
addRemote(host);
var l = loader(function () {
process.exit();
config('clone', host);
})
startMongod(function (err, mongoPort) {
local
.use('http://localhost:2403')
.storage(getStorageUrl(mongoPort))
.listen(function () {
resources(client.use(sanitizeHost(host)), function (resource) {
if(resource.type === 'Static') {
l.add();
downloadFiles(host, resource, l.tic);
}
delete resource._id;
l.add();
// clone resource
local.use('/resources').del({path: resource.path}, function (err, res) {
local.use('/resources').post(resource, function (err, res) {
l.tic();
});
});
});
})
;
})
})
;
program
.command('push [host]')
.description(' - push all local resources and files to a remote host')
.action(function (host) {
function use(host) {
if(!host) return cancel('no remote found! add one with `dpd remote` or specify one `dpd push myapp.com`');
pushTo(client.use(sanitizeHost(host)), function () {
console.log(host, 'is now up to date');
});
}
var remotes = config('remotes') || {};
if(host) {
use(host);
} else if(Object.keys(remotes).length > 1) {
console.log('more than one remote found... you must specify one of the following hosts');
program.choose(Object.keys(remotes), function (i) {
use(Object.keys(remotes)[i]);
});
} else {
use(Object.keys(remotes)[0]);
}
})
;
program
.command('dev [port]')
.description(' - boot a local development server')
.action(function (port) {
dropFiles(function () {
listen(port);
});
})
;
program
.command('repl')
.description(' - connect to a running instance through a mongo repl')
.action(function (port) {
var mongo = require('child_process').spawn('mongo', ['localhost:' + config('mongod').port + '/' + getAppName()]);
process.stdin.pipe(mongo.stdin);
process.stdin.resume();
mongo.stdout.on('data', function (data) {
if(~data.toString().indexOf('/tweeter')) {
mongo.stdout.pipe(process.stdout);
}
})
})
;
/**
* Parse arguments
*/
program.parse(process.argv);
if(program.args.length === 0) {
listen();
}