Comparing version 0.1.7 to 0.2.0
1126
ftp.js
@@ -1,4 +0,4 @@ | ||
var util = require('util'), | ||
net = require('net'), | ||
var Socket = require('net').Socket, | ||
EventEmitter = require('events').EventEmitter, | ||
inherits = require('util').inherits, | ||
XRegExp = require('./xregexp'); | ||
@@ -9,450 +9,434 @@ | ||
reXTimeval = XRegExp.cache('^(?<year>\\d{4})(?<month>\\d{2})(?<date>\\d{2})(?<hour>\\d{2})(?<minute>\\d{2})(?<second>\\d+)$'), | ||
reKV = /(.+?)=(.+?);/; | ||
rePASV = /([\d]+),([\d]+),([\d]+),([\d]+),([-\d]+),([-\d]+)/, | ||
reEOL = /\r?\n/g, | ||
reResEnd = /(?:^|\r?\n)(\d{3}) [^\r\n]*\r?\n$/;/*, | ||
reRmLeadCode = /(^|\r?\n)\d{3}(?: |\-)/g;*/ | ||
var MONTHS = { | ||
jan: 1, | ||
feb: 2, | ||
mar: 3, | ||
apr: 4, | ||
may: 5, | ||
jun: 6, | ||
jul: 7, | ||
aug: 8, | ||
sep: 9, | ||
oct: 10, | ||
nov: 11, | ||
dec: 12 | ||
}; | ||
jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, | ||
jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 | ||
}, | ||
TYPE = { | ||
SYNTAX: 0, | ||
INFO: 1, | ||
SOCKETS: 2, | ||
AUTH: 3, | ||
UNSPEC: 4, | ||
FILESYS: 5 | ||
}, | ||
RETVAL = { | ||
PRELIM: 1, | ||
OK: 2, | ||
WAITING: 3, | ||
ERR_TEMP: 4, | ||
ERR_PERM: 5 | ||
}, | ||
ERRORS = { | ||
421: 'Service not available, closing control connection', | ||
425: 'Can\'t open data connection', | ||
426: 'Connection closed; transfer aborted', | ||
450: 'Requested file action not taken / File unavailable (e.g., file busy)', | ||
451: 'Requested action aborted: local error in processing', | ||
452: 'Requested action not taken / Insufficient storage space in system', | ||
500: 'Syntax error / Command unrecognized', | ||
501: 'Syntax error in parameters or arguments', | ||
502: 'Command not implemented', | ||
503: 'Bad sequence of commands', | ||
504: 'Command not implemented for that parameter', | ||
530: 'Not logged in', | ||
532: 'Need account for storing files', | ||
550: 'Requested action not taken / File unavailable (e.g., file not found, no access)', | ||
551: 'Requested action aborted: page type unknown', | ||
552: 'Requested file action aborted / Exceeded storage allocation (for current directory or dataset)', | ||
553: 'Requested action not taken / File name not allowed' | ||
}, | ||
bytesCRLF = new Buffer([13, 10]), | ||
bytesNOOP = new Buffer('NOOP\r\n'); | ||
var FTP = module.exports = function(options) { | ||
var FTP = module.exports = function() { | ||
this._socket = undefined; | ||
this._dataSock = undefined; | ||
this._state = undefined; | ||
this._pasvPort = undefined; | ||
this._pasvIP = undefined; | ||
this._pasvSock = undefined; | ||
this._feat = undefined; | ||
this._curReq = undefined; | ||
this._queue = []; | ||
this.debug = false; | ||
this._buffer = ''; | ||
this._debug = undefined; | ||
this.options = { | ||
host: 'localhost', | ||
port: 21, | ||
/*secure: false,*/ | ||
connTimeout: 15000, // in ms | ||
debug: false | ||
host: undefined, | ||
port: undefined, | ||
user: undefined, | ||
password: undefined, | ||
secure: false, | ||
connTimeout: undefined, | ||
pasvTimeout: undefined, | ||
keepalive: undefined | ||
}; | ||
extend(true, this.options, options); | ||
if (typeof this.options.debug === 'function') | ||
this.debug = this.options.debug; | ||
this.connected = false; | ||
}; | ||
util.inherits(FTP, EventEmitter); | ||
inherits(FTP, EventEmitter); | ||
FTP.prototype.connect = function(port, host) { | ||
var self = this, | ||
socket = this._socket, | ||
curData = ''; | ||
if (typeof port === 'string') | ||
this.options.host = port; | ||
else if (typeof port === 'number') | ||
this.options.port = port; | ||
if (host !== undefined) | ||
this.options.host = host; | ||
FTP.prototype.connect = function(options) { | ||
var self = this; | ||
if (typeof options !== 'object') | ||
options = {}; | ||
this.connected = false; | ||
this.options.host = options.host || 'localhost'; | ||
this.options.port = options.port || 21; | ||
this.options.user = options.user || 'anonymous'; | ||
this.options.password = options.password || 'anonymous@'; | ||
this.options.secure = options.secure || false; | ||
this.options.connTimeout = options.connTimeout || 10000; | ||
this.options.pasvTimeout = options.pasvTimeout || 10000; | ||
this.options.keepalive = options.keepalive || 10000; | ||
if (typeof options.debug === 'function') | ||
this._debug = options.debug; | ||
host = this.options.host; | ||
port = this.options.port; | ||
this._socket = new Socket(); | ||
this._feat = {}; | ||
this._socket.setTimeout(0); | ||
if (socket) | ||
socket.end(); | ||
if (this._dataSock) | ||
this._dataSock.end(); | ||
var connTimeout = setTimeout(function() { | ||
var timer = setTimeout(function() { | ||
self._socket.destroy(); | ||
self._socket = undefined; | ||
self.emit('timeout'); | ||
self._reset(); | ||
self.emit('error', new Error('Timeout while connecting to server')); | ||
}, this.options.connTimeout); | ||
socket = this._socket = new net.Socket(); | ||
socket.setEncoding('binary'); | ||
socket.setTimeout(0); | ||
socket.on('connect', function() { | ||
clearTimeout(connTimeout); | ||
self.debug&&self.debug('Connected'); | ||
}); | ||
socket.on('end', function() { | ||
self.debug&&self.debug('Disconnected'); | ||
if (self._dataSocket) | ||
self._dataSocket.end(); | ||
self.emit('end'); | ||
}); | ||
socket.on('close', function(hasError) { | ||
clearTimeout(connTimeout); | ||
if (self._dataSocket) | ||
self._dataSocket.end(); | ||
self.emit('close', hasError); | ||
}); | ||
socket.on('error', function(err) { | ||
self.emit('error', err); | ||
}); | ||
socket.on('data', function(data) { | ||
curData += data; | ||
if (/(?:\r\n|\n)$/.test(curData)) { | ||
var resps = parseResponses(curData.split(/\r\n|\n/)), processNext = false; | ||
if (resps.length === 0) | ||
return; | ||
curData = ''; | ||
if (self.debug) { | ||
for (var i=0,len=resps.length; i<len; ++i) { | ||
self.debug('Response: code = ' + resps[i][0] | ||
+ (resps[i][1] ? '; text = ' + util.inspect(resps[i][1]) | ||
: '')); | ||
var keepalive, | ||
noopreq = { | ||
cmd: 'NOOP', | ||
cb: function() { | ||
keepalive = setTimeout(donoop, self.options.keepalive); | ||
} | ||
} | ||
}; | ||
for (var i=0,code,text,group,len=resps.length; i<len; ++i) { | ||
code = resps[i][0]; | ||
text = resps[i][1]; | ||
group = getGroup(code); // second digit | ||
function donoop() { | ||
if (!self._socket || !self._socket.writable) | ||
clearTimeout(keepalive); | ||
else if (!self._curReq && self._queue.length === 0) { | ||
self._curReq = noopreq; | ||
self._socket.write(bytesNOOP); | ||
} else | ||
keepalive = setTimeout(donoop, self.options.keepalive); | ||
} | ||
if (!self._state) { | ||
if (code === 220) { | ||
self._state = 'connected'; | ||
self.send('FEAT', function(e, text) { | ||
if (!e && /\r\n|\n/.test(text)) { | ||
var feats = text.split(/\r\n|\n/); | ||
feats.shift(); // "Features:" | ||
feats.pop(); // "End" | ||
for (var i=0,sp,len=feats.length; i<len; ++i) { | ||
feats[i] = feats[i].trim(); | ||
if ((sp = feats[i].indexOf(' ')) > -1) | ||
self._feat[feats[i].substring(0, sp).toUpperCase()] = feats[i].substring(sp+1); | ||
else | ||
self._feat[feats[i].toUpperCase()] = true; | ||
} | ||
self.debug&&self.debug('Features: ' + util.inspect(self._feat)); | ||
} | ||
self.emit('connect'); | ||
}); | ||
} else | ||
self.emit('error', new Error('Did not receive service ready response')); | ||
return; | ||
this._socket.once('connect', function() { | ||
clearTimeout(timer); | ||
self.connected = true; | ||
var cmd; | ||
self._curReq = { | ||
cmd: '', | ||
cb: function reentry(err, text, code) { | ||
// not all servers are required to support FEAT (RFC 2389) | ||
if (err && cmd !== 'FEAT') { | ||
self.emit('error', err); | ||
return self._socket.end(); | ||
} | ||
if (code >= 200 && !processNext) | ||
processNext = true; | ||
else if (code < 200) | ||
continue; | ||
if (!cmd) { | ||
// sometimes the initial greeting can contain useful information | ||
// about authorized use, other limits, etc. | ||
self.emit('greeting', text); | ||
if (group === 0) { | ||
// all in here are errors except 200 | ||
if (code === 200) | ||
self._callCb(); | ||
else | ||
self._callCb(makeError(code, text)); | ||
} else if (group === 1) { | ||
// informational group | ||
if (code >= 211 && code <= 215) | ||
self._callCb(text); | ||
else | ||
self._callCb(makeError(code, text)); | ||
} else if (group === 2) { | ||
// control/data connection-related | ||
if (code === 226) { | ||
// closing data connection, file action request successful | ||
self._callCb(); | ||
} else if (code === 227) { | ||
// server entering passive mode | ||
var parsed = text.match(/([\d]+),([\d]+),([\d]+),([\d]+),([-\d]+),([-\d]+)/); | ||
if (!parsed) | ||
throw new Error('Could not parse passive mode response: ' + text); | ||
self._pasvIP = parsed[1] + '.' + parsed[2] + '.' + parsed[3] + '.' | ||
+ parsed[4]; | ||
self._pasvPort = (parseInt(parsed[5], 10) * 256) | ||
+ parseInt(parsed[6], 10); | ||
self._pasvConnect(); | ||
return; | ||
} else | ||
self._callCb(makeError(code, text)); | ||
} else if (group === 3) { | ||
// authentication-related | ||
if (code === 331 || code === 230) | ||
self._callCb((code === 331)); | ||
else | ||
self._callCb(makeError(code, text)); | ||
} else if (group === 5) { // group 4 is unused | ||
// server file system state | ||
if (code === 250 && self._queue[0][0] === 'MLST') | ||
self._callCb(text); | ||
else if (code === 250 || code === 350) | ||
self._callCb(); | ||
else if (code === 257) { | ||
var path = text.match(/(?:^|\s)\"(.*)\"(?:$|\s)/); | ||
if (path) | ||
path = path[1].replace(/\"\"/g, '"'); | ||
else | ||
path = text; | ||
self._callCb(path); | ||
} else | ||
self._callCb(makeError(code, text)); | ||
cmd = 'USER'; | ||
self._send('USER ' + self.options.user, reentry); | ||
} else if (cmd === 'USER') { | ||
if (code === 331 && !self.options.password) { | ||
self.emit('error', makeError('Password required', code)); | ||
return self._socket.end(); | ||
} | ||
cmd = 'PASS'; | ||
self._send('PASS ' + self.options.password, reentry); | ||
} else if (cmd === 'PASS') { | ||
cmd = 'FEAT'; | ||
self._send(cmd, reentry); | ||
} else if (cmd === 'FEAT') { | ||
if (!err) | ||
self._parseFeat(text); | ||
cmd = 'TYPE'; | ||
self._send('TYPE I', reentry); | ||
} else if (cmd === 'TYPE') { | ||
keepalive = setTimeout(donoop, self.options.keepalive); | ||
self.emit('ready'); | ||
} | ||
} | ||
if (processNext) | ||
self.send(); | ||
}; | ||
}); | ||
this._socket.setEncoding('binary'); | ||
this._socket.on('data', function(chunk) { | ||
self._buffer += chunk; | ||
var m; | ||
if (m = reResEnd.exec(self._buffer)) { | ||
var code, retval, reRmLeadCode; | ||
// we have a terminating response line | ||
code = parseInt(m[1], 10); | ||
reEOL.lastIndex = 0; | ||
//var isML = (reEOL.test(self._buffer) && reEOL.test(self._buffer)); | ||
reEOL.lastIndex = 0; | ||
retval = code / 100 >> 0; | ||
// RFC 959 does not require each line in a multi-line response to begin | ||
// with '<code>-', but many servers will do this. | ||
// | ||
// remove this leading '<code>-' (or '<code> ' from last line) from each | ||
// line in the response ... | ||
reRmLeadCode = '(^|\\r?\\n)'; | ||
reRmLeadCode += m[1]; | ||
reRmLeadCode += '(?: |\\-)'; | ||
reRmLeadCode = RegExp(reRmLeadCode, 'g'); | ||
self._buffer = self._buffer.replace(reRmLeadCode, '$1').trim(); | ||
if (retval === RETVAL.ERR_TEMP || retval === RETVAL.ERR_PERM) | ||
self._curReq.cb(makeError(self._buffer, code)); | ||
else | ||
self._curReq.cb(undefined, self._buffer, code); | ||
self._buffer = ''; | ||
// a hack to signal we're waiting for a PASV data connection to complete | ||
// first before executing any more queued requests ... | ||
// | ||
// also: don't forget our current request if we're expecting another | ||
// terminating response .... | ||
if (self._curReq && retval !== RETVAL.PRELIM) { | ||
self._curReq = undefined; | ||
self._send(); | ||
} | ||
} | ||
}); | ||
socket.connect(port, host); | ||
this._socket.once('error', function(err) { | ||
clearTimeout(timer); | ||
self.emit('error', err); | ||
}); | ||
var hasReset = false; | ||
this._socket.once('end', function() { | ||
clearTimeout(timer); | ||
self.connected = false; | ||
self._reset(); | ||
hasReset = true; | ||
self.emit('end'); | ||
}); | ||
this._socket.once('close', function(had_err) { | ||
clearTimeout(timer); | ||
self.connected = false; | ||
if (!hasReset) | ||
self._reset(); | ||
self.emit('close', had_err); | ||
}); | ||
this._socket.connect(this.options.port, this.options.host); | ||
}; | ||
FTP.prototype.end = function() { | ||
if (this._socket) | ||
if (this._socket && this._socket.writable) | ||
this._socket.end(); | ||
if (this._dataSock) | ||
this._dataSock.end(); | ||
if (this._pasvSock && this._pasvSock.writable) | ||
this._pasvSock.end(); | ||
this._socket = undefined; | ||
this._dataSock = undefined; | ||
this._pasvSock = undefined; | ||
}; | ||
/* Standard features */ | ||
FTP.prototype.auth = function(user, password, callback) { | ||
if (this._state !== 'connected') | ||
return false; | ||
if (typeof user === 'function') { | ||
callback = user; | ||
user = 'anonymous'; | ||
password = 'anonymous@'; | ||
} else if (typeof password === 'function') { | ||
callback = password; | ||
password = 'anonymous@'; | ||
// "Standard" (RFC 959) commands | ||
FTP.prototype.abort = function(immediate, cb) { | ||
if (typeof immediate === 'function') { | ||
cb = immediate; | ||
immediate = true; | ||
} | ||
var cmds = [['USER', user], ['PASS', password]], cur = 0, self = this, | ||
cb = function(err, result) { | ||
if (err) { | ||
callback(err); | ||
return; | ||
} | ||
if (result === true) { | ||
if (!self.send(cmds[cur][0], cmds[cur][1], cb)) | ||
return callback(new Error('Connection severed')); | ||
++cur; | ||
} else if (result === false) { | ||
// logged in | ||
cur = 0; | ||
self._state = 'authorized'; | ||
if (!self.send('TYPE', 'I', callback)) | ||
return callback(new Error('Connection severed')); | ||
} | ||
}; | ||
cb(undefined, true); | ||
return true; | ||
if (immediate) | ||
this._send('ABOR', cb, true); | ||
else | ||
this._send('ABOR', cb); | ||
}; | ||
FTP.prototype.pwd = function(cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('PWD', cb); | ||
FTP.prototype.cwd = function(path, cb) { | ||
this._send('CWD ' + path, cb); | ||
}; | ||
FTP.prototype.cwd = function(path, cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('CWD', path, cb); | ||
FTP.prototype.delete = function(path, cb) { | ||
this._send('DELE' + path, cb); | ||
}; | ||
FTP.prototype.cdup = function(cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('CDUP', cb); | ||
FTP.prototype.status = function(cb) { | ||
this._send('STAT', cb); | ||
}; | ||
FTP.prototype.get = function(path, cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
FTP.prototype.rename = function(from, to, cb) { | ||
var self = this; | ||
return this.send('PASV', function(e, stream) { | ||
if (e) | ||
return cb(e); | ||
this._send('RNFR' + from, function(err) { | ||
if (err) | ||
return cb(err); | ||
stream._decoder = undefined; | ||
var r = self.send('RETR', path, function(e) { | ||
if (e) | ||
return stream.emit('error', e); | ||
stream.emit('success'); | ||
}); | ||
if (r) | ||
cb(undefined, stream); | ||
else | ||
cb(new Error('Connection severed')); | ||
self._send('RNTO ' + to, cb); | ||
}); | ||
}; | ||
FTP.prototype.put = function(input, destpath, cb) { | ||
var isBuffer = Buffer.isBuffer(input); | ||
if (this._state !== 'authorized' || (!isBuffer && !input.readable)) | ||
return false; | ||
FTP.prototype.list = function(path, cb) { | ||
var self = this, cmd; | ||
if (!isBuffer) | ||
input.pause(); | ||
if (typeof path === 'function') { | ||
cb = path; | ||
path = undefined; | ||
cmd = 'LIST'; | ||
} else | ||
cmd = 'LIST ' + path; | ||
var self = this; | ||
return this.send('PASV', function(e, outstream) { | ||
if (e) | ||
return cb(e); | ||
this._pasv(function(err, sock) { | ||
if (err) | ||
return cb(err); | ||
outstream._decoder = undefined; | ||
var r = self.send('STOR', destpath, cb); | ||
if (r) { | ||
if (!isBuffer) { | ||
input.pipe(outstream); | ||
input.resume(); | ||
} else | ||
outstream.end(input); | ||
} else | ||
cb(new Error('Connection severed')); | ||
}); | ||
}; | ||
if (self._queue[0] && self._queue[0].cmd === 'ABOR') | ||
return cb(); | ||
FTP.prototype.append = function(input, destpath, cb) { | ||
var isBuffer = Buffer.isBuffer(input); | ||
if (this._state !== 'authorized' || (!isBuffer && !input.readable)) | ||
return false; | ||
var sockerr, done = false, lastreply = false, entries, buffer = ''; | ||
if (!isBuffer) | ||
input.pause(); | ||
sock.setEncoding('binary'); | ||
sock.on('data', function(chunk) { | ||
buffer += chunk; | ||
}); | ||
sock.once('error', function(err) { | ||
if (!sock.aborting) | ||
sockerr = err; | ||
}); | ||
sock.once('end', ondone); | ||
sock.once('close', ondone); | ||
var self = this; | ||
return this.send('PASV', function(e, outstream) { | ||
if (e) | ||
return cb(e); | ||
function ondone() { | ||
if (!done) { | ||
done = true; | ||
if (lastreply) { | ||
if (sockerr) | ||
return cb(new Error('Unexpected data connection error: ' + sockerr)); | ||
if (sock.aborting) | ||
return cb(); | ||
var r = self.send('APPE', destpath, cb); | ||
if (r) { | ||
if (!isBuffer) { | ||
input.resume(); | ||
input.pipe(outstream); | ||
} else | ||
outstream.end(input); | ||
// process received data | ||
entries = buffer.split(reEOL); | ||
entries.pop(); // ending EOL | ||
for (var i = 0, len = entries.length; i < len; ++i) | ||
entries[i] = parseListEntry(entries[i]); | ||
cb(undefined, entries); | ||
} | ||
} | ||
} | ||
else | ||
cb(new Error('Connection severed')); | ||
// this callback will be executed multiple times, the first is when server | ||
// replies with 150 and then a final reply to indicate whether the transfer | ||
// was actually a success or not | ||
self._send(cmd, function(err, text, code) { | ||
if (err) | ||
return cb(err); | ||
if (code !== 150) { | ||
lastreply = true; | ||
ondone(); | ||
} | ||
}); | ||
}); | ||
}; | ||
FTP.prototype.mkdir = function(path, cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('MKD', path, cb); | ||
}; | ||
FTP.prototype.get = function(path, cb) { | ||
var self = this; | ||
this._pasv(function(err, sock) { | ||
if (err) | ||
return cb(err); | ||
FTP.prototype.rmdir = function(path, cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('RMD', path, cb); | ||
}; | ||
if (self._queue[0] && self._queue[0].cmd === 'ABOR') | ||
return cb(); | ||
FTP.prototype.delete = function(path, cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('DELE', path, cb); | ||
}; | ||
// modify behavior of socket events so that we can emit 'error' once for | ||
// either a TCP-level error OR an FTP-level error response that we get when | ||
// the socket is closed (e.g. the server ran out of space). | ||
var sockerr, started = false, lastreply = false, done = false; | ||
sock._emit = sock.emit; | ||
sock.emit = function(ev, arg1) { | ||
if (ev === 'error') { | ||
sockerr = err; | ||
return; | ||
} else if (ev === 'end' || ev === 'close') { | ||
if (!done) { | ||
done = true; | ||
ondone(); | ||
} | ||
return; | ||
} | ||
sock._emit.apply(sock, Array.prototype.slice.call(arguments)); | ||
}; | ||
FTP.prototype.rename = function(pathFrom, pathTo, cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
function ondone() { | ||
if (done && lastreply) { | ||
sock._emit('end'); | ||
sock._emit('close'); | ||
} | ||
} | ||
var self = this; | ||
return this.send('RNFR', pathFrom, function(e) { | ||
if (e) | ||
return cb(e); | ||
sock.pause(); | ||
if (!self.send('RNTO', pathTo, cb)) | ||
cb(new Error('Connection severed')); | ||
// this callback will be executed multiple times, the first is when server | ||
// replies with 150, then a final reply after the data connection closes | ||
// to indicate whether the transfer was actually a success or not | ||
self._send('RETR ' + path, function(err, text, code) { | ||
if (sockerr || err) { | ||
if (!started) | ||
cb(sockerr || err); | ||
else { | ||
sock._emit('error', sockerr || err); | ||
sock._emit('close', true); | ||
} | ||
return; | ||
} | ||
if (code === 150) { | ||
started = true; | ||
cb(undefined, sock); | ||
sock.resume(); | ||
} else { | ||
lastreply = true; | ||
ondone(); | ||
} | ||
}); | ||
}); | ||
}; | ||
FTP.prototype.system = function(cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('SYST', cb); | ||
FTP.prototype.put = function(input, path, cb) { | ||
this._store('STOR ' + path, input, cb); | ||
}; | ||
FTP.prototype.status = function(cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
return this.send('STAT', cb); | ||
FTP.prototype.append = function(input, path, cb) { | ||
this._store('APPE ' + path, input, cb); | ||
}; | ||
FTP.prototype.list = function(path, streaming, cb) { | ||
if (this._state !== 'authorized') | ||
return false; | ||
FTP.prototype.pwd = function(cb) { // PWD is optional | ||
this._send('PWD', cb); | ||
}; | ||
if (typeof path === 'function') { | ||
cb = path; | ||
path = undefined; | ||
streaming = false; | ||
} else if (typeof path === 'boolean') { | ||
cb = streaming; | ||
streaming = path; | ||
path = undefined; | ||
} | ||
if (typeof streaming === 'function') { | ||
cb = streaming; | ||
streaming = false; | ||
} | ||
FTP.prototype.cdup = function(cb) { // CDUP is optional | ||
this._send('CDUP', cb); | ||
}; | ||
var self = this, | ||
emitter = new EventEmitter(); | ||
this._pasvGetLines(emitter, 'LIST', function(e) { | ||
if (e) | ||
return cb(e); | ||
var cbTemp = function(e) { | ||
if (e) | ||
return emitter.emit('error', e); | ||
emitter.emit('success'); | ||
}, r; | ||
if (path) | ||
r = self.send('LIST', path, cbTemp); | ||
else | ||
r = self.send('LIST', cbTemp); | ||
if (r) { | ||
if (!streaming) { | ||
var entries = []; | ||
emitter.on('entry', function(entry) { | ||
entries.push(entry); | ||
}); | ||
emitter.on('raw', function(line) { | ||
entries.push(line); | ||
}); | ||
emitter.on('success', function() { | ||
cb(undefined, entries); | ||
}); | ||
emitter.on('error', function(err) { | ||
cb(err); | ||
}); | ||
} else | ||
cb(undefined, emitter); | ||
} else | ||
cb(new Error('Connection severed')); | ||
}); | ||
FTP.prototype.mkdir = function(path, cb) { // MKD is optional | ||
this._send('MKD' + path, cb); | ||
}; | ||
/* Extended features */ | ||
FTP.prototype.rmdir = function(path, cb) { // RMD is optional | ||
this._send('RMD' + path, cb); | ||
}; | ||
FTP.prototype.system = function(cb) { // SYST is optional | ||
this._send('SYST', cb); | ||
}; | ||
// "Extended" (RFC 3659) commands | ||
FTP.prototype.size = function(path, cb) { | ||
if (this._state !== 'authorized' || !this._feat.SIZE) | ||
return false; | ||
return this.send('SIZE', path, cb); | ||
this._send('SIZE ' + path, function(err, text) { | ||
if (err) | ||
return cb(err); | ||
cb(undefined, parseInt(text, 10)); | ||
}); | ||
}; | ||
FTP.prototype.lastMod = function(path, cb) { | ||
if (this._state !== 'authorized' || !this._feat.MDTM) | ||
return false; | ||
return this.send('MDTM', path, function(e, text) { | ||
if (e) | ||
return cb(e); | ||
var val = reXTimeval.exec(text), | ||
ret; | ||
this._send('MDTM ' + path, function(err, text, code) { | ||
if (err) | ||
return cb(err); | ||
var val = reXTimeval.exec(text), ret; | ||
if (!val) | ||
@@ -470,190 +454,186 @@ return cb(new Error('Invalid date/time format from server')); | ||
FTP.prototype.restart = function(offset, cb) { | ||
if (this._state !== 'authorized' || !this._feat.REST | ||
|| !(/STREAM/i.test(this._feat.REST))) | ||
return false; | ||
return this.send('REST', offset, cb); | ||
this._send('REST ' + offset, cb); | ||
}; | ||
/* Internal helper methods */ | ||
FTP.prototype.send = function(cmd, params, cb) { | ||
if (!this._socket || !this._socket.writable) | ||
return false; | ||
if (cmd) { | ||
cmd = (''+cmd).toUpperCase(); | ||
if (typeof params === 'function') { | ||
cb = params; | ||
params = undefined; | ||
} | ||
if (!params) | ||
this._queue.push([cmd, cb]); | ||
else | ||
this._queue.push([cmd, params, cb]); | ||
} | ||
if (this._queue.length) { | ||
var fullcmd = this._queue[0][0] | ||
+ (this._queue[0].length === 3 ? ' ' + this._queue[0][1] : ''); | ||
this.debug&&this.debug('> ' + fullcmd); | ||
this._socket.write(fullcmd + '\r\n'); | ||
} | ||
// Private/Internal methods | ||
FTP.prototype._parseFeat = function(text) { | ||
var lines = text.split(reEOL); | ||
lines.shift(); // initial response line | ||
lines.pop(); // final response line | ||
return true; | ||
for (var i = 0, len = lines.length; i < len; ++i) | ||
lines[i] = lines[i].trim(); | ||
// just store the raw lines for now | ||
this._feat = lines; | ||
}; | ||
FTP.prototype._pasvGetLines = function(emitter, type, cb) { | ||
var self = this; | ||
return this.send('PASV', function(e, stream) { | ||
if (e) | ||
return cb(e); | ||
var curData = '', lines; | ||
stream.setEncoding('binary'); | ||
stream.on('data', function(data) { | ||
curData += data; | ||
if (/\r\n|\n/.test(curData)) { | ||
if (curData[curData.length-1] === '\n') { | ||
lines = curData.split(/\r\n|\n/); | ||
curData = ''; | ||
} else { | ||
var pos = curData.lastIndexOf('\r\n'); | ||
if (pos === -1) | ||
pos = curData.lastIndexOf('\n'); | ||
lines = curData.substring(0, pos).split(/\r\n|\n/); | ||
curData = curData.substring(pos+1); | ||
FTP.prototype._pasv = function(cb) { | ||
var self = this, first = true, ip, port; | ||
this._send('PASV', function reentry(err, text) { | ||
if (err) | ||
return cb(err); | ||
self._curReq = undefined; | ||
if (first) { | ||
var m = rePASV.exec(text); | ||
if (!m) | ||
return cb(new Error('Unable to parse PASV server response')); | ||
ip = m[1]; | ||
ip += '.'; | ||
ip += m[2]; | ||
ip += '.'; | ||
ip += m[3]; | ||
ip += '.'; | ||
ip += m[4]; | ||
port = (parseInt(m[5], 10) * 256) + parseInt(m[6], 10); | ||
first = false; | ||
} | ||
self._pasvConnect(ip, port, function(err, sock) { | ||
if (err) { | ||
// try the IP of the main connection if the server was somehow | ||
// misconfigured and gave for example a LAN IP instead of WAN IP over | ||
// the Internet | ||
if (ip !== self._socket.remoteAddress) { | ||
ip = self._socket.remoteAddress; | ||
return reentry(); | ||
} | ||
processDirLines(lines, emitter, type, self.debug); | ||
// automatically abort PASV mode | ||
self._send('ABOR', function() { | ||
cb(err); | ||
self._send(); | ||
}, true); | ||
return; | ||
} | ||
cb(undefined, sock); | ||
self._send(); | ||
}); | ||
stream.on('end', function() { | ||
emitter.emit('end'); | ||
}); | ||
stream.on('error', function(e) { | ||
emitter.emit('error', e); | ||
}); | ||
cb(); | ||
}); | ||
}; | ||
FTP.prototype._pasvConnect = function() { | ||
if (!this._pasvPort) | ||
return false; | ||
FTP.prototype._pasvConnect = function(ip, port, cb) { | ||
var self = this, | ||
socket = new Socket(), | ||
sockerr, | ||
timedOut = false, | ||
timer = setTimeout(function() { | ||
timedOut = true; | ||
socket.destroy(); | ||
cb(new Error('Timed out while making data connection')); | ||
}, this.options.pasvTimeout); | ||
var self = this; | ||
socket.setTimeout(0); | ||
this.debug&&this.debug('(PASV) About to attempt data connection to: ' | ||
+ this._pasvIP + ':' + this._pasvPort); | ||
socket.once('connect', function() { | ||
clearTimeout(timer); | ||
self._pasvSocket = socket; | ||
cb(undefined, socket); | ||
}); | ||
socket.once('error', function(err) { | ||
sockerr = err; | ||
}); | ||
socket.once('end', function() { | ||
clearTimeout(timer); | ||
}); | ||
socket.once('close', function(had_err) { | ||
clearTimeout(timer); | ||
if (!self._pasvSocket && !timedOut) { | ||
var errmsg = 'Unable to make data connection'; | ||
if (sockerr) { | ||
errmsg += ': ' + sockerr; | ||
sockerr = undefined; | ||
} | ||
cb(new Error(errmsg)); | ||
} | ||
self._pasvSocket = undefined; | ||
}); | ||
var s = this._dataSock = new net.Socket(); | ||
s.on('connect', function() { | ||
clearTimeout(s._pasvTimeout); | ||
self.debug&&self.debug('(PASV) Data connection successful'); | ||
self._callCb(s); | ||
}); | ||
s.on('end', function() { | ||
self.debug&&self.debug('(PASV) Data connection closed'); | ||
}); | ||
s.on('close', function(had_err) { | ||
clearTimeout(self._pasvTimeout); | ||
self._pasvPort = self._pasvIP = undefined; | ||
self._dataSock = undefined; | ||
}); | ||
s.on('error', function(err) { | ||
self.debug&&self.debug('(PASV) Error: ' + err); | ||
self._callCb(err); | ||
}); | ||
s._pasvTimeout = setTimeout(function() { | ||
var r = self.send('ABOR', function(e) { | ||
s.destroy(); | ||
if (e) | ||
return self._callCb(e); | ||
self._callCb(new Error('(PASV) Data connection timed out while connecting')); | ||
}); | ||
if (!r) | ||
self._callCb(new Error('Connection severed')); | ||
}, this.options.connTimeout); | ||
socket.connect(port, ip); | ||
}; | ||
s.connect(this._pasvPort, this._pasvIP); | ||
FTP.prototype._store = function(cmd, input, cb) { | ||
var isBuffer = Buffer.isBuffer(input); | ||
return true; | ||
}; | ||
if (!isBuffer) | ||
input.pause(); | ||
FTP.prototype._callCb = function(result) { | ||
if (!this._queue.length) | ||
return; | ||
var self = this; | ||
this._pasv(function(err, sock) { | ||
if (err) | ||
return cb(err); | ||
var req = this._queue.shift(), cb = (req.length === 3 ? req[2] : req[1]); | ||
if (!cb) | ||
return; | ||
if (self._queue[0] && self._queue[0].cmd === 'ABOR') | ||
return cb(); | ||
if (result instanceof Error) | ||
process.nextTick(function() { cb(result); }); | ||
else if (typeof result !== 'undefined') | ||
process.nextTick(function() { cb(undefined, result); }); | ||
else | ||
process.nextTick(cb); | ||
}; | ||
var sockerr; | ||
sock.once('error', function(err) { | ||
sockerr = err; | ||
}); | ||
// this callback will be executed multiple times, the first is when server | ||
// replies with 150, then a final reply after the data connection closes | ||
// to indicate whether the transfer was actually a success or not | ||
self._send(cmd, function(err, text, code) { | ||
if (sockerr || err) | ||
return cb(sockerr || err); | ||
/******************************************************************************/ | ||
/***************************** Utility functions ******************************/ | ||
/******************************************************************************/ | ||
function processDirLines(lines, emitter, type, debug) { | ||
for (var i=0,result,len=lines.length; i<len; ++i) { | ||
if (lines[i].length) { | ||
debug&&debug('(PASV) Got ' + type + ' line: ' + lines[i]); | ||
if (type === 'LIST') | ||
result = parseList(lines[i]); | ||
else if (type === 'MLSD') | ||
result = parseMList(lines[i]); | ||
emitter.emit((typeof result === 'string' ? 'raw' : 'entry'), result); | ||
} | ||
} | ||
} | ||
if (code === 150) { | ||
if (isBuffer) | ||
sock.end(input); | ||
else { | ||
input.pipe(sock); | ||
input.resume(); | ||
} | ||
} else | ||
cb(); | ||
}); | ||
}); | ||
}; | ||
function parseResponses(lines) { | ||
var resps = [], | ||
multiline = ''; | ||
for (var i=0,match,len=lines.length; i<len; ++i) { | ||
if (match = lines[i].match(/^(\d{3})(?:$|(\s|\-)(.*))/)) { | ||
if (match[2] === '-') { | ||
if (match[3]) | ||
multiline += match[3] + '\n'; | ||
continue; | ||
} else | ||
match[3] = (match[3] ? multiline + match[3] : multiline); | ||
if (match[3].length) | ||
resps.push([parseInt(match[1], 10), match[3]]); | ||
else | ||
resps.push([parseInt(match[1], 10)]); | ||
multiline = ''; | ||
} else | ||
multiline += lines[i] + '\n'; | ||
FTP.prototype._send = function(cmd, cb, promote) { | ||
if (cmd !== undefined) { | ||
if (promote) | ||
this._queue.unshift({ cmd: cmd, cb: cb }); | ||
else | ||
this._queue.push({ cmd: cmd, cb: cb }); | ||
} | ||
return resps; | ||
} | ||
if (!this._curReq && this._queue.length) { | ||
this._curReq = this._queue.shift(); | ||
if (this._curReq.cmd === 'ABOR' && this._pasvSocket) | ||
this._pasvSocket.aborting = true; | ||
this._socket.write(this._curReq.cmd); | ||
this._socket.write(bytesCRLF); | ||
} | ||
}; | ||
function parseMList(line) { | ||
var ret, result = line.trim().split(reKV); | ||
if (result && result.length > 0) { | ||
ret = {}; | ||
if (result.length === 1) | ||
ret.name = result[0].trim(); | ||
else { | ||
var i = 1; | ||
for (var k,v,len=result.length; i<len; i+=3) { | ||
k = result[i]; | ||
v = result[i+1]; | ||
ret[k] = v; | ||
} | ||
ret.name = result[result.length-1].trim(); | ||
} | ||
} else | ||
ret = line; | ||
return ret; | ||
} | ||
FTP.prototype._reset = function() { | ||
if (this._socket && this._socket.writable) | ||
this._socket.end(); | ||
if (this._socket && this._socket.connTimer) | ||
clearTimeout(this._socket.connTimer); | ||
if (this._socket && this._socket.keepalive) | ||
clearInterval(this._socket.keepalive); | ||
this._socket = undefined; | ||
this._pasvSock = undefined; | ||
this._feat = undefined; | ||
this._curReq = undefined; | ||
this._queue = []; | ||
this._buffer = ''; | ||
this.options.host = this.options.port = this.options.user | ||
= this.options.password = this.options.secure | ||
= this.options.connTimeout = this.options.pasvTimeout | ||
= this.options.keepalive = this._debug = undefined; | ||
this.connected = false; | ||
}; | ||
function parseList(line) { | ||
// Utility functions | ||
function parseListEntry(line) { | ||
var ret, | ||
info, | ||
thisYear = (new Date()).getFullYear(), | ||
month, | ||
@@ -668,2 +648,4 @@ day, | ||
type: ret.type, | ||
name: undefined, | ||
target: undefined, | ||
rights: { | ||
@@ -676,3 +658,3 @@ user: ret.permission.substring(0, 3).replace('-', ''), | ||
group: ret.group, | ||
size: ret.size, | ||
size: parseInt(ret.size, 10), | ||
date: undefined | ||
@@ -683,3 +665,3 @@ }; | ||
day = parseInt(ret.date1, 10); | ||
year = thisYear; | ||
year = (new Date()).getFullYear(); | ||
hour = parseInt(ret.hour, 10); | ||
@@ -695,3 +677,4 @@ mins = parseInt(ret.minute, 10); | ||
mins = '0' + mins; | ||
info.date = new Date(year + '-' + month + '-' + day + 'T' + hour + ':' + mins); | ||
info.date = new Date(year + '-' + month + '-' + day | ||
+ 'T' + hour + ':' + mins); | ||
} else if (ret.month2 !== undefined) { | ||
@@ -718,3 +701,3 @@ month = parseInt(MONTHS[ret.month2.toLowerCase()], 10); | ||
type: (ret.isdir ? 'd' : '-'), | ||
size: (ret.isdir ? '0' : ret.size), | ||
size: (ret.isdir ? 0 : parseInt(ret.size, 10)), | ||
date: undefined, | ||
@@ -742,3 +725,4 @@ }; | ||
info.date = new Date(year + '-' + month + '-' + day + 'T' + hour + ':' + mins); | ||
info.date = new Date(year + '-' + month + '-' + day | ||
+ 'T' + hour + ':' + mins); | ||
ret = info; | ||
@@ -752,74 +736,6 @@ } else | ||
function makeError(code, text) { | ||
var err = new Error('Server Error: ' + code + (text ? ' ' + text : '')); | ||
function makeError(msg, code) { | ||
var err = new Error(msg); | ||
err.code = code; | ||
err.text = text; | ||
return err; | ||
} | ||
function getGroup(code) { | ||
return parseInt(code / 10, 10) % 10; | ||
} | ||
/** | ||
* Adopted from jquery's extend method. Under the terms of MIT License. | ||
* | ||
* http://code.jquery.com/jquery-1.4.2.js | ||
* | ||
* Modified by Brian White to use Array.isArray instead of the custom isArray method | ||
*/ | ||
function extend() { | ||
// copy reference to target object | ||
var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; | ||
// Handle a deep copy situation | ||
if (typeof target === "boolean") { | ||
deep = target; | ||
target = arguments[1] || {}; | ||
// skip the boolean and the target | ||
i = 2; | ||
} | ||
// Handle case when target is a string or something (possible in deep copy) | ||
if (typeof target !== "object" && typeof target !== 'function') | ||
target = {}; | ||
var isPlainObject = function(obj) { | ||
// Must be an Object. | ||
// Because of IE, we also have to check the presence of the constructor property. | ||
// Make sure that DOM nodes and window objects don't pass through, as well | ||
if (!obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval) | ||
return false; | ||
var has_own_constructor = hasOwnProperty.call(obj, "constructor"); | ||
var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf"); | ||
// Not own constructor property must be Object | ||
if (obj.constructor && !has_own_constructor && !has_is_property_of_method) | ||
return false; | ||
// Own properties are enumerated firstly, so to speed up, | ||
// if last one is own, then all properties are own. | ||
var last_key, key; | ||
for (key in obj) | ||
last_key = key; | ||
return typeof last_key === "undefined" || hasOwnProperty.call(obj, last_key); | ||
}; | ||
for (; i < length; i++) { | ||
// Only deal with non-null/undefined values | ||
if ((options = arguments[i]) !== null) { | ||
// Extend the base object | ||
for (name in options) { | ||
src = target[name]; | ||
copy = options[name]; | ||
// Prevent never-ending loop | ||
if (target === copy) | ||
continue; | ||
// Recurse if we're merging object literal values or arrays | ||
if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) { | ||
var clone = src && (isPlainObject(src) || Array.isArray(src)) ? src : Array.isArray(copy) ? [] : {}; | ||
// Never move original objects, clone them | ||
target[name] = extend(deep, clone, copy); | ||
// Don't bring in undefined values | ||
} else if (typeof copy !== "undefined") | ||
target[name] = copy; | ||
} | ||
} | ||
} | ||
// Return the modified object | ||
return target; | ||
} |
{ "name": "ftp", | ||
"version": "0.1.7", | ||
"version": "0.2.0", | ||
"author": "Brian White <mscdex@mscdex.net>", | ||
"description": "An FTP client module for node.js", | ||
"main": "./ftp", | ||
"engines": { "node" : ">=0.4.0" }, | ||
"engines": { "node" : ">=0.8.0" }, | ||
"keywords": [ "ftp", "client", "transfer" ], | ||
@@ -8,0 +8,0 @@ "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/node-ftp/raw/master/LICENSE" } ], |
197
README.md
@@ -10,13 +10,15 @@ Description | ||
* [node.js](http://nodejs.org/) -- v0.4.0 or newer | ||
* [node.js](http://nodejs.org/) -- v0.8.0 or newer | ||
Install | ||
============ | ||
======= | ||
npm install ftp | ||
npm install ftp | ||
Examples | ||
======== | ||
* Get a pretty-printed directory listing of the current (remote) working directory: | ||
* Get a directory listing of the current (remote) working directory: | ||
@@ -26,33 +28,12 @@ ```javascript | ||
// connect to localhost:21 | ||
var conn = new FTPClient(); | ||
conn.on('connect', function() { | ||
// authenticate as anonymous | ||
conn.auth(function(e) { | ||
if (e) | ||
throw e; | ||
conn.list(function(e, entries) { | ||
if (e) | ||
throw e; | ||
console.log('<start of directory list>'); | ||
for (var i=0,len=entries.length; i<len; ++i) { | ||
if (typeof entries[i] === 'string') | ||
console.log('<raw entry>: ' + entries[i]); | ||
else { | ||
if (entries[i].type === 'l') | ||
entries[i].type = 'LINK'; | ||
else if (entries[i].type === '-') | ||
entries[i].type = 'FILE'; | ||
else if (entries[i].type === 'd') | ||
entries[i].type = 'DIR'; | ||
console.log(' ' + entries[i].type + ' ' + entries[i].size | ||
+ ' ' + entries[i].date + ' ' + entries[i].name); | ||
} | ||
} | ||
console.log('<end of directory list>'); | ||
conn.end(); | ||
}); | ||
var c = new FTPClient(); | ||
c.on('ready', function() { | ||
c.list(function(err, list) { | ||
if (err) throw err; | ||
console.dir(list); | ||
c.end(); | ||
}); | ||
}); | ||
conn.connect(); | ||
// connect to localhost:21 as anonymous | ||
c.connect(); | ||
``` | ||
@@ -63,17 +44,15 @@ | ||
```javascript | ||
// Assume we have the same connection 'conn' from before and are currently | ||
// authenticated ... | ||
var FTPClient = require('ftp'); | ||
var fs = require('fs'); | ||
conn.get('foo.txt', function(e, stream) { | ||
if (e) | ||
throw e; | ||
stream.on('success', function() { | ||
conn.end(); | ||
var c = new FTPClient(); | ||
c.on('ready', function() { | ||
c.get('foo.txt', function(err, stream) { | ||
if (err) throw err; | ||
stream.once('close', function() { c.end(); }); | ||
stream.pipe(fs.createWriteStream('foo.local-copy.txt')); | ||
}); | ||
stream.on('error', function(e) { | ||
console.log('ERROR during get(): ' + e); | ||
conn.end(); | ||
}); | ||
stream.pipe(fs.createWriteStream('localfoo.txt')); | ||
}); | ||
// connect to localhost:21 as anonymous | ||
c.connect(); | ||
``` | ||
@@ -84,10 +63,14 @@ | ||
```javascript | ||
// Assume we have the same connection 'conn' from before and are currently | ||
// authenticated ... | ||
var FTPClient = require('ftp'); | ||
var fs = require('fs'); | ||
conn.put(fs.createReadStream('foo.txt'), 'remotefoo.txt', function(e) { | ||
if (e) | ||
throw e; | ||
conn.end(); | ||
var c = new FTPClient(); | ||
c.on('ready', function() { | ||
c.put(fs.createReadStream('foo.txt'), 'foo.remote-copy.txt', function(err) { | ||
if (err) throw err; | ||
c.end(); | ||
}); | ||
}); | ||
// connect to localhost:21 as anonymous | ||
c.connect(); | ||
``` | ||
@@ -98,95 +81,99 @@ | ||
_Events_ | ||
-------- | ||
Events | ||
------ | ||
* **connect**() - Fires when a connection to the server has been successfully established. | ||
* **ready**() - Emitted when connection and authentication were sucessful. | ||
* **timeout**() - Fires if the connection timed out while attempting to connect to the server. | ||
* **close**(< _boolean_ >hadErr) - Emitted when the connection has fully closed. | ||
* **close**(<_boolean_>hasError) - Fires when the connection is completely closed (similar to net.Socket's close event). The specified boolean indicates whether the connection was terminated due to a transmission error or not. | ||
* **end**() - Emitted when the connection has ended. | ||
* **end**() - Fires when the connection has ended. | ||
* **error**(< _Error_ >err) - Emitted when an error occurs. In case of protocol-level errors, `err` contains a 'code' property that references the related 3-digit FTP response code. | ||
* **error**(<_Error_>err) - Fires when an exception/error occurs (similar to net.Socket's error event). The given Error object represents the error raised. | ||
Methods | ||
------- | ||
_Methods_ | ||
--------- | ||
**\* Note: As with the 'error' event, any error objects passed to callbacks will have a 'code' property for protocol-level errors.** | ||
**\* Note 1: If a particular action results in an FTP-specific error, the error object supplied to the callback or 'error' event will contain 'code' and 'text' properties that contain the relevant FTP response code and the associated error text respectively.** | ||
* **(constructor)**() - Creates and returns a new FTP client instance. | ||
**\* Note 2: Methods that return a boolean success value will immediately return false if the action couldn't be carried out for reasons including: no server connection or the relevant command is not available on that particular server.** | ||
* **connect**(< _object_ >config) - _(void)_ - Connects to an FTP server. Valid config properties: | ||
### Standard | ||
* host - _string_ - The hostname or IP address of the FTP server. **Default:** 'localhost' | ||
These are actions defined by the "original" FTP RFC (959) and are generally supported by all FTP servers. | ||
* port - _integer_ - The port of the FTP server. **Default:** 21 | ||
* **(constructor)**([<_object_>config]) - Creates and returns a new instance of the FTP module using the specified configuration object. Valid properties of the passed in object are: | ||
* <_string_>host - The hostname or IP address of the FTP server. **Default:** "localhost" | ||
* <_integer_>port - The port of the FTP server. **Default:** 21 | ||
* <_integer_>connTimeout - The number of milliseconds to wait for a connection to be established. **Default:** 15000 | ||
* <_function_>debug - Accepts a string and gets called for debug messages **Default:** (no debug output) | ||
* user - _string_ - Username for authentication. **Default:** 'anonymous' | ||
* **connect**(<_integer_>port,][<_string_>host]) - _(void)_ - Attempts to connect to the FTP server. If the port and host are specified here, they override and overwrite those set in the constructor. | ||
* password - _string_ - Password for authentication. **Default:** 'anonymous@' | ||
* connTimeout - _integer_ - How long (in milliseconds) to wait for the main connection to be established. **Default:** 10000 | ||
* pasvTimeout - _integer_ - How long (in milliseconds) to wait for a PASV data connection to be established. **Default:** 10000 | ||
* keepalive - _integer_ - How often (in milliseconds) to send a 'dummy' (NOOP) command to keep the connection alive. **Default:** 10000 | ||
* **end**() - _(void)_ - Closes the connection to the server. | ||
* **auth**([<_string_>username, <_string_>password,] <_function_>callback) - <_boolean_>success - Authenticates with the server (leave out username and password to log in as anonymous). The callback has these parameters: the error (undefined if none). | ||
### Required "standard" commands (RFC 959) | ||
* **list**([<_string_>path,] [<_boolean_>streamList,] <_function_>callback) - <_boolean_>success_ - Retrieves the directory listing of the specified path. path defaults to the current working directory. If streamList is set to true, an EventEmitter will be passed to the callback, otherwise an array of objects (format shown below) and raw strings will be passed in to the callback. The callback has these parameters: the error (undefined if none) and a list source. If streaming the list, the following events are emitted on the list source: | ||
* **list**([< _string_ >path, ]< _function_ >callback) - _(void)_ - Retrieves the directory listing of `path`. `path` defaults to the current working directory. `callback` has 2 parameters: < _Error_ >err, < _array_ >list. `list` is an array of objects with these properties: | ||
* **entry**(<_object_>entryInfo) - Emitted for each file or subdirectory. entryInfo contains the following possible properties: | ||
* <_string_>name - The name of the entry. | ||
* <_string_>type - A single character denoting the entry type: 'd' for directory, '-' for file, or 'l' for symlink (UNIX only). | ||
* <_string_>size - The size of the entry in bytes. | ||
* <_Date_>date - The last modified date of the entry. | ||
* <_object_>rights - **(*NIX only)** - The various permissions for this entry. | ||
* <_string_>user - An empty string or any combination of 'r', 'w', 'x'. | ||
* <_string_>group - An empty string or any combination of 'r', 'w', 'x'. | ||
* <_string_>other - An empty string or any combination of 'r', 'w', 'x'. | ||
* <_string_>owner - **(*NIX only)** - The user name or ID that this entry belongs to. | ||
* <_string_>group - **(*NIX only)** - The group name or ID that this entry belongs to. | ||
* <_string_>target - **(*NIX only)** - For symlink entries, this is the symlink's target. | ||
* name - _string_ - The name of the entry. | ||
* **raw**(<_string_>rawListing) - Emitted when a directory listing couldn't be parsed and provides you with the raw directory listing from the server. | ||
* size - _string_ - The size of the entry in bytes. | ||
* **end**() - Emitted when the server has finished sending the directory listing, which may or may not be due to error. | ||
* date - _Date_ - The last modified date of the entry. | ||
* **success**() - Emitted when the server says it successfully sent the entire directory listing. | ||
* rights - _object_ - The various permissions for this entry **(*NIX only)**. | ||
* **error**(<_Error_>err) - Emitted when an error was encountered while obtaining the directory listing. | ||
* user - _string_ - An empty string or any combination of 'r', 'w', 'x'. | ||
* **pwd**(<_function_>callback) - <_boolean_>success - Retrieves the current working directory. The callback has these parameters: the error (undefined if none) and a string containing the current working directory. | ||
* group - _string_ - An empty string or any combination of 'r', 'w', 'x'. | ||
* **cwd**(<_string_>newPath, <_function_>callback) - <_boolean_>success - Changes the current working directory to newPath. The callback has these parameters: the error (undefined if none). | ||
* other - _string_ - An empty string or any combination of 'r', 'w', 'x'. | ||
* **cdup**(<_function_>callback) - <_boolean_>success - Changes the working directory to the parent of the current directory. The callback has these parameters: the error (undefined if none). | ||
* type - _string_ - A single character denoting the entry type: 'd' for directory, '-' for file (or 'l' for symlink on **\*NIX only**). | ||
* owner - _string_ - The user name or ID that this entry belongs to **(*NIX only)**. | ||
* **get**(<_string_>filename, <_function_>callback) - <_boolean_>success - Retrieves a file from the server. The callback has these parameters: the error (undefined if none) and a ReadableStream. The ReadableStream will emit 'success' if the file was successfully transferred. | ||
* group - _string_ - The group name or ID that this entry belongs to **(*NIX only)**. | ||
* **put**(<_mixed_>input, <_string_>filename, <_function_>callback) - <_boolean_>success - Sends a file to the server. The `input` can be a ReadableStream or a single Buffer. The callback has these parameters: the error (undefined if none). | ||
* target - _string_ - For symlink entries, this is the symlink's target **(*NIX only)**. | ||
* **append**(<_mixed_>input, <_string_>filename, <_function_>callback) - <_boolean_>success - Same as **put**, except if the file already exists, it will be appended to instead of overwritten. | ||
* **get**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves a file, `path`, from the server. `callback` has 2 parameters: < _Error_ >err, < _ReadableStream_ >fileStream. | ||
* **mkdir**(<_string_>dirname, <_function_>callback) - <_boolean_>success - Creates a new directory on the server. The callback has these parameters: the error (undefined if none) and a string containing the path of the newly created directory. | ||
* **put**(< _mixed_ >input, < _string_ >path, < _function_ >callback) - _(void)_ - Sends data to the server to be stored as `path`. `input` can be a ReadableStream or a single Buffer. `callback` has 1 parameter: < _Error_ >err. | ||
* **rmdir**(<_string_>dirname, <_function_>callback) - <_boolean_>success - Removes a directory on the server. The callback has these parameters: the error (undefined if none). | ||
* **append**(< _mixed_ >input, < _string_ >path, < _function_ >callback) - _(void)_ - Same as **put()**, except if `path` already exists, it will be appended to instead of overwritten. | ||
* **delete**(<_string_>entryName, <_function_>callback) - <_boolean_>success - Deletes a file on the server. The callback has these parameters: the error (undefined if none). | ||
* **rename**(< _string_ >oldPath, < _string_ >newPath, < _function_ >callback) - _(void)_ - Renames `oldPath` to `newPath` on the server. `callback` has 1 parameter: < _Error_ >err. | ||
* **rename**(<_string_>oldFilename, <_string_>newFilename, <_function_>callback) - <_boolean_>success - Renames a file on the server. The callback has these parameters: the error (undefined if none). | ||
* **delete**(< _string_ >path, < _function_ >callback) - _(void)_ - Deletes a file, `path`, on the server. `callback` has 1 parameter: < _Error_ >err. | ||
* **system**(<_function_>callback) - <_boolean_>success - Retrieves information about the server's operating system. The callback has these parameters: the error (undefined if none) and a string containing the text returned by the server. | ||
* **cwd**(< _string_ >path, < _function_ >callback) - _(void)_ - Changes the current working directory to `path`. `callback` has 1 parameter: < _Error_ >err. | ||
* **status**(<_function_>callback) - <_boolean_>success - Retrieves human-readable information about the server's status. The callback has these parameters: the error (undefined if none) and a string containing the text returned by the server. | ||
* **abort**(< _function_ >callback) - _(void)_ - Aborts the current data transfer (e.g. from get(), put(), or list()). `callback` has 1 parameter: < _Error_ >err. | ||
* **status**(< _function_ >callback) - _(void)_ - Retrieves human-readable information about the server's status. `callback` has 2 parameters: < _Error_ >err, < _string_ >status. | ||
### Extended | ||
### Optional "standard" commands (RFC 959) | ||
These are actions defined by later RFCs that may not be supported by all FTP servers. | ||
* **mkdir**(< _string_ >path, < _function_ >callback) - _(void)_ - Creates a new directory, `path`, on the server. `callback` has 1 parameter: < _Error_ >err. | ||
* **size**(<_string_>filename, <_function_>callback) - <_boolean_>success - Retrieves the size of the specified file. The callback has these parameters: the error (undefined if none) and a string containing the size of the file in bytes. | ||
* **rmdir**(< _string_ >path, < _function_ >callback) - _(void)_ - Removes a directory, `path`, on the server. `callback` has 1 parameter: < _Error_ >err. | ||
* **lastMod**(<_string_>filename, <_function_>callback) - <_boolean_>success - Retrieves the date and time the specified file was last modified. The callback has these parameters: the error (undefined if none) and a _Date_ instance representing the last modified date. | ||
* **cdup**(< _function_ >callback) - _(void)_ - Changes the working directory to the parent of the current directory. `callback` has 1 parameter: < _Error_ >err. | ||
* **restart**(<_mixed_>byteOffset, <_function_>callback) - <_boolean_>success - Sets the file byte offset for the next file transfer action (get/put/append). byteOffset can be an _integer_ or _string_. The callback has these parameters: the error (undefined if none). | ||
* **pwd**(< _function_ >callback) - _(void)_ - Retrieves the current working directory. `callback` has 2 parameters: < _Error_ >err, < _string_ >cwd. | ||
* **system**(< _function_ >callback) - _(void)_ - Retrieves information about the server's operating system. `callback` has 2 parameters: < _Error_ >err, < _string_ >info. | ||
### Extended commands (RFC 3659) | ||
* **size**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves the size of `path`. `callback` has 2 parameters: < _Error_ >err, < _integer_ >numBytes. | ||
* **lastMod**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves the last modified date and time for `path`. `callback` has 2 parameters: < _Error_ >err, < _Date_ >lastModified. | ||
* **restart**(< _integer_ >byteOffset, < _function_ >callback) - _(void)_ - Sets the file byte offset for the next file transfer action (get/put/append) to `byteOffset`. `callback` has 1 parameter: < _Error_ >err. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
36982
679
175
2