UNPKG

promise-sftp

Version:

a promise-based sftp client for node.js

554 lines (531 loc) 20.7 kB
// Generated by CoffeeScript 1.10.0 /* jshint node:true */ /* jshint -W097 */ (function() { 'use strict'; var ERROR_CODES, FtpConnectionError, FtpReconnectError, Promise, PromiseSftp, STATUSES, SshClient, complexPassthroughMethods, fs, otherPrototypeMethods, path, simplePassthroughMethods, slice = [].slice; Promise = require('bluebird'); fs = require('fs'); SshClient = require('ssh2').Client; path = require('path'); FtpConnectionError = require('promise-ftp-common').FtpConnectionError; FtpReconnectError = require('promise-ftp-common').FtpReconnectError; STATUSES = require('promise-ftp-common').STATUSES; ERROR_CODES = require('./errorCodes'); simplePassthroughMethods = { rename: 'continue', rmdir: 'continue', fastGet: 'callback', fastPut: 'callback', createReadStream: 'return', createWriteStream: 'return', open: 'continue', close: 'continue', write: 'continue', fstat: 'continue', fsetstat: 'continue', futimes: 'continue', fchown: 'continue', fchmod: 'continue', opendir: 'continue', readdir: 'continue', unlink: 'continue', stat: 'continue', lstat: 'continue', setstat: 'continue', utimes: 'continue', chown: 'continue', chmod: 'continue', readlink: 'continue', symlink: 'continue', realpath: 'continue', ext_openssh_rename: 'continue', ext_openssh_statvfs: 'continue', ext_openssh_fstatvfs: 'continue', ext_openssh_hardlink: 'continue', ext_openssh_fsync: 'continue' }; complexPassthroughMethods = { list: 'none', get: 'none', put: 'none', append: 'none', "delete": 'none', mkdir: 'continue', listSafe: 'none', size: 'none', lastMod: 'none', read: 'continue', wait: 'none' }; otherPrototypeMethods = ['connect', 'reconnect', 'logout', 'end', 'destroy', 'getConnectionStatus', 'restart']; PromiseSftp = (function() { var i, j, len, len1, methodList, methodName, ref; function PromiseSftp() { var _connect, _getSftpStream, autoReconnect, autoReconnectPromise, changePassword, closeSftpError, closeSshError, commonLogicFactory, connectOptions, connectionStatus, continueLogicFactory, continuePromise, finishLogic, finishType, keyboardInteractive, lastSftpError, lastSshError, name, promisifiedClientMethods, restartOffset, sftpClientContext, sshClient, unexpectedClose; if (!(this instanceof PromiseSftp)) { throw new TypeError("PromiseSftp constructor called without 'new' keyword"); } connectionStatus = STATUSES.NOT_YET_CONNECTED; sshClient = new SshClient(); sftpClientContext = {}; connectOptions = null; autoReconnect = null; keyboardInteractive = null; changePassword = null; lastSshError = null; lastSftpError = null; closeSshError = null; closeSftpError = null; unexpectedClose = null; autoReconnectPromise = null; continuePromise = Promise.resolve(); promisifiedClientMethods = {}; restartOffset = null; sshClient.on('error', function(err) { return lastSshError = err; }); sshClient.on('close', function(hadError) { if (hadError) { closeSshError = lastSshError; } unexpectedClose = connectionStatus !== STATUSES.DISCONNECTING && connectionStatus !== STATUSES.LOGGING_OUT; connectionStatus = STATUSES.DISCONNECTED; autoReconnectPromise = null; if (keyboardInteractive) { sshClient.removeListener('keyboard-interactive', keyboardInteractive); } if (changePassword) { return sshClient.removeListener('change password', changePassword); } }); continueLogicFactory = function(clientContext, name) { return function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return new Promise(function(resolve, reject) { return continuePromise = new Promise(function(resolve2, reject2) { var ready, ref; clientContext.client.once('continue', resolve2); ready = (ref = clientContext.client)[name].apply(ref, slice.call(args).concat([function() { var args2, err, result; err = arguments[0], args2 = 2 <= arguments.length ? slice.call(arguments, 1) : []; if (err) { return reject(err); } if (args2.length === 0) { result = null; } else if (args2.length === 1) { result = args2[0]; } else { result = args2; } return resolve(result); }])); if (ready) { clientContext.client.removeListener('continue', resolve2); return resolve2(); } }); }); }; }; finishLogic = function(stream) { continuePromise = new Promise(function(resolve, reject) { return stream.once('close', resolve); }); return void 0; }; _getSftpStream = continueLogicFactory({ client: sshClient }, 'sftp'); _connect = function(tempStatus) { return new Promise(function(resolve, reject) { var onSshClose, onSshEnd, onSshError, onSshReady, serverMessage; connectionStatus = tempStatus; serverMessage = null; sshClient.once('banner', function(msg, lang) { return serverMessage = msg; }); if (keyboardInteractive) { sshClient.on('keyboard-interactive', keyboardInteractive); } if (changePassword) { sshClient.on('change password', changePassword); } onSshReady = function() { sshClient.removeListener('error', onSshError); sshClient.removeListener('close', onSshClose); sshClient.removeListener('end', onSshEnd); closeSshError = null; return _getSftpStream().then(function(sftp) { sftpClientContext.client = sftp; return resolve(serverMessage); })["catch"](function(err) { lastSftpError = err; sshClient.destroy(); return reject(err); }); }; onSshError = function(err) { sshClient.removeListener('ready', onSshReady); sshClient.removeListener('close', onSshClose); sshClient.removeListener('end', onSshEnd); return reject(err); }; onSshClose = function() { sshClient.removeListener('ready', onSshReady); sshClient.removeListener('error', onSshError); sshClient.removeListener('end', onSshEnd); return reject(new FtpConnectionError('unexpected close')); }; onSshEnd = function() { sshClient.removeListener('ready', onSshReady); sshClient.removeListener('error', onSshError); sshClient.removeListener('close', onSshClose); return reject(new FtpConnectionError('unexpected end')); }; sshClient.once('ready', onSshReady); sshClient.once('error', onSshError); sshClient.once('close', onSshClose); sshClient.once('end', onSshEnd); return sshClient.connect(connectOptions); }).then(function(serverMessage) { closeSftpError = null; unexpectedClose = false; connectionStatus = STATUSES.CONNECTED; sftpClientContext.client.on('error', function(err) { return lastSftpError = err; }); sftpClientContext.client.on('close', function(hadError) { if (hadError) { closeSftpError = lastSftpError; } return sshClient.destroy(); }); return serverMessage; }); }; this.connect = function(options) { return continuePromise.then(function() { var _changePassword, _keyboardInteractive, key, value; if (connectionStatus !== STATUSES.NOT_YET_CONNECTED && connectionStatus !== STATUSES.DISCONNECTED) { throw new FtpConnectionError("can't connect when connection status is: '" + connectionStatus + "'"); } connectOptions = {}; for (key in options) { value = options[key]; connectOptions[key] = value; } autoReconnect = !!options.autoReconnect; delete connectOptions.autoReconnect; if (connectOptions.privateKeyFile && !connectOptions.privateKey) { connectOptions.privateKey = fs.readFileSync(connectOptions.privateKeyFile); } delete connectOptions.privateKeyFile; if (connectOptions.changePassword) { _changePassword = connectOptions.changePassword; changePassword = function(message, language, finish) { return Promise["try"](function() { return _changePassword(message, language); })["catch"](function(err) { return null; }).then(finish); }; } delete connectOptions.changePassword; if (connectOptions.tryKeyboard) { _keyboardInteractive = connectOptions.tryKeyboard; connectOptions.tryKeyboard = true; keyboardInteractive = function(name, instructions, instructionsLang, prompts, finish) { return Promise["try"](function() { return _keyboardInteractive(name, instructions, instructionsLang, prompts); })["catch"](function(err) { return null; }).all(finish); }; } if (connectOptions.user && !connectOptions.username) { connectOptions.username = connectOptions.user; } delete connectOptions.user; if (connectOptions.connTimeout && !connectOptions.readyTimeout) { connectOptions.readyTimeout = connectOptions.connTimeout; } delete connectOptions.connTimeout; if (connectOptions.pasvTimeout && !connectOptions.readyTimeout) { connectOptions.readyTimeout = connectOptions.pasvTimeout; } delete connectOptions.pasvTimeout; if (connectOptions.keepalive && !connectOptions.keepaliveInterval) { connectOptions.keepaliveInterval = connectOptions.keepalive; } delete connectOptions.keepalive; return _connect(STATUSES.CONNECTING); }); }; this.reconnect = function() { return continuePromise.then(function() { if (connectionStatus !== STATUSES.NOT_YET_CONNECTED && connectionStatus !== STATUSES.DISCONNECTED) { throw new FtpConnectionError("can't reconnect when connection status is: '" + connectionStatus + "'"); } return _connect(STATUSES.RECONNECTING); }); }; this.end = function() { return (autoReconnectPromise || Promise.resolve()).then(function() { return continuePromise; }).then(function() { if (connectionStatus === STATUSES.NOT_YET_CONNECTED || connectionStatus === STATUSES.DISCONNECTED || connectionStatus === STATUSES.DISCONNECTING) { throw new FtpConnectionError("can't end connection when connection status is: " + connectionStatus); } return new Promise(function(resolve, reject) { restartOffset = null; connectionStatus = STATUSES.DISCONNECTING; sshClient.once('close', function(hadError) { return resolve(hadError ? lastSshError || true : false); }); return sshClient.end(); }); }); }; this.logout = this.end; this.destroy = function() { var wasDisconnected; if (connectionStatus === STATUSES.NOT_YET_CONNECTED || connectionStatus === STATUSES.DISCONNECTED) { wasDisconnected = true; } else { wasDisconnected = false; connectionStatus = STATUSES.DISCONNECTING; } restartOffset = null; sshClient.destroy(); return wasDisconnected; }; this.getConnectionStatus = function() { return connectionStatus; }; this.restart = function(byteOffset) { return Promise["try"](function() { restartOffset = byteOffset; return void 0; }); }; this.list = function(path) { if (path == null) { path = '.'; } return promisifiedClientMethods.readdir(path).then(function(files) { var file, i, len, results; results = []; for (i = 0, len = files.length; i < len; i++) { file = files[i]; results.push({ type: file.longname.substr(0, 1), name: file.filename, size: file.attrs.size, date: new Date(file.attrs.mtime * 1000), rights: { user: file.longname.substr(1, 3).replace(/-/g, ''), group: file.longname.substr(4, 3).replace(/-/g, ''), other: file.longname.substr(7, 3).replace(/-/g, '') }, owner: file.attrs.uid, group: file.attrs.gid, target: null, sticky: null }); } return results; }); }; this.get = function(sourcePath, options) { if (options == null) { options = {}; } return Promise["try"](function() { if (restartOffset !== null) { options.start = options.start || restartOffset; options.flags = options.flags || 'r+'; restartOffset = null; } return promisifiedClientMethods.createReadStream(sourcePath, options).then(function(stream) { finishLogic(stream); return stream; }); }); }; this.put = function(input, destPath) { return Promise["try"](function() { var options; if (restartOffset !== null) { options = { start: restartOffset, flags: 'r+' }; restartOffset = null; } if (typeof input === 'string') { if (!options) { return promisifiedClientMethods.fastPut(input, destPath); } input = fs.createReadStream(input); } return promisifiedClientMethods.createWriteStream(destPath, options).then(function(stream) { finishLogic(stream); if (input instanceof Buffer) { return stream.end(input); } input.pipe(stream); return void 0; }); }); }; this.append = function(input, destPath) { return promisifiedClientMethods.createWriteStream(destPath, { flags: 'a' }).then(function(stream) { finishLogic(stream); if (input instanceof Buffer) { return stream.end(input); } else if (typeof input === 'string') { input = fs.createReadStream(input); } input.pipe(stream); return void 0; }); }; this["delete"] = promisifiedClientMethods.unlink; this.mkdir = (function(_this) { return function(dirPath, recursive, attributes) { var addMkdirJob, currPath, i, isFirst, len, result, token, tokens; if (typeof recursive === 'object') { attributes = recursive; recursive = false; } if (!recursive) { return promisifiedClientMethods.mkdir(dirPath, attributes); } result = Promise.resolve(); tokens = dirPath.split(/\//g); currPath = dirPath.charAt(0) === '/' ? '/' : ''; isFirst = true; for (i = 0, len = tokens.length; i < len; i++) { token = tokens[i]; if (isFirst) { currPath = "" + token; isFirst = false; } else { currPath = currPath + "/" + token; } if (token === '.' || token === '..') { continue; } addMkdirJob = function(newPath) { return _this.mkdir(newPath, false, attributes); }; result = result.then(addMkdirJob(currPath))["catch"](function(err) { if (err.code !== ERROR_CODES.FAILURE && err.code !== ERROR_CODES.FILE_ALREADY_EXISTS) { throw err; } }); } return result; }; })(this); this.listSafe = this.list; this.size = (function(_this) { return function(filePath) { return _this.stat(filePath).then(function(stats) { return stats.size; }); }; })(this); this.lastMod = (function(_this) { return function(filePath) { return _this.stat(filePath).then(function(stats) { return new Date(stats.mtime * 1000); }); }; })(this); this.read = function(handle, buffer, offset, length, position) { return promisifiedClientMethods.read(handle, buffer, offset, length, position).spread(function(bytesRead, buffer, position) { return { bytesRead: bytesRead, buffer: buffer, position: position }; }); }; this.wait = function() {}; commonLogicFactory = function(name, finishType, handler) { if (finishType === 'continue') { promisifiedClientMethods[name] = continueLogicFactory(sftpClientContext, name); } else if (finishType === 'callback') { promisifiedClientMethods[name] = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return Promise.promisify(sftpClientContext.client[name], sftpClientContext.client).apply(null, args); }; } else if (finishType === 'return') { promisifiedClientMethods[name] = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return Promise["try"](function() { var ref; return (ref = sftpClientContext.client)[name].apply(ref, args); }); }; } else if (finishType === 'none') { promisifiedClientMethods[name] = null; } else { throw new Error("Unrecognized finishType: " + finishType); } if (!handler) { handler = promisifiedClientMethods[name]; } return function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return Promise["try"](function() { var originalError; if (unexpectedClose && autoReconnect && !autoReconnectPromise) { originalError = closeSftpError || closeSshError; autoReconnectPromise = _connect(STATUSES.RECONNECTING)["catch"](function(err) { throw new FtpReconnectError(originalError, err, false); }); } if (autoReconnectPromise) { return autoReconnectPromise; } else if (connectionStatus !== STATUSES.CONNECTED) { throw new FtpConnectionError("can't perform '" + name + "' command when connection status is: " + connectionStatus); } }).then(function() { return continuePromise; }).then(function() { return handler.apply(null, args); }); }; }; for (name in simplePassthroughMethods) { finishType = simplePassthroughMethods[name]; this[name] = commonLogicFactory(name, finishType); } for (name in complexPassthroughMethods) { finishType = complexPassthroughMethods[name]; this[name] = commonLogicFactory(name, finishType, this[name]); } } ref = [Object.keys(simplePassthroughMethods), Object.keys(complexPassthroughMethods), otherPrototypeMethods]; for (i = 0, len = ref.length; i < len; i++) { methodList = ref[i]; for (j = 0, len1 = methodList.length; j < len1; j++) { methodName = methodList[j]; PromiseSftp.prototype[methodName] = null; } } return PromiseSftp; })(); module.exports = PromiseSftp; }).call(this);