Source: index.js


var path = require('path');
var async = require('async');
var Client = require('ssh2').Client;
var glob = require("glob");
var fs = require("fs-extra");
var through = require('through');
var _ = require("underscore");
var _s = require("underscore.string");

var pkg = require('./package.json');
var debug = require('debug')(pkg.name);



/**
 * @throw err if then is null
 * @param then
 * @param err
 */
var returnOrThrow = function(then, err){
  if(then){
    var args = Array.prototype.slice.call(arguments);
    args.shift();
    then.apply(null, args);
  } else if(err) {
    debug('returnOrThrow '+ err);
    throw err;
  }
};

var scanLocalDirectory = function(localPath, then){
  // scan local directories
  var options = {
    cwd: localPath
  };
  glob( '**/', options, function (er, dirs) {
    // scan local files
    options.nodir = true;
    glob( '**', options, function (er, files) {
      then(dirs, files);
    });
  });
};

/**
 * Server credentials information
 * It can use password or key
 * to login, or run sudo command
 * transparently
 *
 * @note It is a class to only support documentation
 * @constructor
 */
function ServerCredentials(){
  this.username = '';
  this.password = '';
  this.host = 'localhost';
  this.port = 22;
  this.privateKey = '';
}

/**
 * sudo challenge completion over ssh
 *
 * If the login success, hasLogin is true
 *
 * @param stream Stream
 * @param pwd string
 * @param then callback(bool hasLogin)
 */
var sudoChallenge = function(stream, pwd, then){

  debug('waiting for sudo');

  var hasReceivedData = false;
  var hasChallenge = false;

  // this is a general timeout on the command
  // passed this 10 secs, it fails
  var tChallenge = setTimeout(function(){
    debug('Login has failed by timeout');
    stream.removeListener('data', checkPwdInput);
    if (then) then(true);
  }, 10000);

  var checkPwdInput = function(data){

    data = ''+data;
    hasReceivedData = true;

    // there can t be anything to resolve
    // if the challenge has not been sent
    if(!hasChallenge ){

      // first data is always the challenge
      if( data.match(/\[sudo\] password/) || data.match(/Password:/) ){
        hasChallenge = true;
        debug('Challenge started...');
        // if so send the password on stdin
        stream.write(pwd+'\n');

      }else{

        // otherwise,
        // the command has probably ran successfully
        clearTimeout(tChallenge);
        stream.removeListener('data', checkPwdInput);
        debug('Login done without a challenge');
        if (then) then(false);

      }

      // once the challenge is set,
      // it must be concluded
      // right after it s beginning
    } else if(hasChallenge){

      clearTimeout(tChallenge);
      stream.removeListener('data', checkPwdInput);

      hasChallenge = false;
      // this case handle only en.
      if(data.match(/Sorry, try again/) || data.match(/Password:/) ){
        debug('... Failed to resolve the challenge');
        if (then) then(true);
      }else{
        debug('... Challenge was successfully resolved');
        if (then) then(false);
      }
    }
  };
  stream.on('data', checkPwdInput);

  // this is for commands like rm -f /some
  var checkEmptyOutputCommands = function(){
    if(!hasReceivedData && !hasChallenge){
      clearTimeout(tChallenge);
      stream.removeListener('data', checkPwdInput);
      stream.removeListener('data', checkEmptyOutputCommands);
      debug('Login was done, without a challenge, without a data');
      if (then) then(false);
    }
  };
  stream.on('close', checkEmptyOutputCommands);
};

// todo
// better not to do that as it s a global
process.setMaxListeners(100);
/**
 *
 * @constructor
 */
function SSH2Utils(){}

/**
 * opens ssh connection
 *
 * @param server ServerCredentials
 * @param done (err, ssh2.Client conn)
 */
var connect = function(server, done){

  if(!server){
    throw new Error('missing server parameter')
  }

  if( server instanceof Client ){
    debug('re using existing connection');
    done(false, server);
  }else{
    server.username = server.username || server.userName || server.user; // it is acceptable
    debug('%s@%s:%s',server.username,server.host,server.port);

    if(!server.username){
      throw new Error('invalid server parameter')
    }

    var conn = new Client();
    conn.on('ready', function() {
      Object.keys(server).forEach(function(k){
        if(conn[k]){
          throw 'Cannot redefine existing field '+k+' on ssh2Client object, it already exists.'
        }
        conn[k] = server[k];
      });
      done(null, conn);
    });

    try{
      conn.connect(server);

      debug('connecting');

      conn.on('error',function(stderr){
        if(stderr) debug(''+stderr);
        done(stderr, null);
      });


      // manage process termination
      conn.pendingStreams = [];
      var superEnd = conn.end;
      conn.end = function(){
        conn.pendingStreams.forEach(function(stream){
          stream.kill(conn.pendingStreams.length);
        });
        conn.pendingStreams = [];
        superEnd.call(conn);
      };
      // manage user pressing ctrl+C
      var sigIntSent = function(){
        conn.end();
      };
      process.on('SIGINT', sigIntSent);
      conn.on('close',function(){
        try{
          process.removeListener('SIGINT', sigIntSent);
        }catch(ex){}
      });
      conn.on('end',function(){
        try{
          process.removeListener('SIGINT', sigIntSent);
        }catch(ex){}
      });
    }catch(ex){
      debug(''+ex);
      done(ex, null);
    }
  }
};

/**
 *
 * @param cmd String
 * @param stream Stream
 */
var sendSigInt = function(cmd, stream, length){
  debug('sendSigInt '+cmd);
  try{
    // this is a workaround for more ssh implementations
    for(var i=0;i<length;i++){
      stream.write("\x03");
    }
  }catch(ex){ }
  // this only works with openssh@centos
  try{
    for(var i=0;i<length;i++){
      stream.signal('SIGINT');
    }
  }catch(ex){ }
};

/**
 * Execute a command and returns asap
 *
 * @param conn ssh2.Client
 * @param server ServerCredentials
 * @param cmd String
 * @param done callback(err, ssh2._Channel_ stream)
 */
var sudoExec = function(conn, server, cmd, done){

  var opts = {};

  opts.pty = !!cmd.match(/^su(do\s|\s)/) && ('password' in server);

  debug('cmd %j', cmd);
  debug('pty %j', opts.pty);

  conn.exec(cmd, opts, function(err, stream) {

    if (err) debug('err %j', err);
    if (err) return done(err);

    stream.stderr.on('data', function(data){
      debug('sudoExec stderr %s', _s.trim(''+data))
    });
    stream.on('data', function(data){
      debug('sudoExec stdout %s', _s.trim(''+data))
    });

    if(done) done(null, stream);

    if( opts.pty ){
      sudoChallenge(stream, server['password'], function(hasLoginError){
        if(hasLoginError) debug(
          'login failure, hasLoginError:%j', hasLoginError);
      });
    }

    stream.kill = function(length){
      sendSigInt(cmd, stream, length || 1);
    };
    // manage process termination with open handle
    stream.on('close', function(){
      var k = conn.pendingStreams.indexOf(stream);
      if(k>-1) conn.pendingStreams.splice(k,1);
    });
    conn.pendingStreams.push(stream);
  });
};

/**
 * @see connect
 */
SSH2Utils.prototype.getConnReady = connect;

/**
 * Executes a command and return its output
 *  like child_process.exec.
 * non-interactive
 *
 * also take care of
 * - remote program termination with ctrl+C
 *
 * @param server ServerCredentials|ssh2.Client
 * @param cmd String
 * @param done callback(bool err, String stdout, String stderr, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.execOne = function(server, cmd, done){

  connect(server, function(err, conn){
    if( err) return returnOrThrow(done, err, '', ''+err, server, conn);

    sudoExec(conn, server, cmd, function(err, stream){
      if( err) return returnOrThrow(done, err, '', ''+err, server, conn);

      var stderr = '';
      var stdout = '';
      stream.stderr.on('data', function(data){
        stderr += data.toString();
      });
      stream.on('data', function(data){
        stdout += data.toString();
      });

      stream.on('close', function(){
        var fineErr = null;
        if(stderr){
          fineErr = new Error(_s.trim(stderr));
          debug('stdout %j', stdout);
          debug('stderr %j', stderr);
        }
        returnOrThrow(done, fineErr, stdout, stderr, server, conn);
      });
    });
  });

};

/**
 * Executes a command and return its output
 *  like child_process.exec.
 * non-interactive
 *
 * also take care of
 * - remote program termination with ctrl+C
 *
 * If cmd is an array of string,
 * they are executed in serie,
 * respective output of each stdout / stderr is join then returned.
 *
 * @param server ServerCredentials|ssh2.Client
 * @param cmd String|[String]
 * @param doneEach callback(bool err, String stdout, String stderr, ServerCredentials server, ssh2.Client conn)
 * @param done callback(bool err, String stdout, String stderr, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.exec = function(server, cmd, doneEach, done){

  var that = this;
  if(_.isString(cmd)){
    cmd = [cmd];
  }
  if(!done&& _.isFunction(doneEach) ){
    done = doneEach;
    doneEach = null;
  }
  var cmds = [];
  var conn_;
  var err_;
  var stdout_ = '';
  var stderr_ = '';
  cmd.forEach(function(c){
    cmds.push(function(next){
      that.execOne(conn_ || server, c, function(err, stdout, stderr, server, conn){
        conn_ = conn;
        err_ = err;
        stdout_ += stdout;
        stderr_ += stderr;
        if(doneEach) doneEach(err, stdout, stderr, server, conn);
        next();
      });
    });
  });

  async.series(cmds, function(){
    returnOrThrow(done, err_, stdout_, stderr_, server, conn_);
  });

};

/**
 * Executes a command and return its stream,
 *  like of child_process.spawn.
 * interactive
 *
 * also take care of
 * - manage sudo cmd
 * - log errors to output
 *
 * If cmd is an array, they are executed in serie,
 * the pipe is open asap,
 * you ll receive each stdout stderr data in serie
 *
 * @param server ServerCredentials|ssh2.Client
 * @param cmd String|[String]
 * @param doneEach callback(bool err, String stdout, String stderr, ServerCredentials server, ssh2.Client conn)
 * @param done callback(bool err, Stream stdout, Stream stderr, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.run = function(server, cmd, doneEach, done){
  var stdoutStream = through();
  var stderrStream = through();
  if(_.isString(cmd)){
    cmd = [cmd];
  }
  if(!done&& _.isFunction(doneEach) ){
    done = doneEach;
    doneEach = null;
  }
  var cmds = [];
  var conn_;
  var err_;
  var stream_;
  connect(server, function(err, conn){
    if(err) return returnOrThrow(done, err, null, ''+err, server, conn);
    cmd.forEach(function(c, i){
      cmds.push(function(next){
        sudoExec(conn, server, c, function(err, stream){
          if(err) return returnOrThrow(done, err, stream, stream.stderr, server, conn);

          conn_ = conn;
          err_ = err;

          (function(stream, i){
            var onStdoutData = function(d){
              stdoutStream.emit('data', d);
            };
            var onStderrData = function(d){
              stderrStream.emit('data', d);
            };
            stream.on('data', onStdoutData);
            stream.stderr.on('data', onStderrData);
            var onClose = function(err){
              setTimeout(function(){
                if(i===cmds.length){
                  stdoutStream.emit('close', err);
                }
                stream.removeListener('close', onClose);
                stream.removeListener('data', onStdoutData);
                stream.stderr.removeListener('data', onStderrData);
              },500);
            };
            stream.on('close', onClose);
          })(stream, i+1);

          if(!stream_){ // execute only once
            returnOrThrow(done, err, stdoutStream, stderrStream, server, conn);
          }

          if(doneEach) doneEach(err, stream, stream.stderr, server, conn);

          stream_ = stream;
          next();

        });
      })
    });

    async.series(cmds, function(){
      if(!stream_){
        returnOrThrow(done, err_, stdoutStream, stderrStream, server, conn_);
      }
    });

  });

};

/**
 * Executes a set of multiple and sequential commands.
 *
 * @param server ServerCredentials|ssh2.Client
 * @param cmds [String]
 * @param cmdComplete callback(String command, String response, ServerCredentials server)
 * @param then callback(err, String allSessionText, ServerCredentials server)
 */
SSH2Utils.prototype.runMultiple = SSH2Utils.prototype.run;

/**
 * Reads a file on the remote
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.readFile = function(server, remoteFile, then){

  connect(server, function(err, conn){
    if(err) return returnOrThrow(then, err, '', server, conn);

    conn.sftp(function(err, sftp){
      if(err) return returnOrThrow(then, err, '', server, conn);

      debug('createReadStream %s', remoteFile);
      var content = '';
      var stream = sftp.createReadStream(remoteFile);
      stream.on('data', function(d){
        content += ''+d;
      });
      var finish = function(readErr){
        stream.removeListener('error', finish);
        stream.removeListener('close', finish);
        returnOrThrow(then, readErr, content, server, conn);
      };
      stream.on('error', finish);
      stream.on('close', finish);
    });
  });
};

/**
 * Reads a file on the remote via sudo
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.readFileSudo = function(server, remoteFile, then){

  var content = '';
  this.run(server, 'sudo cat '+remoteFile+'', function(err, stdout, stderr, server, conn){
    if(err) return returnOrThrow(then, err, content, server, conn);

    var readErr;
    stdout.on('data', function(d){
      content += ''+d;
    });
    stdout.on('error', function(e){
      readErr = e;
    });
    stdout.on('close', function(){
      returnOrThrow(then, readErr, content, server, conn);
    });
  });
};

/**
 * Reads a large file on the remote
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param then callback(err, Stream data, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.streamReadFile = function(server, remoteFile, then){

  connect(server, function(err,conn){
    conn.sftp(function(err, sftp){
      var stream = sftp.createReadStream(remoteFile);
      returnOrThrow(then, err, stream, server, conn);
    });
  });
};

/**
 * Reads a large file on the remote via sudo
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param then callback(err, Stream data, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.streamReadFileSudo = function(server, remoteFile, then){
  this.run(server, 'sudo cat '+remoteFile+'', function(err, stdout, stderr, server, conn){
    returnOrThrow(then, err, stdout, server, conn);
  });
};

/**
 * Downloads a file to the local
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param localPath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.getFile = function(server, remoteFile, localPath, then){

  connect(server, function(err,conn){
    conn.sftp(function(err, sftp){
      if(err) return returnOrThrow(then, err, server, conn);
      sftp.fastGet(remoteFile, localPath, function(err){
        returnOrThrow(then, err, server, conn);
      });
    });
  });
};

/**
 * Ensure a remote file contains a certain text piece of text
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param contain String
 * @param then callback(contains, err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.ensureFileContains = function(server, remoteFile, contain, then){
  var that = this;
  that.exec(server, 'grep "'+contain+'" '+remoteFile, function(err, stdout, stderr, server, conn){
    if(stdout.length>0){
      then(true, err, stdout, stderr, server, conn)
    } else {
      that.exec(conn, 'echo "'+contain+'" >> '+remoteFile, function(err, stdout, stderr, server, conn){
        then(!!err, err, stdout, stderr, server, conn);
      });
    }
  });
};

/**
 * Ensure a remote file contains a certain text piece of text
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param contain String
 * @param then callback(contains, err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.ensureFileContainsSudo = function(server, remoteFile, contain, then){
  var that = this;
  that.exec(server, 'sudo grep "'+contain+'" '+remoteFile, function(err, stdout, stderr, server, conn){
    if(stdout.length>0){
      then(true, err, stdout, stderr, server, conn)
    } else {
      that.exec(conn, 'sudo echo "'+contain+'" >> '+remoteFile, function(err,stdout,stderr,server,conn){
        then(!!err, err, stdout, stderr, server, conn);
      });
    }
  });
};

/**
 * Uploads a file on the remote remote
 *
 * @param server ServerCredentials|ssh2.Client
 * @param localFile String
 * @param remoteFile String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.putFile = function(server, localFile, remoteFile, then){

  debug('from %s to %s', path.relative(__dirname,localFile), remoteFile);

  remoteFile = remoteFile.replace(/[\\]/g,'/'); // windows needs this
  var remotePath = path.dirname(remoteFile);
  this.mkdir(server, remotePath, function(err, server, conn){
    if(err) return returnOrThrow(then, err, server, conn);

    conn.sftp(function(err, sftp){
      if(err) return returnOrThrow(then, err, server, conn);

      debug('put %s %s',
        path.relative(process.cwd(),localFile), path.relative(remotePath,remoteFile));

      sftp.fastPut(localFile, remoteFile, function(err){
        returnOrThrow(then, err, server, conn);
      });
    });
  });

};

/**
 * Uploads a file on the remote via sudo support
 *
 * @param server ServerCredentials|ssh2.Client
 * @param localFile String
 * @param remoteFile String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.putFileSudo = function(server, localFile, remoteFile, then){

  var that = this;

  debug('from %s to %s', path.relative(__dirname, localFile), remoteFile);

  remoteFile = remoteFile.replace(/[\\]/g,'/'); // windows needs this
  var remotePath = path.dirname(remoteFile);
  var fileName = path.basename(remoteFile);

  this.mktemp(server, pkg.name, function(err, tmpPath, server, conn){
    if(err) return returnOrThrow(then, err, server, conn);

    conn.sftp(function(err, sftp){
      if(err) return returnOrThrow(then, err, server, conn);

      debug('put %s %s',
        path.relative(process.cwd(), localFile), path.relative(remotePath, remoteFile));

      sftp.fastPut(localFile, tmpPath+'/'+fileName, function(err){
        if(err) return returnOrThrow(then, err, server, conn);

        that.mkdirSudo(conn,remotePath, function(err){
          if(err) return returnOrThrow(then, err, server, conn);

          that.exec(conn, 'sudo cp '+tmpPath+'/'+fileName+' '+remoteFile, function(err, stdout, stderr){
            if(err) return returnOrThrow(then, err, server, conn);

            that.rmdirSudo(conn, tmpPath+'/'+fileName, function(err){
              returnOrThrow(then, err, server, conn);
            });

          });

        });

      });
    });
  });

};

/**
 * Writes content to a remote file
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param content String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.writeFile = function(server, remoteFile, content, then){

  debug('write to %s',remoteFile);

  remoteFile = remoteFile.replace(/[\\]/g,'/'); // windows needs this

  var remotePath = path.dirname(remoteFile);
  debug('mkdir %s', remotePath);
  this.mkdir(server, remotePath, function(err, server, conn){
    if(err){
      return returnOrThrow(then, err, server, conn);
    }

    debug('write %s', remoteFile);

    conn.sftp(function sftpOpen(err, sftp){
      if(err){
        return returnOrThrow(then, err, server, conn);
      }
      try{
        debug('stream start');
        var wStream = sftp.createWriteStream(remoteFile, {flags: 'w+', encoding: null});
        wStream.on('error', function (err) {
          debug('stream error %j', err);
          wStream.removeAllListeners('finish');
          returnOrThrow(then, err, server, conn);
        });
        wStream.on('finish', function () {
          debug('stream finish');
          returnOrThrow(then, err, server, conn);
        });
        wStream.end(''+content);
      }catch(ex){
        debug('stream ex %j', ex);
        return returnOrThrow(then, ex, server, conn);
      }
    });
  });
};

/**
 * Writes content to a remote file
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param content String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.writeFileSudo = function(server, remoteFile, content, then){
  throw 'todo';
};

/**
 * Tells if a file exists on remote
 * by trying to open handle on it.
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param then callback(err, exists, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.fileExists = function(server, remoteFile, then){

  remoteFile = remoteFile.replace(/[\\]/g,'/'); // windows needs this
  debug('fileExists %s',remoteFile);

  connect(server, function sshConnect(err, conn){
    if (err) return returnOrThrow(then, err, server, conn);
    conn.sftp(function sftpOpen(err, sftp){
      if (err) return returnOrThrow(then, err, server, conn);
      sftp.open(remoteFile, 'r', function stfpOpenFileHandle(err, handle){
        if(handle) sftp.close(handle);
        returnOrThrow(then, err, !err, server, conn);
      })
    });
  });
};

/**
 * Tells if a file exists on remote
 * by trying to open handle on it.
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remoteFile String
 * @param then callback(err, exists, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.fileExistsSudo = function(server, remoteFile, then){

  remoteFile = path.normalize(remoteFile).replace(/\\/g, '/');
  var remoteFileName = path.basename(remoteFile);
  var remotePath = path.dirname(remoteFile);
  debug('fileExistsSudo %s', remoteFile);

  this.exec(server, 'sudo ls -alh '+remotePath+'/', function(err, stdout, stderr, server, conn){
    returnOrThrow(then, err, !!stdout.match(remoteFileName), server, conn);
  });
};

/**
 * Deletes a file or directory
 * rm -fr /some/path
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.rmdir = function(server, remotePath, then){
  debug('rmdir %s',remotePath);
  this.exec(server, 'rm -fr '+remotePath, function rmdir (err, stderr, stdout, server, conn){
    var fineErr = null;
    if( stdout ){
      fineErr = new  Error(stdout);
      fineErr.code = 3;
    }
    returnOrThrow(then, fineErr, server, conn);
  });
};

/**
 * Deletes a file or directory
 * sudo rm -fr /some/path
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.rmdirSudo = function(server, remotePath, then){
  debug('rmdirSudo %s', remotePath);
  this.exec(server, 'sudo rm -fr '+remotePath, function rmdirSudo (err, stderr, stdout, server, conn){
    var fineErr = null;
    if( stdout ){
      fineErr = new  Error(stdout);
      fineErr.code = 3;
    }
    returnOrThrow(then, fineErr, server, conn);
  });
};

/**
 * Creates a concurrent-safe temporary remote directory.
 *
 * It does not attempt to keep track of temp files created during the session.
 * Thus it won t delete them on connection close.
 *
 * @param server ServerCredentials|ssh2.Client
 * @param suffix String
 * @param then callback(err, tmpDirName, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.mktemp = function(server, suffix, then){
  debug('mktemp %s',suffix);
  this.exec(server, 'mktemp -d --suffix='+suffix, function mkdir (err, stderr, stdout, server, conn){
    // if response is done on stderr when everything s fine,
    // errors may go into stdout or fd.pipe[3], it is unclear and for sure untested
    var tempPath = _s.trim(stderr);
    returnOrThrow(then, null, tempPath, server, conn);
  });
};

/**
 * Creates a remote directory
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.mkdir = function(server, remotePath, then){
  debug('mkdir %s',remotePath);
  this.exec(server, 'mkdir -p '+remotePath, function mkdir (err, stderr, stdout, server, conn){
    var fineErr = null;
    if( stdout ){
      fineErr = new  Error(stdout);
      fineErr.code = 3;
    }
    returnOrThrow(then, fineErr, server, conn);
  });
};

/**
 * Creates a remote directory
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.mkdirSudo = function(server, remotePath, then){
  debug('mkdirSudo %s',remotePath);
  this.exec(server, 'sudo mkdir -p '+remotePath, function mkdirSudo (err, stderr, stdout, server, conn){
    var fineErr = null;
    if( stdout ){
      fineErr = new  Error(stdout);
      fineErr.code = 3;
    }
    returnOrThrow(then, fineErr, server, conn);
  });
};

/**
 * Ensure a remote directory exists and is empty
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.ensureEmptyDir = function(server, remotePath, then){
  debug('ensureEmptyDir %s',remotePath);
  var that = this;
  that.rmdir(server, remotePath, function(err, server, conn){
    if(err) return returnOrThrow(then, err, server, conn);
    that.mkdir(server, remotePath, function(err, server, conn){
      returnOrThrow(then, err, server, conn);
    });
  });
};

/**
 * Ensure a remote directory exists and is empty
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.ensureEmptyDirSudo = function(server, remotePath, then){
  debug('ensureEmptyDir %s',remotePath);
  var that = this;
  that.rmdirSudo(server, remotePath, function(err, server, conn){
    if(err) return returnOrThrow(then, err, server, conn);
    that.mkdirSudo(server, remotePath, function(err, server, conn){
      returnOrThrow(then, err, server, conn);
    });
  });
};

/**
 * Ensure a file belongs to connected user
 * by sudo chmod -R /path
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.ensureOwnership = function(server, remotePath, then){
  debug('ensureWritable %s',remotePath);
  var that = this;
  server.username = server.username || server.userName || server.user;
  that.exec(server, 'sudo chown -R '+server.username+':'+server.username+' '+remotePath, function(err, stdout, stderr, server, conn) {
    returnOrThrow(then, err, server, conn);
  });
};

/**
 * Uploads a local directory to the remote.
 * Partly in series, partly parallel.
 * Proceed such
 * sudo rm -fr /remotePath
 * sudo mkdir -p /remotePath
 * recursive sftp mkdir
 * recursive sftp put
 *
 * @param server ServerCredentials|ssh2.Client
 * @param localPath String
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.putDir = function(server, localPath, remotePath, then){
  var that = this;
  that.ensureEmptyDir(server, remotePath, function(err, server, conn){
    if(err) return returnOrThrow(then, err, server, conn);

    conn.sftp(function(err, sftp){
      if(err) return returnOrThrow(then, err, server, conn);

      debug('ready');
      scanLocalDirectory(localPath, function(dirs, files){

        // create remote directories
        var dirHandlers = [];
        dirs.forEach(function(f){
          dirHandlers.push(function(next){
            var to = path.join(remotePath, f).replace(/[\\]/g,'/'); // windows needs this
            debug(pkg.name, 'mkdir %s', to);
            that.mkdir(server, to, function(err){
              if(err) debug('mkdir %s %s', to, err.message);
              next();
            });
          })
        });

        // push files to remote
        var filesHandlers = [];
        files.forEach(function(f){
          filesHandlers.push(function(next){
            var from = path.join(localPath, f);
            var to = path.join(remotePath, f).replace(/[\\]/g,'/'); // windows needs this
            debug(pkg.name, 'put %s %s', path.relative(process.cwd(), from), to);
            sftp.fastPut(from, to, function(err){
              if(err) debug('fastPut %s %s %s', from, to, err.message);
              next();
            });
          })
        });

        // then push the scanned files and directories
        async.series(dirHandlers, function(){
          async.parallelLimit(filesHandlers, 4, function(){
            returnOrThrow(then, err, server, conn);
          });
        });
      });
    });
  });
};

/**
 * Uploads a local directory to the remote.
 * Partly in series, partly parallel.
 * Proceed such
 * sudo rm -fr /remotePath
 * sudo mkdir -p /remotePath
 * recursive sftp mkdir
 * recursive sftp put
 *
 * @param server ServerCredentials|ssh2.Client
 * @param localPath String
 * @param remotePath String
 * @param then callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.putDirSudo = function(server, localPath, remotePath, then){

  var that = this;

  var tmpRemotePath = path.join('/tmp/ssh2-utils/', remotePath);
  that.ensureEmptyDirSudo(server, remotePath, function(err, server, conn){
    if(err) return returnOrThrow(then, err, server, conn);

    that.ensureEmptyDirSudo(conn, tmpRemotePath, function(err, server, conn){
      if(err) return returnOrThrow(then, err, server, conn);

      that.ensureOwnership(conn, tmpRemotePath, function(err, server, conn) {
        if (err) return returnOrThrow(then, err, server, conn);

        conn.sftp(function(err, sftp){
          if(err) return returnOrThrow(then, err, server, conn);

          debug('ready');
          scanLocalDirectory(localPath, function(dirs, files){

            // create remote directories
            var dirHandlers = [];
            dirs.forEach(function(f){
              dirHandlers.push(function(next){
                var to = path.join(tmpRemotePath, f).replace(/[\\]/g,'/');
                debug(pkg.name, 'mkdir %s', to);
                that.mkdirSudo(to, function(err){
                  if(err) debug('mkdir %s %s', to, err.message);
                  next();
                });
              })
            });

            // push files to remote
            var filesHandlers = [];
            files.forEach(function(f){
              filesHandlers.push(function(next){
                var from = path.join(localPath, f);
                var to = path.join(tmpRemotePath, f).replace(/[\\]/g,'/'); // windows needs this
                debug(pkg.name, 'put %s %s', path.relative(process.cwd(), from), to);
                sftp.fastPut(from, to, function(err){
                  if(err) debug('fastPut %s %s %s', from, to, err.message);
                  next();
                });
              })
            });

            // then push the scanned files and directories
            async.series(dirHandlers, function(){
              async.parallelLimit(filesHandlers, 4, function(){
                if(err) return returnOrThrow(then, err, server, conn);
                that.exec(conn, 'sudo cp -R '+path.join(tmpRemotePath, '*')+' '+remotePath+'/', function(err, stdout, stderr, server, conn){
                  if(err) return returnOrThrow(then, err, server, conn);
                  that.rmdirSudo(conn, tmpRemotePath, function(err, server, conn){
                    returnOrThrow(then, err, server, conn);
                  });
                });
              });
            });
          });
        });

      });


    });
  });

};

/**
 * Downloads a remote directory to the local.
 * remote traverse directories over sftp.
 * then get files in parallel
 *
 * @param server ServerCredentials|ssh2.Client
 * @param remotePath String
 * @param localPath String
 * @param allDone callback(err, ServerCredentials server, ssh2.Client conn)
 */
SSH2Utils.prototype.getDir = function(server, remotePath, localPath, allDone){

  var that = this;
  server.username = server.username || server.userName || server.user;

  connect(server, function(err,conn){
    conn.sftp(function(err, sftp){
      if (err) throw err;

      debug('ready');

      var files = [];
      var dirs = [];
      function readdir(p, then){
        sftp.readdir(p, function sftpReaddir(err,list){
          if (err) throw err;
          var toRead = [];
          list.forEach(function(item){
            var fpath = p+'/'+item.filename;
            toRead.push(function(done){
              sftp.stat(fpath, function sftpStats(err,stat){
                if (err) throw err;
                if(stat.isDirectory()){
                  dirs.push(fpath.replace(remotePath, '' ) );
                  readdir(fpath,done);
                }else if(stat.isFile()){
                  files.push(fpath.replace(remotePath, '' ) );
                  done();
                }
              });
            });
          });
          async.parallelLimit(toRead,4, function(){
            if(then) then(dirs,files);
          });
        });
      }

      readdir(remotePath, function(dirs,files){
        var todoDirs = [];
        var todoFiles = [];
        dirs.forEach(function(dir){
          todoDirs.push(function(done){
            fs.mkdirs(localPath+dir,done);
          });
        });
        files.forEach(function(file){
          todoFiles.push(function(done){
            that.readFile(server, remotePath+file, localPath+file, done);
          });
        });

        async.parallelLimit(todoDirs,4, function(){
          async.parallelLimit(todoFiles,4, allDone);
        });
      });

    });
  });
};

module.exports = SSH2Utils;