Skip to content
This repository has been archived by the owner on Mar 18, 2018. It is now read-only.

Commit

Permalink
Merge pull request #9 from jmversteeg/support-msysgit
Browse files Browse the repository at this point in the history
Add support for Connection.copy() on windows (in an msysgit environment)
  • Loading branch information
gregberge committed Apr 6, 2015
2 parents 6bb2504 + 9e7faf1 commit ec60ec4
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 82 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
root = true

[*]
indent_style = space
indent_size = 2
282 changes: 221 additions & 61 deletions lib/connection.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
var _ = require('lodash');
var path = require('path');
var exec = require('child_process').exec;
var LineWrapper = require('stream-line-wrapper');
var Promise = require('bluebird');
var whereis = require('whereis');
var sprintf = require('sprintf-js').sprintf;
var remote = require('./remote');

// Expose connection.
Expand Down Expand Up @@ -39,6 +42,30 @@ Connection.prototype.log = function () {
this.options.log.apply(null, arguments);
};

/**
* Builds a command that will be executed remotely through SSH
* @param {string} command
* @returns {string}
*/
Connection.prototype.buildSshCommand = function (command) {

var connection = this;

// In sudo mode, we use a TTY channel.
var args = /^sudo/.exec(command) ? ['-tt'] : [];
args.push.apply(args, connection.sshArgs);
args.push(remote.format(connection.remote));

// Escape double quotes in command.
command = command.replace(/"/g, '\\"');

// Complete arguments.
args = ['ssh'].concat(args).concat(['"' + command + '"']);

return args.join(' ');

};

/**
* Run a new SSH command.
*
Expand All @@ -64,24 +91,15 @@ Connection.prototype.run = function (command, options, cb) {
return new Promise(function (resolve, reject) {
connection.log('Running "%s" on host "%s".', command, connection.remote.host);

// In sudo mode, we use a TTY channel.
var args = /^sudo/.exec(command) ? ['-tt'] : [];
args.push.apply(args, connection.sshArgs);
args.push(remote.format(connection.remote));

// Escape double quotes in command.
command = command.replace(/"/g, '\\"');

// Complete arguments.
args = ['ssh'].concat(args).concat(['"' + command + '"']);

// Log wrappers.
var stdoutWrapper = new LineWrapper({prefix: '@' + connection.remote.host + ' '});
var stderrWrapper = new LineWrapper({prefix: '@' + connection.remote.host + '-err '});

// Exec command.
var cmd = connection.buildSshCommand(command);

var child = exec(
args.join(' '),
cmd,
options,
function(err, stdout, stderr) {
if (err) return reject(err);
Expand All @@ -101,6 +119,160 @@ Connection.prototype.run = function (command, options, cb) {
}).nodeify(cb);
};

/**
* Executes the given command with child_process.exec, appropriately transforming the stdout and stderr streams
* @param {Connection} connection The associated connection object (used exclusively for output decoration)
* @param {string} cmd
* @param {Object} cmdOptions Array of options passed to child_process.exec
* @returns {Promise}
*/
function execCommand(connection, cmd, cmdOptions) {

return new Promise(function (resolve, reject) {

// Exec command.
var child = exec(cmd, cmdOptions, function (err, stdout, stderr) {
if (err) reject(err);
else
resolve({
child: child,
stdout: stdout,
stderr: stderr
});
});

if (connection.options.stdout)
child.stdout
.pipe(new LineWrapper({prefix: '@' + connection.remote.host + ' '}))
.pipe(connection.options.stdout);

if (connection.options.stderr)
child.stderr
.pipe(new LineWrapper({prefix: '@' + connection.remote.host + '-err '}))
.pipe(connection.options.stderr);

});

}

/**
* Performs the copy operation via rsync
* @param {Object} options
* @param {Connection} connection
* @param {string} src
* @param {string} dest
* @returns {Promise}
*/
function copyViaRsync(options, connection, src, dest) {

// Complete src.
var completeSrc = options.direction === 'remoteToLocal' ?
remote.format(connection.remote) + ':' + src :
src;

// Complete dest.
var completeDest = options.direction === 'localToRemote' ?
remote.format(connection.remote) + ':' + dest :
dest;

connection.log('Copy "%s" to "%s" via rsync', completeSrc, completeDest);

// Format excludes.
var excludes = options.ignores ? formatExcludes(options.ignores) : [];

// Append options to rsync command.
var rsyncOptions = excludes.concat(['-az']).concat(options.rsync);

// Build command.
var cmd = ['rsync'].concat(rsyncOptions).concat([
'-e',
'"ssh ' + connection.sshArgs.join(' ') + '"',
completeSrc,
completeDest
]).join(' ');

var cmdOptions = _.omit(options, 'direction');

return execCommand(connection, cmd, cmdOptions);

}

/**
* Generates an array of commands to use when copying over scp
* @param {Object} options
* @param {Connection} connection
* @param {string} src
* @param {string} dest
* @returns {string[]}
*/
function generateScpCommands(options, connection, src, dest) {
var generateCommand = function (cmd, dest) {
return (options.direction === 'remoteToLocal' && dest === 'dest' || options.direction === 'localToRemote' && dest === 'src') ?
cmd : connection.buildSshCommand(cmd);
};

var generatePath = function (path, dest) {
return (options.direction === 'remoteToLocal' && dest === 'dest' || options.direction === 'localToRemote' && dest === 'src') ?
path : remote.format(connection.remote) + ':' + path;
};

// Format excludes.
var excludes = options.ignores ? formatExcludes(options.ignores) : [];

var packageFile = sprintf('%s.tmp.tar.gz', path.basename(src));
var fromPath = generatePath(path.dirname(src) + '/' + packageFile, 'src');
var toPath = generatePath(dest, 'dest');

var cdSource = ['cd', path.dirname(src)].join(' ');
var cdDest = ['cd', dest].join(' ');

var tar = generateCommand(
[cdSource, ['tar'].concat(excludes).concat('-czf', packageFile, path.basename(src)).join(' ')].join(' && '),
'src');

// The command to untar the destination package
var untar = generateCommand(
[cdDest, ['tar'].concat('--strip-components', '1', '-xzf', packageFile).join(' ')].join(' && '),
'dest');

return [
tar,
generateCommand(['mkdir', '-p', dest].join(' '), 'dest'),
buildSCPCommand(connection, fromPath, toPath),
generateCommand([cdSource, ['rm', packageFile].join(' ')].join(' && '), 'src'),
untar,
generateCommand([cdDest, ['rm', packageFile].join(' ')].join(' && '), 'dest')
];
}

/**
* Performs the copy operation via tar+scp
* @param {Object} options
* @param {Connection} connection
* @param {string} src
* @param {string} dest
* @returns {Promise}
*/
function copyViaScp(options, connection, src, dest) {

var commands = generateScpCommands(options, connection, src, dest);

var cmdOptions = _.omit(options, 'direction');

// Executes an array of commands in series
return Promise.reduce(commands, function (results, cmd) {
return execCommand(connection, cmd, cmdOptions).then(function (res) {
results.stdout += res.stdout;
results.stderr += res.stderr;
return results;
});
}, {
stdout: '',
stderr: ''
});

}

/**
* Remote file copy.
*
Expand All @@ -122,59 +294,15 @@ Connection.prototype.copy = function (src, dest, options, cb) {
options = _.defaults(options || {}, {
maxBuffer: 1000 * 1024,
direction: 'localToRemote',
rsync: []
rsync: []
});

var connection = this;

return new Promise(function (resolve, reject) {

// Complete src.
var completeSrc = options.direction === 'remoteToLocal' ?
remote.format(connection.remote) + ':' + src :
src;

// Complete dest.
var completeDest = options.direction === 'localToRemote' ?
remote.format(connection.remote) + ':' + dest :
dest;
return isRsyncAvailable().then(function (rsyncAvailable) {

// Format excludes.
var excludes = options.ignores ? formatExcludes(options.ignores) : [];
var handler = rsyncAvailable ? copyViaRsync : copyViaScp;
return handler(options, this, src, dest);

// Append options to rsync command.
var rsyncOptions = excludes.concat(['-az']).concat(options.rsync);

// Build command.
var args = ['rsync'].concat(rsyncOptions).concat([
'-e',
'"ssh ' + connection.sshArgs.join(' ') + '"',
completeSrc,
completeDest
]);

connection.log('Remote copy "%s" to "%s"', completeSrc, completeDest);

// Log wrappers.
var stdoutWrapper = new LineWrapper({prefix: '@' + connection.remote.host + ' '});
var stderrWrapper = new LineWrapper({prefix: '@' + connection.remote.host + '-err '});

// Exec command.
var child = exec(args.join(' '), _.omit(options, 'direction'), function (err, stdout, stderr) {
if (err) return reject(err);
resolve({
child: child,
stdout: stdout,
stderr: stderr
});
});

if (connection.options.stdout)
child.stdout.pipe(stdoutWrapper).pipe(connection.options.stdout);

if (connection.options.stderr)
child.stderr.pipe(stderrWrapper).pipe(connection.options.stderr);
}).nodeify(cb);
}.bind(this)).nodeify(cb);
};

/**
Expand All @@ -190,6 +318,19 @@ function formatExcludes(excludes) {
}, []);
}

/**
* Checks whether the rsync binary is available
*
* @returns {Promise.<boolean>}
*/
function isRsyncAvailable() {
return new Promise(function (resolve) {
whereis('rsync', function (err, path) {
resolve(!err);
});
});
}

/**
* Build ssh args.
*
Expand All @@ -213,3 +354,22 @@ function buildSSHArgs(options) {

return args;
}

/**
* Build SCP command.
*
* @param {Connection} connection
* @param {string} from
* @param {string} to
* @returns {string}
*/
function buildSCPCommand(connection, from, to) {

var scp = ['scp'];

if (connection.remote.port) scp = scp.concat('-P', connection.remote.port);
if (connection.options.key) scp = scp.concat('-i', connection.options.key);

return scp.concat(from, to).join(' ');

}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"dependencies": {
"bluebird": "^2.9.14",
"lodash": "^3.5.0",
"stream-line-wrapper": "^0.1.1"
"sprintf-js": "^1.0.2",
"stream-line-wrapper": "^0.1.1",
"whereis": "^0.4.0"
}
}
2 changes: 2 additions & 0 deletions test/connection-pool.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
var rewire = require('rewire');
var expect = require('chai').use(require('sinon-chai')).expect;
var childProcess = require('./mocks/child-process');
var mockWhereis = require('./mocks/mock-whereis');
var Connection = rewire('../lib/connection');
var ConnectionPool = rewire('../lib/connection-pool');

describe('SSH Connection pool', function () {
beforeEach(function () {
Connection.__set__('exec', childProcess.exec.bind(childProcess));
Connection.__set__('whereis', mockWhereis({rsync: '/bin/rsync'}));
ConnectionPool.__set__('Connection', Connection);
});

Expand Down
Loading

0 comments on commit ec60ec4

Please sign in to comment.