"use strict";

var _path = _interopRequireDefault(require("path"));

var _ssh = _interopRequireDefault(require("ssh2"));

var _pMap = _interopRequireDefault(require("p-map"));

var _assert = _interopRequireDefault(require("assert"));

var _sbScandir = _interopRequireDefault(require("sb-scandir"));

var _shellEscape = _interopRequireDefault(require("shell-escape"));

var Helpers = _interopRequireWildcard(require("./helpers"));

function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

class SSH {
  constructor() {
    this.connection = null;
  }

  connect(givenConfig) {
    const connection = new _ssh.default();
    this.connection = connection;
    return new Promise(function (resolve) {
      resolve(Helpers.normalizeConfig(givenConfig));
    }).then(config => new Promise((resolve, reject) => {
      connection.on('error', reject);

      if (config.onKeyboardInteractive) {
        connection.on('keyboard-interactive', config.onKeyboardInteractive);
      }

      connection.on('ready', () => {
        connection.removeListener('error', reject);
        resolve(this);
      });
      connection.on('end', () => {
        if (this.connection === connection) {
          this.connection = null;
        }
      });
      connection.on('close', () => {
        if (this.connection === connection) {
          this.connection = null;
        }

        const error = new Error('No response from server'); // $FlowIgnore: Custom attribute

        error.code = 'ETIMEDOUT';
        reject(error);
      });
      connection.connect(config);
    }));
  }

  requestShell() {
    var _this = this;

    return _asyncToGenerator(function* () {
      const connection = _this.connection;
      (0, _assert.default)(connection, 'Not connected to server');
      return new Promise(function (resolve, reject) {
        connection.shell(Helpers.generateCallback(resolve, reject));
      });
    })();
  }

  requestSFTP() {
    var _this2 = this;

    return _asyncToGenerator(function* () {
      const connection = _this2.connection;
      (0, _assert.default)(connection, 'Not connected to server');
      return new Promise(function (resolve, reject) {
        connection.sftp(Helpers.generateCallback(resolve, reject));
      });
    })();
  }

  mkdir(path, type = 'sftp', givenSftp = null) {
    var _this3 = this;

    return _asyncToGenerator(function* () {
      (0, _assert.default)(_this3.connection, 'Not connected to server');
      (0, _assert.default)(type === 'exec' || type === 'sftp', 'Type should either be sftp or exec');

      if (type === 'exec') {
        const output = yield _this3.exec('mkdir', ['-p', path]);

        if (output.stdout) {
          throw new Error(output.stdout);
        }
      } else {
        (0, _assert.default)(!givenSftp || typeof givenSftp === 'object', 'sftp must be an object');
        const sftp = givenSftp || (yield _this3.requestSFTP());

        const makeSftpDirectory = retry => Helpers.mkdirSftp(path, sftp).catch(error => {
          if (retry && error && (error.message === 'No such file' || error.code === 'ENOENT')) {
            return _this3.mkdir(_path.default.dirname(path), 'sftp', sftp).then(() => makeSftpDirectory(false));
          }

          throw error;
        });

        try {
          yield makeSftpDirectory(true);
        } finally {
          if (!givenSftp) {
            sftp.end();
          }
        }
      }
    })();
  }

  exec(command, parameters = [], options = {}) {
    var _this4 = this;

    return _asyncToGenerator(function* () {
      (0, _assert.default)(_this4.connection, 'Not connected to server');
      (0, _assert.default)(typeof options === 'object' && options, 'options must be an Object');
      (0, _assert.default)(!options.cwd || typeof options.cwd === 'string', 'options.cwd must be a string');
      (0, _assert.default)(!options.stdin || typeof options.stdin === 'string', 'options.stdin must be a string');
      (0, _assert.default)(!options.stream || ['stdout', 'stderr', 'both'].indexOf(options.stream) !== -1, 'options.stream must be among "stdout", "stderr" and "both"');
      (0, _assert.default)(!options.options || typeof options.options === 'object', 'options.options must be an object');
      const output = yield _this4.execCommand([command].concat((0, _shellEscape.default)(parameters)).join(' '), options);

      if (!options.stream || options.stream === 'stdout') {
        if (output.stderr) {
          throw new Error(output.stderr);
        }

        return output.stdout;
      }

      if (options.stream === 'stderr') {
        return output.stderr;
      }

      return output;
    })();
  }

  execCommand(givenCommand, options = {}) {
    var _this5 = this;

    return _asyncToGenerator(function* () {
      let command = givenCommand;
      const connection = _this5.connection;
      (0, _assert.default)(connection, 'Not connected to server');
      (0, _assert.default)(typeof options === 'object' && options, 'options must be an Object');
      (0, _assert.default)(!options.cwd || typeof options.cwd === 'string', 'options.cwd must be a string');
      (0, _assert.default)(!options.stdin || typeof options.stdin === 'string', 'options.stdin must be a string');
      (0, _assert.default)(!options.options || typeof options.options === 'object', 'options.options must be an object');

      if (options.cwd) {
        command = `cd ${(0, _shellEscape.default)([options.cwd])}; ${command}`;
      }

      const output = {
        stdout: [],
        stderr: []
      };
      return new Promise(function (resolve, reject) {
        connection.exec(command, options.options || {}, Helpers.generateCallback(function (stream) {
          stream.on('data', function (chunk) {
            if (options.onStdout) options.onStdout(chunk);
            output.stdout.push(chunk);
          });
          stream.stderr.on('data', function (chunk) {
            if (options.onStderr) options.onStderr(chunk);
            output.stderr.push(chunk);
          });

          if (options.stdin) {
            stream.write(options.stdin);
            stream.end();
          }

          stream.on('close', function (code, signal) {
            resolve({
              code,
              signal,
              stdout: output.stdout.join('').trim(),
              stderr: output.stderr.join('').trim()
            });
          });
        }, reject));
      });
    })();
  }

  getFile(localFile, remoteFile, givenSftp = null, givenOpts = null) {
    var _this6 = this;

    return _asyncToGenerator(function* () {
      (0, _assert.default)(_this6.connection, 'Not connected to server');
      (0, _assert.default)(typeof localFile === 'string' && localFile, 'localFile must be a string');
      (0, _assert.default)(typeof remoteFile === 'string' && remoteFile, 'remoteFile must be a string');
      (0, _assert.default)(!givenSftp || typeof givenSftp === 'object', 'sftp must be an object');
      (0, _assert.default)(!givenOpts || typeof givenOpts === 'object', 'opts must be an object');
      const opts = givenOpts || {};
      const sftp = givenSftp || (yield _this6.requestSFTP());

      try {
        yield new Promise(function (resolve, reject) {
          sftp.fastGet(remoteFile, localFile, opts, Helpers.generateCallback(resolve, reject));
        });
      } finally {
        if (!givenSftp) {
          sftp.end();
        }
      }
    })();
  }

  putFile(localFile, remoteFile, givenSftp = null, givenOpts = null) {
    var _this7 = this;

    return _asyncToGenerator(function* () {
      (0, _assert.default)(_this7.connection, 'Not connected to server');
      (0, _assert.default)(typeof localFile === 'string' && localFile, 'localFile must be a string');
      (0, _assert.default)(typeof remoteFile === 'string' && remoteFile, 'remoteFile must be a string');
      (0, _assert.default)(!givenSftp || typeof givenSftp === 'object', 'sftp must be an object');
      (0, _assert.default)(!givenOpts || typeof givenOpts === 'object', 'opts must be an object');
      (0, _assert.default)((yield Helpers.exists(localFile)), `localFile does not exist at ${localFile}`);
      const that = _this7;
      const opts = givenOpts || {};
      const sftp = givenSftp || (yield _this7.requestSFTP());

      function putFile(retry) {
        return new Promise(function (resolve, reject) {
          sftp.fastPut(localFile, remoteFile, opts, Helpers.generateCallback(resolve, function (error) {
            if (error.message === 'No such file' && retry) {
              resolve(that.mkdir(_path.default.dirname(remoteFile), 'sftp', sftp).then(() => putFile(false)));
            } else {
              reject(error);
            }
          }));
        });
      }

      try {
        yield putFile(true);
      } finally {
        if (!givenSftp) {
          sftp.end();
        }
      }
    })();
  }

  putFiles(files, givenConfig = {}) {
    var _this8 = this;

    return _asyncToGenerator(function* () {
      (0, _assert.default)(_this8.connection, 'Not connected to server');
      (0, _assert.default)(Array.isArray(files), 'files must be an array');

      for (let i = 0, length = files.length; i < length; i += 1) {
        const file = files[i];
        (0, _assert.default)(file, 'files items must be valid objects');
        (0, _assert.default)(file.local && typeof file.local === 'string', `files[${i}].local must be a string`);
        (0, _assert.default)(file.remote && typeof file.remote === 'string', `files[${i}].remote must be a string`);
      }

      const transferred = [];
      const config = Helpers.normalizePutFilesOptions(givenConfig);
      const sftp = config.sftp || (yield _this8.requestSFTP());

      try {
        yield (0, _pMap.default)(files,
        /*#__PURE__*/
        function () {
          var _ref = _asyncToGenerator(function* (file) {
            yield _this8.putFile(file.local, file.remote, sftp, config.sftpOptions);
            transferred.push(file);
          });

          return function (_x) {
            return _ref.apply(this, arguments);
          };
        }());
      } catch (error) {
        error.transferred = transferred;
        throw error;
      } finally {
        if (!sftp) {
          sftp.end();
        }
      }
    })();
  }

  putDirectory(localDirectory, remoteDirectory, givenConfig = {}) {
    var _this9 = this;

    return _asyncToGenerator(function* () {
      (0, _assert.default)(_this9.connection, 'Not connected to server');
      (0, _assert.default)(typeof localDirectory === 'string' && localDirectory, 'localDirectory must be a string');
      (0, _assert.default)(typeof remoteDirectory === 'string' && remoteDirectory, 'remoteDirectory must be a string');
      (0, _assert.default)((yield Helpers.exists(localDirectory)), `localDirectory does not exist at ${localDirectory}`);
      (0, _assert.default)((yield Helpers.stat(localDirectory)).isDirectory(), `localDirectory is not a directory at ${localDirectory}`);
      (0, _assert.default)(typeof givenConfig === 'object' && givenConfig, 'config must be an object');
      const config = Helpers.normalizePutDirectoryOptions(givenConfig);
      const sftp = config.sftp || (yield _this9.requestSFTP());
      const scanned = yield (0, _sbScandir.default)(localDirectory, config.recursive, config.validate);
      const files = scanned.files.map(i => _path.default.relative(localDirectory, i));
      const directories = scanned.directories.map(i => _path.default.relative(localDirectory, i));
      let failed = false;
      let directoriesQueue = Promise.resolve();
      const directoriesCreated = new Set();

      const createDirectory =
      /*#__PURE__*/
      function () {
        var _ref2 = _asyncToGenerator(function* (path) {
          if (!directoriesCreated.has(path)) {
            directoriesCreated.add(path);
            directoriesQueue = directoriesQueue.then(() => _this9.mkdir(path, 'sftp', sftp));
            yield directoriesQueue;
          }
        });

        return function createDirectory(_x2) {
          return _ref2.apply(this, arguments);
        };
      }();

      try {
        yield (0, _pMap.default)(files,
        /*#__PURE__*/
        function () {
          var _ref3 = _asyncToGenerator(function* (file) {
            const localFile = _path.default.join(localDirectory, file);

            const remoteFile = _path.default.join(remoteDirectory, file).split(_path.default.sep).join('/');

            const remoteFileDirectory = _path.default.dirname(remoteFile);

            yield createDirectory(remoteFileDirectory);

            try {
              yield _this9.putFile(localFile, remoteFile, sftp, config.sftpOptions);
              config.tick(localFile, remoteFile, null);
            } catch (_) {
              failed = true;
              config.tick(localFile, remoteFile, _);
            }
          });

          return function (_x3) {
            return _ref3.apply(this, arguments);
          };
        }(), {
          concurrency: config.concurrency
        });
        yield (0, _pMap.default)(directories,
        /*#__PURE__*/
        function () {
          var _ref4 = _asyncToGenerator(function* (entry) {
            const remoteEntry = _path.default.join(remoteDirectory, entry).split(_path.default.sep).join('/');

            yield createDirectory(remoteEntry);
          });

          return function (_x4) {
            return _ref4.apply(this, arguments);
          };
        }(), {
          concurrency: config.concurrency
        });
      } finally {
        if (!config.sftp) {
          sftp.end();
        }
      }

      return !failed;
    })();
  }

  dispose() {
    if (this.connection) {
      this.connection.end();
    }
  }

}

module.exports = SSH;