promise-sftp
Version:
a promise-based sftp client for node.js
554 lines (531 loc) • 20.7 kB
JavaScript
// 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);