/**
* FTPimp
* @author Nicholas Riley
*/
"use strict";
require('colors');
var net = require('net'),//{{{
fs = require('fs'),
path = require('path'),
/**
* @mixin
* @see {@link https://nodejs.org/api/events.html|Node.js API: EventEmitter}
*/
EventEmitter = require('events').EventEmitter,
dbg,
StatObject,
Queue,
handle,
ftp,
cmd,
/** @constructor */
CMD = require('./lib/command'),
/**
* The main FTP API object
* @constructor
* @mixes EventEmitter
* @param {null|object} config - The ftp connection settings (optional)
* @param {boolean} connect - Whether or not to start the connection automatically; default is true;
* @todo The major functions have been added and this current version
* is more stable and geared for asynchronous NodeJS. The following commands need to be added:
* @todo Add FTP.stou
* @todo Add FTP.rein
* @todo Add FTP.site
* @todo Add FTP.mode
* @todo Add FTP.acct
* @todo Add FTP.appe
* @todo Add FTP.help
* @todo Add ability to opt into an active port connection for data transfers
*
* FTP extends the NodeJS EventEmitter - see
*/
FTP = function (cfg, connect) {
ftp = this;
connect = connect ? true : false;
if (cfg) {
Object.keys(cfg).forEach(function (key) {
ftp.config[key] = cfg[key];
});
if (undefined !== cfg.ascii) {
ftp.ascii = ftp.ascii.concat(cfg.ascii);
}
if (ftp.config.debug) {
debug.enable();
} else {
debug.disable();
}
}
//set new handler
cmd = ftp.cmd = CMD.create(ftp);
ftp.handle = ftp.Handle.create();
if (connect) {
ftp.connect();
}
},
/**
* A debugger for developing and issue tracking
* @namespace
* @memberof FTP
*/
debug = {
/** Disable debugging */
disable: function () {
dbg = debug.disabled;
},
/** Holds the disabled debugger */
disabled: function () {
return undefined;
},
/** Enable debugging */
enable: function () {
dbg = debug.enabled;
},
/** Holds the enabled debugger */
enabled: function () {
console.log.apply(console, arguments);
}
};//}}}
/**
* Initializes the main FTP sequence
* ftp will emit a ready event once
* the server connection has been established
* @param {null|object} config - The ftp connection settings (optional)
* @param {boolean} connect - Whether or not to start the connection automatically; default is true;
* @returns {object} FTP - return new FTP instance object
*/
FTP.create = function (cfg, connect) {
return new FTP(cfg, connect);
};
/**
* The command module
* @type {object}
* @extends module:command
*/
FTP.CMD = CMD;
FTP.prototype = new EventEmitter();
//expose debugger everywhere
FTP.debug = debug;
FTP.prototype.debug = debug;
/** @constructor */
FTP.prototype.Handle = function () {
return undefined;
};
//TODO - document totalPipes && openPipes
FTP.prototype.totalPipes = 0;
FTP.prototype.openPipes = 0;
/**
* Holds the current file transfer type [ascii, binary, ecbdic, local]
* @type {string}
*/
FTP.prototype.currentType = 'ascii';
/**
* List of files to get and put in ASCII
* @type {array}
*/
FTP.prototype.ascii = {
am: 1,
asp: 1,
bat: 1,
c: 1,
cfm: 1,
cgi: 1,
conf: 1,
cpp: 1,
css: 1,
dhtml: 1,
diz: 1,
h: 1,
hpp: 1,
htm: 1,
html: 1,
in: 1,
inc: 1,
java: 1,
js: 1,
jsp: 1,
lua: 1,
m4: 1,
mak: 1,
md5: 1,
nfo: 1,
nsi: 1,
pas: 1,
patch: 1,
php: 1,
phtml: 1,
pl: 1,
po: 1,
py: 1,
qmail: 1,
sh: 1,
shtml: 1,
sql: 1,
svg: 1,
tcl: 1,
tpl: 1,
txt: 1,
vbs: 1,
xhtml: 1,
xml: 1,
xrc: 1
};
/**
* Set once the ftp connection is established
* @type {boolean}
*/
FTP.prototype.isReady = false;
/**
* Refernce to the socket created for data transfers
* @alias FTP#pipe
* @type {object}
*/
FTP.prototype.pipe = null;
/**
* Set by the ftp.abort method to tell the pipe to close any open data connection
* @type {object}
* @alias FTP#pipeAborted
*/
FTP.prototype.pipeAborted = false;
/**
* Set by the ftp.openDataPort method to tell the process that the pipe has been closed
* @type {object}
* @alias FTP#pipeClosed
*/
FTP.prototype.pipeClosed = false;
/**
* Set by the ftp.put method while the pipe is connecting and while connected
* @type {object}
* @alias FTP#pipeActive
*/
FTP.prototype.pipeActive = false;
/**
* Refernce to the socket created for data transfers
* @type {object}
* @alias FTP#socket
*/
FTP.prototype.socket = null;
/**
* The FTP log in information.
* @type {string}
* @alias FTP#config
*/
FTP.prototype.config = {
host: 'localhost',
port: 21,
user: 'root',
pass: '',
debug: false
};
/**
* Current working directory.
* @type {string}
* @alias FTP#cwd
*/
FTP.prototype.cwd = '';
/**
* The user defined directory set by the FTP admin.
* @type {string}
* @alias FTP#baseDir
*/
FTP.prototype.baseDir = '';
/**
* Creates and returns a new FTP connection handler
* @returns {object} The new Handle instance
*/
FTP.prototype.Handle.create = function () {
return new FTP.prototype.Handle();
};
handle = FTP.prototype.Handle.prototype;
/**
* Ran at beginning to start a connection, can be overriden
* @example
* //Overriding the ftpimp.init instance method
* var FTP = require('ftpimp'),
* //get connection settings
* config = {
* host: 'localhost',
* port: 21,
* user: 'root',
* pass: ''
* },
* ftp,
* //override init
* MyFTP = function(){
* this.init();
* };
* //override the prototype
* MyFTP.prototype = FTP.prototype;
* //override the init method
* MyFTP.prototype.init = function () {
* dbg('Initializing!');
* ftp.handle = ftp.Handle.create();
* ftp.connect();
* };
* //start new MyFTP instance
* ftp = new MyFTP(config);
*/
FTP.prototype.init = function () {//{{{
//create a new socket and login
ftp.connect();
};//}}}
var ExeQueue = function (command, callback, runLevel, holdQueue) {
var that = this,
n,
method = command.split(' ', 1)[0],
bind = function (name) {
that[name.slice(1)] = function () {
if (name === '_responseHandler') {
}
dbg('calling : ' + name + ' > ' + command, arguments);
that[name].apply(that, arguments);
};
};
that.command = command;
that.method = method;
that.pipeData = [];
that.pipeDataSize = 0;
that.holdQueue = holdQueue;
that.callback = callback;
that.runLevel = runLevel;
that.ended = false;
that.ping = null;
handle.data.waiting = true;
for (n in ExeQueue.prototype) {
if (ExeQueue.prototype.hasOwnProperty(n) && n.charAt(0) === '_' && ExeQueue.prototype.hasOwnProperty(n)) {
//remove underscore and provide hook
bind(n);
}
}
ftp.once('response', that.responseHandler);
that.started = Date.now();
ftp.socket.write(command + '\r\n', function () {
dbg(('Run> command sent: ' + command).yellow);
});
};
ExeQueue.create = function (command, callback, runLevel, holdQueue) {
return new ExeQueue(command, callback, runLevel, holdQueue);
};
//end the queue
ExeQueue.prototype._end = function () {
var that = this;
that.checkProc();
};
//end the queue
ExeQueue.prototype._endStopwatch = function () {
var that = this;
that.ended = Date.now();
that.ping = that.ended - that.started;
};
ExeQueue.prototype.queueHolding = false;
ExeQueue.prototype._checkProc = function () {
var that = this;
dbg('check process for end: ', that);
if (that.holdQueue) {
dbg(('ExeQueue> Ending process, holding queue: ' + that.command).yellow);
} else {
dbg(('ExeQueue> Ending process: ' + that.command).yellow);
ftp.emit('endproc');
}
};
ExeQueue.prototype._closeTransfer = function () {
dbg('ExeQueue> closing transfer and ending Proc'.magenta);
var exeQueue = this;
exeQueue.closePipe();
//exeQueue.checkProc();
};
ExeQueue.prototype._closePipe = function () {
dbg('ExeQueue> closing transfer pipe'.magenta);
let exeQueue = this;
let data = exeQueue.pipeData;
let bufferSize = exeQueue.pipeDataSize;
try {
ftp.pipe.removeListener('data', exeQueue.receiveData);
ftp.pipe.removeListener('end', exeQueue.closePipe);
//check for buffers
if (data.length && Array.isArray(data)) {
data = Buffer.concat(data, bufferSize);
}
} catch (dataNotBoundError) {
dbg('data not bound: ', dataNotBoundError);
}
dbg('ExeQueue> total size(' + (data ? data.length : 0) + ')');
exeQueue.callback(null, data);
exeQueue.checkProc();
};
ExeQueue.prototype._responseHandler = function (code, data) {
dbg(('Response handler: ' + code).cyan, data);
var exeQueue = this;
exeQueue.endStopwatch();
//dbg('pipe is ' + (ftp.pipeClosed ? 'closed' : 'open'));
if (code >= 500 && code < 600) {
dbg('handling error response code...');
dbg(exeQueue);
//if we have an open pipe, wait for it to end
//if (ftp.pipeClosed) {
//end immediately
try {
dbg('killing pipe');
ftp.pipe.removeListener('data', exeQueue.receiveData);
ftp.pipe.removeListener('end', exeQueue.closePipe);
ftp.pipe.destroy();
dbg('---pipe down---'.red);
} catch (dataNotBoundError) {
dbg('data not bound: ', dataNotBoundError);
}
exeQueue.callback(new Error(data), null);
exeQueue.checkProc();
} else if (code === 150 || code === 125) {
if (exeQueue.method === 'STOR') {
ftp.once('dataTransferComplete', exeQueue.closeTransfer);
} else {
dbg('listening for pipe data'.red);
if (ftp.pipeClosed) {
dbg('pipe already closed'.yellow);
ftp.pipeClosed = false;
exeQueue.closePipe();
return;
}
ftp.pipe.on('end', exeQueue.closePipe);
ftp.pipe.on('data', exeQueue.receiveData);
}
} else {
exeQueue.callback(null, data);
if (code !== 227) {
exeQueue.checkProc();
}
}
};
ExeQueue.prototype._receiveData = function (data) {
let c = this;
c.pipeDataSize += data.length;
c.pipeData.push(data);
};
/**
* Run a raw ftp command and issue callback on success/error.
* Same as {@link FTP#run} except this command will be
* will be prioritized to be the next to run in the queue.
* - calls made with this provide a sequential queue
*
* @param {string} command - The command that will be issued ie: <b>"CWD foo"</b>
* @param {function} callback - The callback function to be issued on success/error
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.runNext = function (command, callback, holdQueue) {
ftp.run(command, callback, Queue.RunNext, holdQueue);
};
/**
* Run a raw ftp command and issue callback on success/error.
* Same as {@link FTP#run} except this command will be ran immediately (in parallel)
* and will overrun any current queue action in place.
*
* @param {string} command - The command that will be issued ie: <b>"CWD foo"</b>
* @param {function} callback - The callback function to be issued on success/error
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.runNow = function (command, callback, holdQueue) {
ftp.run(command, callback, Queue.RunNow, holdQueue);
};
/**
* Run a raw ftp command and issue callback on success/error.
* <br>
* Functions created with this provide a sequential queue
* that is asynchronous, so items will be processed
* in the order they are received, but this will happen
* immediately. Meaning, if you make a dozen sequential calls
* of <b>"ftp.run('MDTM', callback);"</b> they will all be read immediately,
* queued in order, and then processed one after the other. Unless
* you set the optional parameter <b>runLevel</b> to <b>true</b>
*
* @param {string} command - The command that will be issued ie: <b>"CWD foo"</b>
* @param {function} callback - The callback function to be issued on success/error
* @param {number} [runLevel=0] - TL;DR see {@link Queue.RunLevels}
* FTP#run will invoke a queueing process, callbacks
* will be stacked to maintain synchronicity. How they stack will depend on the value
* you set for the runLevel
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.run = function (command, callback, runLevel, holdQueue) {//{{{
runLevel = runLevel === undefined ? false : runLevel;
holdQueue = holdQueue === undefined ? false : holdQueue;
var callbackConstruct = function () {
dbg('Run> running callbackConstruct'.yellow + ' ' + command);
//if (command === 'QUIT') {...}
dbg(command, runLevel, holdQueue);
ExeQueue.create(command, callback, runLevel, holdQueue);
};
if (undefined === command) { //TODO || cmd.allowed.indexOf(command.toLowerCase) {
throw new Error('ftp.run > parameter 1 expected command{string}');
} else if (undefined === callback || typeof callback !== 'function') {
throw new Error('ftp.run > parameter 2 expected a callback function');
}
dbg('ftp.Run: ' + [, holdQueue, command].join(' ').cyan);
ftp.queue.register(callbackConstruct, runLevel);
};//}}}
/**
* Establishes a queue to provide synchronicity to ftp
* processes that would otherwise fail from concurrency.
* This function is done automatically when using
* the {@link FTP#run} method to queue commands.
* @fires FTP#queueEmpty
* @member {object} FTP#queue
* @property {array} queue._queue - Stores registered procedures
* and holds them until called by the queue.run method
* @property {boolean} queue.processing - Returns true if there
* are items running in the queue
* @property {function} queue.register - Registers a new callback
* function to be triggered after the queued command completes
* @property {function} queue.run - If there is something in the
* queue and queue.processing is false, than the first item
* stored in queue._queue will be removed from the queue
* and processed.
*/
FTP.prototype.queue = {//{{{
_queue: [],
processing: false,
reset: function () {
//...resets the queue
ftp.queue._queue = [];
},
register: function (callback, runLevel) {
dbg('Queue> Registering callback...');
dbg(('Queue> processing: ' + ftp.queue.processing + '; size: ' + ftp.queue._queue.length).cyan);
runLevel = runLevel === undefined ? false : runLevel;
if (runLevel) {
//run next
if (runLevel === Queue.RunNext) {
ftp.queue._queue.unshift(callback);
} else {
//run now
callback();
return;
}
} else {
ftp.queue._queue.push(callback);
}
if (!ftp.queue.processing) {
ftp.queue.run();
//ftp.emit('endproc');
}
},
run: function () {
dbg('Queue> Loading queue'.yellow);
if (ftp.queue._queue.length > 0) {
ftp.queue.processing = true;
dbg('Queue> Loaded...running');
ftp.queue.currentProc = ftp.queue._queue.shift();
if (!ftp.error) {
ftp.queue.currentProc.call(ftp.queue.currentProc);
}
} else {
/**
* Fired when the primary queue is empty
* @event FTP#queueEmpty
*/
ftp.emit('queueEmpty');
ftp.queue.processing = false;
dbg('--queue empty--'.yellow);
}
}
};
FTP.prototype.on('endproc', function () {
dbg('Event> endproc'.magenta);
});
/** @todo - this needs to be defined */
FTP.prototype.on('endproc', FTP.prototype.queue.run);//}}}
/**
* Provides a factory to create a simple queue procedure. Look
* at the example below to see how we override the callback
* function to perform additional actions before exiting
* the queue and loading the next one.<br>
* Functions created with this provide a synchronized queue
* that is asynchronous in itself, so items will be processed
* in the order they are received, but this will happen
* immediately. Meaning, if you make a dozen sequential calls
* to {@link FTP#filemtime} they will all be read immediately,
* queued in order, and then processed one after the other.
* @constructor
* @memberof FTP
* @see {@link Queue}
* @param {string} command - The command that will be issued ie: <b>"CWD foo"</b>
* @returns {function} queueManager - The simple queue manager
* @TODO - documentation needs to be updated rewrite
*/
var Queue = function (command) {//{{{
var queue = this;
queue.command = command;
var builder = queue.builder();
builder.raw = command;
return builder;
};//}}}
/**
* The queue manager returned when creating a new {@link Queue} object
* @memberof Queue
* @inner
* @param {string} filepath - The location of the remote file to process the set command.
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. Careful, concurrent connections
* will likely end in a socket error. This is meant for fine grained control over certain
* scenarios wherein the process is part of a running queue and you need to perform an ftp
* action prior to the {@link FTP#endproc} event firing and execing the next queue.
*/
Queue.prototype.builder = function () {
var queue = this,
command = queue.command;
return function (filepath, callback, runLevel, holdQueue) {
var hook = (undefined === queue[command + 'Hook']) ? null : queue[command + 'Hook'],
portHandler = function () {
dbg('Queue.builder: checking hook -> ' + typeof hook);
//hook data into custom instance function
ftp.runNow(command + ' ' + filepath, function (err, data) {
if (typeof hook === 'function') {
data = hook(data);
}
callback(err, data);
if (!holdQueue) {
ftp.emit('endproc');
}
}, true);
};
dbg(['Queue.builder::', command, filepath, '> setting '].join(' ').cyan);
dbg(runLevel, holdQueue);
//TODO add list of commands that don't need to change type, or should be a certain type
//ie ls:LIST
ftp.setType(filepath, function () {
dbg('type set');
ftp.openDataPort(portHandler, Queue.RunNow, true);
}, runLevel, true);
};
};
/**
* Static Queue value passed in the runLevel param of methods to control the priority of those methods.
* <br>
* - default RunLevel {@link FTP.Queue.RunLast}<br>
* - Most methods use the {@link FTP#run} call.<br>
* - Every {@link FTP#run} call issued is stacked in a series queue by default. To change to a waterfall
* or run in parallel. <br><br>
*
* <i>RunLevels are a design of FTPimp to control process flow only.</i><br>
* <strong>When implementing parallel actions, parallel calls should only be issued inside the callback
* of a parent waterfall or series queue. Otherwise, the FTP service itself likely will break
* from the concurrent connection attempts.</strong>
* @class
* @readonly
* @enum {number}
* @see {@link FTP#mkdir}, {@link FTP#rmdir}, {@link FTP#put} for examples
* @see {@link FTP#run} for series, {@link FTP#runNext} for waterfall, {@link FTP#runNow} for parallel
* @example
* //series
* ftp.ping(function () { //runs first
* ftp.ping(function () { //runs third
* });
* });
* ftp.ping(function () { //runs second
* });
*
* //waterfall
* var runNext = FTP.Queue.RunNext;
* ftp.ping(function () { //runs first
* //add runNow to the call
* ftp.ping(function () { //runs second
* }, runNow);
* });
* ftp.ping(function () { //runs third
* });
*
* //parallel
* var runNow = FTP.Queue.RunNow;
* ftp.put('foo', function () { //runs first
* });
* ftp.put('foo', function () { //runs second
* }, runNow);
*/
Queue.RunLevels = {
/** {@link FTP.Queue.RunLast} will push the command to the end of the queue; */
last: 0,
/** {@link FTP.Queue.RunNow} will run the command immediately; will overrun a current processing queue */
now: 1,
/** {@link FTP.Queue.RunNext} will run after current queue completes */
next: 2
};
/**
* @readonly
* @property {number} RunNext - value needed for runLevel parameter to run commands immediately after the current queue;
* @see {@link FTP.Queue.RunLevels.next}
* @see {@link FTP#run}
*/
Queue.RunNext = Queue.RunLevels.next;
/**
* @readonly
* @property {number} RunNow - value needed for runLevel parameter to run commands immediately, overrunning any current queue process;
* @see {@link FTP.Queue.RunLevels.now}
* @see {@link FTP#run}
*/
Queue.RunNow = Queue.RunLevels.now;
/**
* @readonly
* @property {number} RunLast - value needed for runLevel parameter, will add command to the end of the queue; default Queue.RunLevel;
* @see {@link FTP.Queue.RunLevels.last}
* @see {@link FTP#run}
*/
Queue.RunLast = Queue.RunLevels.last;
/**
* Create a new {@link Queue} instance for the command type.
* @param {string} command - The command that will be issued, no parameters, ie: <b>"CWD"</b>
*/
Queue.create = function (command) {//{{{
return new Queue(command);
};//}}}
/**
* Register a data hook function to intercept received data
* on the command (parameter 1)
* @param {string} command - The command that will be issued, no parameters, ie: <b>"CWD"</b>
* @param {function} callback - The callback function to be issued.
*/
Queue.registerHook = function (command, callback) {//{{{
if (undefined !== Queue.prototype[command + 'Hook']) {
throw new Error('Handle.Queue already has hook registered: ' + command + 'Hook');
}
Queue.prototype[command + 'Hook'] = callback;
};//}}}
/**
* Called once the socket has established
* a connection to the host
*/
let failedAttempts = [];
const failedTimeThreshold = 10 * 1000;
const maxFailedAttempts = 3;
handle.connected = function () {//{{{
dbg('socket connected!');
if (!ftp.socket.remoteAddress) {
let now = Date.now();
failedAttempts = failedAttempts.filter((time) => {
return time > (now - failedTimeThreshold);
});
if (failedAttempts.length > maxFailedAttempts) {
throw new Error('Max failed attempts reached trying to reconnect to FTP server');
}
failedAttempts.push(now);
setTimeout(ftp.connect, 1000);
return;
}
process.once('exit', ftp.exit);
process.once('SIGINT', ftp.exit);
ftp.config.pasvAddress = ftp.socket.remoteAddress.split('.').join(',');
ftp.socket.on('data', ftp.handle.data);
//process.once('uncaughtException', handle.uncaughtException);
};//}}}
/**
* Called anytime an uncaughtException error is thrown
*/
handle.uncaughtException = function (err) {//{{{
dbg(('!' + err.toString()).red);
ftp.exit();
};//}}}
/**
* Simple way to parse incoming data, and determine
* if we should run any commands from it. Commands
* are found in the lib/command.js file
*/
handle.data = function (data) {//{{{
dbg('....................');
var strData = data.toString().trim(),
strParts,
commandCodes = [],
commandData = {},
cmdName,
code,
i,
end = function () {
dbg('handle.data.waiting: ' + handle.data.waiting, code);
if (handle.data.waiting) {
dbg('handle.data.waiting:: ' + code + ' ' + strData);
if (!handle.data.start) {
handle.data.waiting = false;
/*if (code === 150) {
dbg('holding for data transfer'.yellow);
} else {*/
ftp.emit('response', code, strData);
//}
} else {
handle.data.waiting = true;
handle.data.start = false;
}
} else if (code === 553) {
}
},
run = function () {
if (undefined !== cmd.keys[code]) {
if (code === 227) {
handle.data.waiting = true;
ftp.once('commandComplete', end);
}
cmdName = cmd.keys[code];
dbg('>executing command: ' + cmdName);
cmd[cmdName](strData);
//only open once per ftp instance
}
//we will handle data transfer codes with the openDataPort
if (code !== 227 && code !== 226) {
end();
}
};
dbg(('\n>>>\n' + strData + '\n>>>\n').grey);
strData = strData.split(/[\r|\n]/).filter(Boolean);
strParts = strData.length;
for (i = 0; i < strParts; i++) {
code = strData[i].substr(0, 3);
//make sure its a number and not yet stored
if (code.search(/^[0-9]{3}/) > -1) {
if (commandCodes.indexOf(code) < 0) {
commandCodes.push(code);
commandData[code] = '';
}
commandData[code] += strData[i].substr(3);
}
}
dbg(commandCodes.join(', ').grey);
for (i = 0; i < commandCodes.length; i++) {
code = Number(commandCodes[i]);
strData = commandData[code].trim();
dbg('------------------');
dbg('CODE -> ' + code);
dbg('DATA -> ' + strData);
dbg('------------------');
run();
}
};//}}}
/**
* Waiting for response from server
* @property FTP#Handle#data.waiting
*/
handle.data.waiting = true;
handle.data.start = true;
/**
* Logout from the ftp server
* @param {number} sig - the signal code, if not 0, then socket will
* be destroyed to force closing
*/
FTP.prototype.exit = function (sig) {//{{{
if (undefined !== sig && sig === 0) {
ftp.socket.end();
} else {
//ftp.pipe.close();
ftp.socket.destroy();
if (ftp.pipeActive) {
ftp.pipeAborted = false;
ftp.pipeActive = false;
}
}
};//}}}
/**
* Creates a new socket connection for sending commands
* to the ftp server and runs an optional callback when logged in
* @param {function} callback - The callback function to be issued. (optional)
*/
FTP.prototype.connect = function (callback) {//{{{
/**
* Holds the connected socket object
* @member FTP#socket
*/
ftp.socket = net.createConnection(ftp.config.port, ftp.config.host);
ftp.socket.on('connect', handle.connected);
if (typeof callback === 'function') {
ftp.once('ready', callback);
}
dbg('connected: ' + ftp.config.host + ':' + ftp.config.port);
ftp.socket.on('close', function () {
dbg('**********socket CLOSED**************');
});
ftp.socket.on('end', function () {
dbg('**********socket END**************');
});
};//}}}
/**
* Opens a new data port to the remote server - pasv connection
* which allows for file transfers
* @param {function} callback - The callback function to be issued
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @TODO Add in useActive parameter to choose how to handle data transfers
*/
FTP.prototype.openDataPort = function (callback, runLevel, holdQueue) {//{{{
holdQueue = !!holdQueue;
dbg('holdQ: ', holdQueue);
var dataHandler = function (err, data) {
if (err) {
dbg('error opening data port with PASV');
dbg(err);
return;
}
dbg('opening data port...'.cyan);
dbg(ftp.socket.remoteAddress);
dbg(ftp.config.pasvPort);
//ftp.on('dataPortReady', callback);
ftp.pipe = net.createConnection(
ftp.config.pasvPort,
ftp.socket.remoteAddress
);
//trigger callback once the server has confirmed the port is open
ftp.pipe.once('connect', function () {
dbg('passive connection established ... running callback'.green);
//dbg(callback.toString());
callback.call(ftp);
});
ftp.pipe.once('end', function () {
dbg('----> pipe end ----');
ftp.pipeClosed = true;
ftp.openPipes -= 1;
ftp.emit('dataPortClosed');
});
/*if (ftp.config.debug) {
ftp.pipe.on('data', function (data) {
dbg(data.toString().green);
});
}*/
/*
ftp.pipe.once('error', function (err) {
dbg(('pipe error: ' + err.errno).red);
dbg(ftp.openPipes);
});*/
};
ftp.pasv(dataHandler, runLevel, holdQueue);
};//}}}
/**
* Asynchronously queues files for transfer, and transfers them in order to the server.
* @function
* @param {string|array} paths - The path to read and send the file,
* if you are sending to the same (relative) location you are reading from then
* you can supply a string as a shortcut. Otherwise, use an array [from, to]
* @param {function} callback - The callback function to be issued once the file
* has been successfully written to the remote
* @TODO - rewrite needed, can be simplified at this point
*/
FTP.prototype.put = (function () {//{{{
var running = false,
//TODO - test this further
runQueue;
runQueue = function (curQueue) {
dbg('FTP::put> running the pipe queue'.green, running);
var callback,
data,
dataTransfer,
checkAborted = function () {
if (ftp.pipeAborted) {
dbg('ftp pipe aborted!---'.yellow);
ftp.pipeActive = false;
ftp.pipeAborted = false;
running = false;
ftp.emit('pipeAborted');
runQueue(true);
return true;
}
return false;
};
if (running) {
throw new Error('Put> already running'.yellow);
}
ftp.pipeActive = running = true;
//if the local path wasnt found
if (!curQueue.path) {
dbg('Put> error');
running = false;
callback = curQueue.callback;
data = curQueue.data;
callback.call(callback, data, null);
/*if(!checkAborted()) {
runQueue(true);
}*/
ftp.emit('endproc');
return;
}
dataTransfer = function (runLevel) {
var callback = curQueue.callback,
remotePath = curQueue.path;
if (checkAborted()) {
dbg('Put was aborted');
return;
}
ftp.runNow('STOR ' + remotePath, function (err, data) {
ftp.pipeActive = running = false;
if (curQueue.err) {
dbg('STOR: error occured');
callback(curQueue.err, null);
} else {
dbg('STOR: file saved ' + remotePath);
callback(null, remotePath);
}
dbg('file put successful', curQueue.data);
if (!curQueue.holdQueue) {
ftp.emit('endproc');
}
}, true);
//write file data to remote data socket
curQueue.stream = fs.createReadStream(curQueue.data);
curQueue.stream.pipe(ftp.pipe);
};
//make sure pipe wasn't aborted
ftp.once('pipeAborted', checkAborted);
ftp.once('transferError', function (err) {
curQueue.err = err;
ftp.removeListener('pipeAborted', checkAborted);
});
ftp.setType(curQueue.path, function () {
ftp.openDataPort(dataTransfer, Queue.RunNow, true);
}, Queue.RunNow, true);
};
return function (paths, callback, runLevel, holdQueue) {
//todo add unique id to string
var isString = typeof paths === 'string',
localPath,
remotePath,
pipeFile,
queue;
if (!isString && !(paths instanceof Array)) {
throw new Error('ftp.put > parameter 1 expected an array or string');
} else if (paths.length === 0) {
throw new Error('ftp.put > parameter 1 expected recvd empty array');
}
if (isString || paths.length === 1) {
localPath = remotePath = isString ? paths : paths[0];
} else {
localPath = paths[0];
remotePath = paths[1];
}
//create an index so we know the order...
//the files may be read at different times
//into the pipeFile callback
dbg('>queueing file for put process: "' + localPath + '" to "' + remotePath + '"');
pipeFile = function (err, stat) {
dbg(('>piping file: ' + localPath).green);
if (!err && stat.isDirectory()) {
err = new Error('Cannot put directory @', localPath);
} else if (err) {
dbg('>file read error', err);
queue = {
callback: callback,
data: err,
path: false,
runLevel: runLevel,
holdQueue: holdQueue
};
} else {
dbg('>queueing file: "' + localPath + '" to "' + remotePath + '"');
queue = {
callback: callback,
data: localPath,
path: remotePath,
runLevel: runLevel,
holdQueue: holdQueue
};
}
runQueue(queue);
};
//enqueue a call; preprend call to the ftp queue of commands
//so we don't break order of operations
ftp.queue.register(function () {
fs.stat(localPath, pipeFile);
}, runLevel);
};
}());//}}}
/**
* Issues a single raw request to the server and
* calls the callback function once data is received
* @param {string} command - The command to send to the FTP server
* @example
* //say hi to the server
* var FTP = require('ftpimp'),
* config = require('./myconfig'),
* ftp = FTP.connect(config);
*
* ftp.on('ready', function () {
* ftp.raw('NOOP', function (data) {
* dbg(data);
* });
* });
* @param {function} callback - The callback function
* to be fired once on a data event
*/
FTP.prototype.raw = function (command, callback) {//{{{
var parser = function (data) {
callback.call(callback, data.toString());
};
ftp.socket.once('data', parser);
ftp.socket.write(command + '\r\n');
};//}}}
/**
* Changes the current directory to the root / restricted directory
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.root = function (callback, runLevel, holdQueue) {//{{{
var dir = ftp.baseDir;
if (dir === '/') {
dir = '';
}
ftp.chdir(ftp.baseDir, callback);
};//}}}
/**
* Runs the FTP command MKD - Make a remote directory.
* Creates a directory and returns the directory name.
* Optionally creates directories recursively.
* @param {string} dirpath - The directory name to be created.
* @param {function} callback - The callback function to be issued.
* @param {boolean} recursive - Recursively create directories. (default: false)
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.mkdir = function (dirpath, callback, recursive, runLevel, holdQueue) {//{{{
//TODO add in error handling for parameters
dbg('building mkdir request for: ' + dirpath);
recursive = undefined === recursive ? false : recursive;
var created = [],
mkdirHandler = function (err, data) {
if (!err) {
data = data.match(/"(.*)"/)[1];
}
if (typeof callback === 'function') {
callback.call(callback, err, data);
}
},
isRoot,
paths,
pathsLength,
cur,
i,
makeNext,
continueMake,
addPaths,
endRecursion;
//hijack mkdirHandler
if (recursive) {
//check if path provided starts with root /
isRoot = (dirpath.charAt(0) === path.sep);
paths = dirpath.split(path.sep).filter(Boolean);
pathsLength = paths.length;
cur = '';
created = [];
i = 0;
makeNext = function () {
var index = i;
i += 1;
cur += paths[index] + path.sep;
dbg('making directory: ' + cur);
if (index === pathsLength - 1) {
dbg('Queue> ending recursion'.red);
//release the holdQueue
ftp.run(FTP.prototype.mkdir.raw + ' ' + (isRoot ? path.sep : '') + cur, endRecursion, index !== 0, holdQueue);
} else {
//runLevel if not first item, first item must be used to start the queue
ftp.run(FTP.prototype.mkdir.raw + ' ' + (isRoot ? path.sep : '') + cur, continueMake, index !== 0, true);
}
};
continueMake = function (err, data) {
addPaths(err, data);
makeNext();
};
addPaths = function (err, data) {
if (!err) {
dbg(('adding path:' + data).blue);
data = data.match(/"(.*)"/)[1];
created.push(data);
}
};
endRecursion = function (err, data) {
dbg('Queue> running endRecursion', err, data);
addPaths(err, data);
if (typeof callback === 'function') {
callback(err, created);
}
};
makeNext();
} else {
ftp.run(FTP.prototype.mkdir.raw + ' ' + dirpath, mkdirHandler, runLevel, holdQueue);
}
};//}}}
FTP.prototype.mkdir.raw = 'MKD';
/**
* Runs the FTP command RMD - Remove a remote directory
* @param {string} dirpath - The location of the directory to be deleted.
* @param {function} callback - The callback function to be issued.
* @param {string} recursive - Recursively delete files and subfolders.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.rmdir = function (dirpath, callback, recursive, runLevel, holdQueue) {//{{{
recursive = !!recursive;
var deleted = [],
pending = {},
depth = [],
currentPending = '',
currentDepthPath = '',
filterPaths = function (statObject) {
return statObject.filename !== '.' && statObject.filename !== '..';
},
checkProc = function () {
if (!holdQueue) {
ftp.emit('endproc');
}
},
removeDir = function () {
dbg('removing directory: '.magenta, currentDepthPath);
ftp.runNow(ftp.rmdir.raw + ' ' + currentDepthPath, function (err, res) {
dbg('Directory deleted: '.green, currentDepthPath);
if (err) {
throw new Error('could not delete dir @' + currentDepthPath);
} else {
//stack deleted and remove from queues
deleted.push(currentDepthPath);
depth.pop();
delete pending[currentPending];
//continue deleting as necessary
if (depth.length > 0) {
//update depth to last item in depth array
currentPending = depth[depth.length - 1];
currentDepthPath = depth.join(path.sep);
unlinkPending();
} else {
callback(err, deleted);
checkProc();
}
}
});
},
bindUnlinkHandler = function (item) {
var cmd = item.isDirectory ? ftp.rmdir.raw : ftp.unlink.raw,
filepath,
handleDeleteResponse = function (err) {
if (err) {
dbg('scanning directory: '.cyan, item.filename);
//restack item
checkDir(err, item.filename);
return;
} else {
dbg('file unlinked: '.yellow, item.filename);
deleted.push(filepath);
//continue deleting as necessary
if (pending[currentPending].length > 0) {
unlinkPending();
} else {
//remove the directory if it is empty
removeDir();
}
}
};
filepath = path.join(currentDepthPath, item.filename);
dbg('rmdir: removing "' + filepath + '"');
ftp.runNow(cmd + ' ' + filepath, handleDeleteResponse, true);
},
unlinkPending = function () {
dbg('unlinking file object');
if (pending[currentPending].length > 0) {
bindUnlinkHandler(pending[currentPending].pop());
} else {
removeDir();
}
},
handleFileList = function (err, data) {
dbg('rmdir: handleFileList...'.magenta, err, data.length - 2);
if (err) {
callback(err, data);
} else {
if (data.length === 0) {
callback(new Error('Directory does not exist'), null);
return checkProc();
}
pending[currentPending] = data.filter(filterPaths);
unlinkPending();
}
},
checkDir = function (err, data) {
dbg('checking dir:'.cyan, err, data);
if (!recursive) {
dbg('ending !recursive'.red);
return callback(err, deleted);
}
if (!err) {
pending -= 1;
dbg('rmdir> directory deleted'.yellow, dirpath);
deleted.push(dirpath);
callback(err, deleted);
checkProc();
} else {
data = data ? data : dirpath;
dbg('need to ls directory', data);
//add file to list of items needed to be removed, increases depth
depth.push(data);
//create new array of pending items for directory
pending[data] = [];
//open the queue immediately after this callback
currentPending = data;
currentDepthPath = depth.join(path.sep);
ftp.ls(currentDepthPath, handleFileList, Queue.RunNow, true);
}
};
ftp.run(ftp.rmdir.raw + ' ' + dirpath, checkDir, runLevel, true);
};//}}}
FTP.prototype.rmdir.raw = 'RMD';
/**
* Runs the FTP command PWD - Print Working Directory
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.getcwd = function (callback, runLevel, holdQueue) {//{{{
ftp.run(FTP.prototype.getcwd.raw, function (err, data) {
if (!err) {
data = data.match(/"(.*?)"/)[1];
ftp.cwd = data;
}
callback.call(callback, err, data);
}, runLevel, holdQueue);
};//}}}
FTP.prototype.getcwd.raw = 'PWD';
/**
* Runs the FTP command CWD - Change Working Directory
* @param {string} dirpath - The directory name to change to.
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.chdir = function (dirname, callback, runLevel, holdQueue) {//{{{
ftp.run(FTP.prototype.chdir.raw + ' ' + dirname, function (err, data) {
if (!err) {
dirname = data.match(/"(.*)"/);
if (null !== dirname) {
ftp.cwd = dirname[1];
} else {
ftp.cwd = data;
}
}
callback.call(callback, err, data);
}, runLevel, holdQueue);
};//}}}
FTP.prototype.chdir.raw = 'CWD';
/**
* Runs the FTP command DELE - Delete remote file
* @param {string} filepath - The location of the file to be deleted.
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.unlink = function (filepath, callback, runLevel, holdQueue) {//{{{
ftp.run(FTP.prototype.unlink.raw + ' ' + filepath, function (err, data) {
if (!err) {
data = data.match(/eleted (.*)/)[1];
}
callback.call(callback, err, data);
}, runLevel, holdQueue);
};//}}}
FTP.prototype.unlink.raw = 'DELE';
/**
* Runs the FTP command ABOR - Abort a file transfer, this takes place in parallel
* @param {function} callback - The callback function to be issued.
*/
FTP.prototype.abort = function (callback) {//{{{
ftp.raw('ABOR', function (err, data) {
dbg('--------abort-------');
dbg(ftp.pipeActive, ftp.pipeAborted);
dbg(err, data);
if (ftp.pipeActive) {
dbg('pipe was active'.blue);
ftp.pipeAborted = true;
ftp.pipe.end();
}
if (!err) {
data = data.length > 0;
}
callback.call(callback, err, data);
});
};//}}}
FTP.prototype.abort.raw = 'ABOR';
/**
* Runs the FTP command RETR - Retrieve a remote file
* @function
* @param {string} filepath - The location of the remote file to fetch.
* @param {function} callback - The callback function to be issued.
*/
FTP.prototype.get = Queue.create('RETR');
/**
* Runs the FTP command RETR - Retrieve a remote file and
* then saves the file locally
* @param {string|array} paths - An array of [from, to] paths,
* as in read from <b><i>"remote/location/foo.txt"</i></b> and save
* to <b><i>"local/path/bar.txt"</i></b><hr>
* if you are saving to the same relative location you are reading
* from then you can supply a string as a shortcut.
* @param {function} callback - The callback function to be issued.
*/
FTP.prototype.save = function (paths, callback) {//{{{
var isString = typeof paths === 'string',
localPath,
remotePath,
dataHandler;
if (!isString && !(paths instanceof Array)) {
throw new Error('ftp.put > parameter 1 expected an array or string');
} else if (paths.length === 0) {
throw new Error('ftp.put > parameter 1 expected recvd empty array');
}
if (isString || paths.length === 1) {
localPath = remotePath = isString ? paths : paths[0];
} else {
remotePath = paths[0];
localPath = paths[1];
}
dbg('>saving file: ' + remotePath + ' to ' + localPath);
dataHandler = function (err, data) {
if (!err) {
try {
fs.writeFileSync(localPath, data);
} catch (e) {
err = e;
}
}
callback.call(callback, err, localPath);
};
ftp.get(remotePath, dataHandler);
};//}}}
/**
* Creates a new file stat object similar to Node's fs.stat method.
* @constructor
* @memberof FTP
* @param {string} stat - The stat string of the file or directory
* i.e.<br><b>"drwxr-xr-x 2 userfoo groupbar 4096 Jun 12:43 filename"</b>
*/
StatObject = function (stat) {//{{{
var that = this,
currentDate = new Date();
that.isDirectory = stat[1] === 'd';
that.isSymbolicLink = stat[1] === 'l';
that.isFile = stat[1] === '-';
that.permissions = StatObject.parsePermissions(stat[2]);
that.nlink = stat[3];
that.owner = stat[4];
that.group = stat[5];
that.size = stat[6];
that.mtime = Date.parse(currentDate.getFullYear() + ' ' + stat[7]);
//symbolic links will capture their pointing location
if (that.isSymbolicLink) {
stat[8] = stat[8].split(' -> ');
that.linkTarget = stat[8][1];
}
that.filename = that.isSymbolicLink ? stat[8][0] : stat[8];
};//}}}
/**
* Creates a new file stat object similar to Node's fs.stat method, this
* dummy object is ideal for manipulating your own StatObjects
* @constructor
* @memberof FTP
* @param {string} filename - the filename to set for the stat object
* i.e.<br><b>"drwxr-xr-x 2 userfoo groupbar 4096 Jun 12:43 filename"</b>
*/
StatObject.Dummy = function (filename) {
this.filename = filename ? filename : '';
};
StatObject.Dummy.prototype = StatObject.prototype;
FTP.prototype.StatObject = StatObject;
/** @lends StatObject */
StatObject.prototype = {//{{{
/**
* The regular expression used to parse the stat string
* @type {object}
*/
_reg: /([dl\-])([wrx\-]{9})\s+([0-9]+)\s(\w+)\s+(\w+)\s+([0-9]+)\s(\w+\s+[0-9]{1,2}\s+[0-9]{2}:?[0-9]{2})\s+(.*)/,
//TODO -- raw
/**
* The actual response string
* @instance
* @type {string}
*/
raw: '',
/**
* Set to true if path is a directory
* @instance
* @type {boolean}
*/
isDirectory: false,
/**
* Set to true if path is a symbolic link
* @instance
* @type {boolean}
*/
isSymbolicLink: false,
/**
* Set to true if path is a file
* @instance
* @type {boolean}
*/
isFile: false,
/**
* A number representing the set file permissions; ie: 755
* @instance
* @type {null|number}
*/
permissions: null,
/**
* The number of hardlinks pointing to the file or directory
* @instance
* @type {number}
*/
nlink: 0,
/**
* The owner of the current file or directory
* @instance
* @type {string}
*/
owner: '',
/**
* The group belonging to the file or directory
* @instance
* @type {string}
*/
group: '',
/**
* The size of the file in bytes
* @instance
* @type {number}
*/
size: 0,
/**
* The files <b>relative*</b> modification time. *Since stat strings only
* give us accuracy to the minute, it's more accurate to perform a
* {@link FTP#filemtime} on your file if you wish to compare
* modified times <i>more accurately</i>.
* @instance
* @type {number}
*/
mtime: 0,
/**
* If the filepath points to a symbolic link then this
* will hold a reference to the link's target
* @instance
* @type {null|string}
*/
linkTarget: null,
/**
* The name of the directory, file, or symbolic link
* @instance
* @type {string}
*/
filename: ''
};//}}}
/**
* Create and return a new {@link StatObject} instance
* @param {string} stat - The stat string of the file or directory.
* @returns {object} StatObject - New StatObject
*/
StatObject.create = function (stat) {//{{{
return new StatObject(stat);
};//}}}
/**
* Parses a permission string into it's relative number value
* @param {string} permissionString - The string of permissions i.e. <b>"drwxrwxrwx"</b>
* @returns {number} permissions - The number value of the given permissionString
*/
StatObject.parsePermissions = function (permissionString) {//{{{
var i = 0,
c,
perm,
val = [],
lap = 0,
str = '';
for (i; i < permissionString.length; i += 3) {
str = permissionString.slice(i, i + 3);
perm = 0;
for (c = 0; c < str.length; c++) {
if (str[c] === '-') {
continue;
}
perm += StatObject.values[str[c]];
}
val.push(perm);
lap += 1;
}
return Number(val.join(''));
};//}}}
/**
* Holds the relative number values for parsing permissions
* with {@link StatObject.parsePermissions}
* @static StatObject.values
* @type {object}
*/
StatObject.values = {//{{{
'r': 4,
'w': 2,
'x': 1
};//}}}
Queue.registerHook('LIST', function (data, reg = StatObject.prototype._reg) {//{{{
dbg('ls:hook> data: '.magenta, data);
let list = [];
if (!data) {
return list;
}
data = data.toString().split('\r\n').filter(Boolean);
data.reduce((acc, stat) => {
stat = stat.match(reg);
if (stat) {
acc.push(StatObject.create(stat));
}
return acc;
}, list);
return list;
});//}}}
/**
* Runs the FTP command LIST - List remote files
* @function
* @param {string} filepath - The location of the remote file or directory to list.
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
*
*/
FTP.prototype.ls = Queue.create('LIST');
/**
* Runs the FTP command MDTM - Return the modification time of a file
* @param {string} filepath - The location of the remote file to stat.
* @param {function} callback - The callback function to be issued.
* @returns {number} filemtime - File modified time in milliseconds
* @example
* //getting a date object from the file modified time
* ftp.filemtime('foo.js', function (err, filemtime) {
* if (err) {
* dbg(err);
* } else {
* dbg(filemtime);
* //1402849093000
* var dateObject = new Date(filemtime);
* //Sun Jun 15 2014 09:18:13 GMT-0700 (PDT)
* }
* });
*/
FTP.prototype.filemtime = function (filepath, callback, runLevel, holdQueue) {//{{{
ftp.run('MDTM ' + filepath, function (err, data) {
if (!err) {
data = data.match(/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})/);
if (null !== data) {
data = Date.parse(data[1] + '-' + data[2] + '-' + data[3] + ' ' +
data[4] + ':' + data[5] + ':' + data[6]);
}
}
callback.call(callback, err, data);
});
};//}}}
FTP.prototype.filemtime.raw = 'MDTM';
Queue.registerHook('NLST', function (data) {//{{{
if (null === data) {
return [];
}
var filter = function (elem) {
return elem.length > 0 && elem !== '.' && elem !== '..';
};
data = data.toString().split('\r\n').filter(filter);
return data;
});//}}}
/**
* Runs the FTP command NLST - Name list of remote directory.
* @function
* @param {string} dirpath - The location of the remote directory to list.
* @param {function} callback - The callback function to be issued.
*/
FTP.prototype.lsnames = Queue.create('NLST');
/**
* Runs the FTP command SIZE - Get size of remote file
* @function
* @param {string} filepath - The location of the file to retrieve size from.
* @param {function} callback - The callback function to be issued.
*/
FTP.prototype.size = Queue.create('SIZE');
/**
* Runs the FTP command USER - Send username.
* @param {string} username - The name of the user to log in.
* @param {function} callback - The callback function to be issued.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.user = function (user, callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.user.raw + ' ' + user, callback, runLevel, holdQueue);
};
FTP.prototype.user.raw = 'USER';
/**
* Runs the FTP command PASS - Send password.
* @param {string} pass - The password for the user.
* @param {function} callback - The callback function to be issued.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.pass = function (pass, callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.pass.raw + ' ' + pass, callback, runLevel, holdQueue);
};
FTP.prototype.pass.raw = 'PASS';
/**
* Runs the FTP command PASV - Open a data port in passive mode.
* @param {string} pasv - The pasv parameter a1,a2,a3,a4,p1,p2
* where a1.a2.a3.a4 is the IP address and p1*256+p2 is the port number
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.pasv = function (callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.pasv.raw, callback, runLevel, holdQueue);
};
FTP.prototype.pasv.raw = 'PASV';
/**
* Runs the FTP command PORT - Open a data port in active mode.
* @param {string} port - The port parameter a1,a2,a3,a4,p1,p2.
* This is interpreted as IP address a1.a2.a3.a4, port p1*256+p2.
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.port = function (port, callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.port.raw + ' ' + port, callback, runLevel, holdQueue);
};
FTP.prototype.port.raw = 'PORT';
/**
* Runs the FTP command QUIT - Terminate the connection.
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.quit = function (callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.quit.raw, callback, runLevel, holdQueue);
};
FTP.prototype.quit.raw = 'QUIT';
/**
* Runs the FTP command NOOP - Do nothing; Keeps the connection from timing out;
* determine latency(ms), the latency will be passed as the data(second)
* parameter of the callback
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.ping = function (callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.ping.raw, function (err) {
callback.call(this, err, this.ping);
}, runLevel, holdQueue);
};
FTP.prototype.ping.raw = 'NOOP';
/**
* Runs the FTP command STAT - Return server status
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.stat = function (callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.stat.raw, callback, runLevel, holdQueue);
};
FTP.prototype.stat.raw = 'STAT';
/**
* Runs the FTP command SYST - return system type
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.info = function (callback, runLevel, holdQueue) {
ftp.run(FTP.prototype.info.raw, callback, runLevel, holdQueue);
};
FTP.prototype.info.raw = 'SYST';
/**
* Runs the FTP command RNFR and RNTO - Rename from and rename to; Rename a remote file
* @param {array} paths - The path of the current file and the path you wish
* to rename it to; eg: ['from', 'to']
* @param {function} callback - The callback function to be issued.
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.rename = function (paths, callback, holdQueue) {//{{{
if (!(paths instanceof Array)) {
throw new Error('ftp.rename > parameter 1 expected array; [from, to]');
}
var from = paths[0],
to = paths[1];
//run this in a queue
ftp.run('RNFR ' + from, function (err, data) {
if (err) {
callback.call(callback, err, data);
ftp.emit('endproc');
} else {
//run rename to command immediately
ftp.run(FTP.prototype.rename.raw + ' ' + to, callback, true, holdQueue);
}
}, false, true);
};//}}}
FTP.prototype.rename.raw = 'RNTO';
/**
* Runs the FTP command TYPE - Set transfer type (default ASCII) - <b>will be added in next patch</b>
* @param {string} type - set to this type: 'ascii', 'ebcdic', 'binary', 'local'
* @param {string} secondType - 'nonprint', 'telnet', 'asa'
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.type = function (type, secondType, callback, runLevel, holdQueue) {
var that = this,
cmd = '';
if (typeof secondType === 'function') {
holdQueue = runLevel;
runLevel = callback;
callback = secondType;
secondType = undefined;
}
if (undefined === type || undefined === that.typeMap[type]) {
return callback(new Error('ftp.type > parameter 1 expected valid FTP TYPE; [ascii, binary, ebcdic, local]'), null);
}
cmd = that.typeMap[type];
if (undefined !== secondType && undefined === that.secondTypeMap[secondType]) {
cmd += ' ' + that.secondTypeMap[secondType];
}
//update currentType and run
var done = function (err, data) {
ftp.currentType = type;
callback.call(callback, err, data);
};
ftp.run(FTP.prototype.type.raw + ' ' + cmd, done, runLevel, holdQueue);
};
FTP.prototype.type.raw = 'TYPE';
/**
* Sets the type of file transfer that should be used
* based on the path provided
* @params {string} filepath - the path to the file being transferred
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.setType = function (filepath, callback, runLevel, holdQueue) {
var ext,
hijack = function () {
callback();
if (!holdQueue) {
ftp.emit('endproc');
}
},
args = [undefined, callback, runLevel, holdQueue],
changeType = function (type) {
args[0] = type;
ftp.type.apply(ftp, args);
};
let dotIndex = filepath.indexOf('.');
let ensureType = () => {
if (ftp.currentType !== 'ascii') {
changeType('ascii');
} else {
ftp.queue.register(hijack, runLevel);
}
};
//dot files eg .htaccess or no extension
if (dotIndex < 1) {
return ensureType();
}
ext = filepath.split('.').pop();
if (ftp.ascii[ext]) {
ensureType();
} else if (ftp.currentType === 'ascii') {
changeType('binary');
} else {
ftp.queue.register(hijack, runLevel);
}
};
/**
* Values that are used with {@link FTP#setType} and {@link FTP#type}
* to set the transfer type of data
* @readonly
* @class
* @enum {string}
*/
FTP.prototype.typeMap = {
ascii: 'A',
binary: 'I',
ebcdic: 'E',
local: 'L'
};
/**
* @readonly
* @class
* @enum {string}
*/
FTP.prototype.secondTypeMap = {
nonprint: 'N',
telnet: 'T',
asa: 'C'
};
/*
ftp.raw('TYPE A N', function (err, data) {
dbg(err, data);
});
*/
/**
* Runs the FTP command MODE - Set transfer mode (default Stream) - <b>will be added in next patch</b>
* @param {string} type - set to this type: 'stream', 'block', 'compressed'
* @todo - This still needs to be added - should create an object of methods
*/
FTP.prototype.mode = function () {
dbg('not yet implemented');
};
/**
* Runs the FTP command SITE - Run site specific command - <b>will be added in next patch</b>
* @param {string} command - The command that will be issued
* @param {string} parameters - The parameters to be passed with the command
* @todo - This still needs to be added - should create an object of methods
* @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}.
* @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually
*/
FTP.prototype.site = function () {
dbg('not yet implemented');
};
/**
* @class DEPRECATED! use {@link FTP.Queue}
*/
FTP.prototype.SimpleQueue = Queue;
FTP.prototype.Queue = Queue;
FTP.Queue = Queue;
module.exports = FTP;