UNPKG

ftps

Version:

FTP, FTPS and SFTP client for node.js, mainly a lftp wrapper.

292 lines (267 loc) 9.92 kB
'use strict' var spawn = require('child_process').spawn var _ = require('lodash') var dcp = require('duplex-child-process') /* ** Params : ** { ** host: 'domain.com', // Required ** username: 'Test', // Optional. Use empty username for anonymous access. ** password: 'Test', // Required if username is not empty, except when requiresPassword: false, ** bareCredentials: 'Test,Test$T', // Used instead of username/password for special cases where passwords should not be escaped ** protocol: 'sftp', // Optional, values : 'ftp', 'sftp', 'ftps', ... default: 'ftp' ** // protocol is added on beginning of host, ex : sftp://domain.com in this case ** port: '22', // Optional ** // port is added at the end of the host, ex : sftp://domain.com:28 in this case ** escape: true, // optional, used for escaping shell characters (space, $, etc.), default: true ** retries: 2, // Optional, defaults to 1 (1 = no retries, 0 = unlimited retries) ** timeout: 10, // Optional, Time before failing a connection attempt. Defaults to 10 ** retryInterval: 5, // Optional, Time in seconds between attempts. Defaults to 5 ** retryMultiplier: 1, // Optional, Multiplier by which retryInterval is multiplied each time new attempt fails. Defaults to 1 ** requiresPassword: true, // Optional, defaults to true ** autoConfirm: true, // Optional, defaults to false ** cwd: '', // Optional, defaults to the directory from where the script is executed ** additionalLftpCommands: '', // Additional commands to pass to lftp, splitted by ';' ** requireSSHKey: false, // This option for SFTP Protocol with ssh key authentication ** sshKeyPath: '', // ssh key path for, SFTP Protocol with ssh key authentication ** sshKeyOptions: '' // ssh key options such as 'StrictHostKeyChecking=no' ** } ** ** Usage : ** ftp.cd('some_directory').rm('./test.txt').exec(console.log) */ var FTP = function (options) { this.initialize(options) this.cmds = [] } FTP.prototype.initialize = function (options) { var defaults = { host: '', protocol: 'ftp', username: '', password: '', bareCredentials: '', escape: true, retries: 1, // LFTP by default tries an unlimited amount of times so we change that here timeout: 10, // Time before failing a connection attempt retryInterval: 5, // Time in seconds between attempts retryIntervalMultiplier: 1, // Multiplier by which retryInterval is multiplied each time new attempt fails requiresPassword: true, // Supports Anonymous FTP autoConfirm: false, // Auto confirm ssl certificate, cwd: '', // Use a different working directory additionalLftpCommands: '', // Additional commands to pass to lftp, splitted by ';' requireSSHKey: false, // This option for SFTP Protocol with ssh key authentication sshKeyPath: '', // ssh key path for, SFTP Protocol with ssh key authentication\ sshKeyOptions: '' // ssh key options such as 'StrictHostKeyChecking=no' } // Extend options with defaults var opts = _.pick(_.extend(defaults, options), 'host', 'username', 'password', 'bareCredentials', 'port', 'escape', 'retries', 'timeout', 'retryInterval', 'retryIntervalMultiplier', 'requiresPassword', 'protocol', 'autoConfirm', 'cwd', 'additionalLftpCommands', 'requireSSHKey', 'sshKeyPath', 'sshKeyOptions') // Validation if (!opts.host) throw new Error('You need to set a host.') if (opts.username && opts.requiresPassword === true && !opts.password && !opts.bareCredentials) throw new Error('You need to set a password.') if (opts.protocol && typeof opts.protocol !== 'string') throw new Error('Protocol needs to be of type string.') if (opts.requireSSHKey === true && !opts.sshKeyPath) throw new Error('You need to set a ssh key path.'); // Setting options if (opts.protocol && opts.host.indexOf(opts.protocol + '://') !== 0) { opts.host = opts.protocol + '://' + opts.host } if (opts.port) { opts.host = opts.host + ':' + opts.port } this.options = opts } FTP.prototype.escapeshell = function (cmd) { if (typeof cmd !== 'string') { return '' } return cmd.replace(/([&"\s'$`\\;])/g, '\\$1') } FTP.prototype._escapeshell = function (cmd) { if (this.options.escape) { return this.escapeshell(cmd) } return cmd } FTP.prototype.prepareLFTPOptions = function () { var opts = [] // Only support SFTP or FISH for ssl autoConfirm if ((this.options.protocol.toLowerCase() === 'sftp' || this.options.protocol.toLowerCase() === 'fish') && this.options.autoConfirm) { opts.push('set ' + this.options.protocol.toLowerCase() + ':auto-confirm yes') } // Only support SFTP for openSSH key authentication if(this.options.protocol.toLowerCase() === "sftp" && this.options.requireSSHKey){ var extra = (this.options.sshKeyOptions || '') .split(' ') .filter(Boolean) .map(function (extra) { return ' -o ' + extra }) .join('') opts.push('set sftp:connect-program "ssh' + extra + ' -a -x -i '+this.options.sshKeyPath+'"') } opts.push('set net:max-retries ' + this.options.retries) opts.push('set net:timeout ' + this.options.timeout) opts.push('set net:reconnect-interval-base ' + this.options.retryInterval) opts.push('set net:reconnect-interval-multiplier ' + this.options.retryIntervalMultiplier) opts.push(this.options.additionalLftpCommands) var open = 'open' if (this.options.bareCredentials) { open += ' -u \'' + this.options.bareCredentials + '\'' } else if (this.options.username) { open += ' -u "' + this._escapeshell(this.options.username) + '","' + this._escapeshell(this.options.password) + '"' } open += ' "' + this.options.host + '"' opts.push(open) return opts } FTP.prototype.exec = function (cmds, callback) { if (typeof cmds === 'string') { cmds = cmds.split(';') } if (Array.isArray(cmds)) { this.cmds = this.cmds.concat(cmds) } if (typeof cmds === 'function' && !callback) { callback = cmds } if (!callback) { throw new Error('Callback is missing to exec() function.') } var cmd = this.prepareLFTPOptions().concat(this.cmds).join(';') var spawnoptions this.cmds = [] if (this.options.cwd) { spawnoptions = { cwd: this.options.cwd } } var lftp = spawn('lftp', ['-c', cmd], spawnoptions) var data = '' var error = '' lftp.stdout.on('data', function (res) { data += res }) lftp.stderr.on('data', function (res) { error += res }) lftp.on('error', function (err) { if (callback) { callback(err, { error: error || null, data: data }) } callback = null // Make sure callback is only called once, whether 'exit' event is triggered or not. }) lftp.on('close', function (code) { if (callback) { callback(null, { error: error || null, data: data }) } }) return lftp } FTP.prototype.execAsStream = function (cmds) { if (typeof cmds === 'string') { cmds = cmds.split(';') } if (Array.isArray(cmds)) { this.cmds = this.cmds.concat(cmds) } var cmd = this.prepareLFTPOptions().concat(this.cmds).join(';') var spawnoptions this.cmds = [] if (this.options.cwd) { spawnoptions = { cwd: this.options.cwd } } return dcp.spawn('lftp', ['-c', cmd], spawnoptions) } FTP.prototype.raw = function (cmd) { if (cmd && typeof cmd === 'string') { this.cmds.push(cmd) } return this } FTP.prototype.ls = function () { return this.raw('ls') } FTP.prototype.pwd = function () { return this.raw('pwd') } FTP.prototype.cd = function (directory) { return this.raw('cd ' + this._escapeshell(directory)) } FTP.prototype.cat = function (path) { return this.raw('cat ' + this._escapeshell(path)) } FTP.prototype.put = function (localPath, remotePath) { if (!localPath) { return this } if (!remotePath) { return this.raw('put ' + this._escapeshell(localPath)) } return this.raw('put ' + this._escapeshell(localPath) + ' -o ' + this._escapeshell(remotePath)) } FTP.prototype.addFile = FTP.prototype.put FTP.prototype.get = function (remotePath, localPath) { if (!remotePath) { return this } if (!localPath) { return this.raw('get ' + this._escapeshell(remotePath)) } return this.raw('get ' + this._escapeshell(remotePath) + ' -o ' + this._escapeshell(localPath)) } FTP.prototype.getFile = FTP.prototype.get FTP.prototype.mv = function (from, to) { if (!from || !to) { return this } return this.raw('mv ' + this._escapeshell(from) + ' ' + this._escapeshell(to)) } FTP.prototype.move = FTP.prototype.mv FTP.prototype.rm = function () { return this.raw('rm ' + Array.prototype.slice.call(arguments).map(this._escapeshell.bind(this)).join(' ')) } FTP.prototype.remove = FTP.prototype.rm FTP.prototype.rmdir = function () { return this.raw('rmdir ' + Array.prototype.slice.call(arguments).map(this._escapeshell.bind(this)).join(' ')) } FTP.prototype.mirror = function (opts) { opts = opts || {} opts.remoteDir = opts.remoteDir || '.' opts.localDir = opts.localDir || '.' opts.options = opts.options || '' var raw = 'mirror' if (opts.upload === true) { raw += ' -R' } if (opts.parallel === true) { raw += ' --parallel' } if (typeof opts.parallel === 'number' && opts.parallel >= 1) { raw += ' --parallel=' + parseInt(opts.parallel, 10) } if (opts.options) { raw += ' ' + opts.options } if (opts.filter) { raw += ' -i "' + String(opts.filter).slice(1, -1) + '"' } if (opts.upload === true) { raw += ' ' + this._escapeshell(opts.localDir) + ' ' + this._escapeshell(opts.remoteDir) } else { raw += ' ' + this._escapeshell(opts.remoteDir) + ' ' + this._escapeshell(opts.localDir) } return this.raw(raw) } FTP.prototype.mkdir = function (directory, mode) { return this.raw('mkdir ' + mode + ' ' + directory) }; module.exports = FTP