├── test ├── foo.pdf ├── ftpimp.jpg └── index.js ├── .gitignore ├── config.sample.js ├── .npmignore ├── .jshintrc ├── docker-compose.yml ├── .travis.yml ├── jsdoc.json ├── package.json ├── docker └── Dockerfile ├── lib └── command.js ├── README.md └── index.js /test/foo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkida/ftpimp/HEAD/test/foo.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | log 3 | node_modules 4 | site 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /test/ftpimp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkida/ftpimp/HEAD/test/ftpimp.jpg -------------------------------------------------------------------------------- /config.sample.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: 'localhost', 3 | port: '21', 4 | user: 'root', 5 | pass: '', 6 | debug: false 7 | }; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | log 2 | node_modules 3 | site 4 | build 5 | docs 6 | config* 7 | .git* 8 | .travis.yml 9 | jsdoc.json 10 | .jshintrc 11 | test 12 | docker 13 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "loopfunc": true, 3 | "node": true, 4 | "mocha": true, 5 | "undef": true, 6 | "unused": "vars", 7 | "curly": true, 8 | "laxbreak": true, 9 | "esnext": true, 10 | "predef": [] 11 | } 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | vsftpd: 2 | image: fauria/vsftpd 3 | container_name: vsftpd 4 | environment: 5 | #your host ip, may just be 127.0.0.1 6 | - PASV_ADDRESS=192.168.10.10 7 | ports: 8 | - 21:21 9 | - 20:20 10 | - 21100-21110:21100-21110 11 | pure-ftpd: 12 | image: sparkida/pure-ftpd 13 | container_name: pure-ftpd 14 | net: host 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | before_install: 5 | - cp config.sample.js config.js 6 | - sed -i 's/pass:.*/pass:"travis",/' config.js 7 | - sed -i 's/root/travis/' config.js 8 | - docker run -d --net=host sparkida/pure-ftpd 9 | - sleep 1 10 | node_js: 11 | - '8' 12 | - '7' 13 | - '6' 14 | - '5' 15 | - '4' 16 | git: 17 | depth: 3 18 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": [ 5 | "jsdoc", 6 | "closure" 7 | ] 8 | }, 9 | "source": { 10 | "include": [ 11 | "./README.md", 12 | "./index.js", 13 | "./lib", 14 | "./config.sample.js" 15 | ], 16 | "exclude": [ 17 | "./node_modules" 18 | ], 19 | "includePattern": ".+\\.js(doc)?$", 20 | "excludePattern": "(^|\\/|\\\\)_" 21 | }, 22 | "plugins": [], 23 | "templates": { 24 | "cleverLinks": false, 25 | "monospaceLinks": false 26 | }, 27 | "opts": { 28 | "destination": "./docs", 29 | "recurse": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ftpimp", 3 | "version": "3.0.5", 4 | "preferGlobal": false, 5 | "license": "MIT", 6 | "homepage": "https://sparkida.github.io/ftpimp/index.html", 7 | "description": "FTP client for Windows, OSX and Linux.\nFTPimp is an (imp)roved implementation of the FTP service API for NodeJS.", 8 | "main": "./index", 9 | "directories": { 10 | "lib": "./lib" 11 | }, 12 | "scripts": { 13 | "test": "./node_modules/.bin/mocha --reporter spec -b" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/sparkida/ftpimp" 18 | }, 19 | "keywords": [ 20 | "ftp", 21 | "client", 22 | "transfer", 23 | "mput", 24 | "mget" 25 | ], 26 | "devDependencies": { 27 | "jsdoc": "^3.3.x", 28 | "mocha": "^2.3.x" 29 | }, 30 | "dependencies": { 31 | "colors": "*" 32 | }, 33 | "engine": { 34 | "node": ">=0.10.x" 35 | }, 36 | "author": { 37 | "name": "Nick Riley", 38 | "url": "http://sparkida.com" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | # properly setup debian sources 4 | ENV DEBIAN_FRONTEND noninteractive 5 | RUN echo "deb http://http.debian.net/debian jessie main\n\ 6 | deb-src http://http.debian.net/debian jessie main\n\ 7 | deb http://http.debian.net/debian jessie-updates main\n\ 8 | deb-src http://http.debian.net/debian jessie-updates main\n\ 9 | deb http://security.debian.org jessie/updates main\n\ 10 | deb-src http://security.debian.org jessie/updates main\n\ 11 | " > /etc/apt/sources.list 12 | RUN apt-get update -qq && \ 13 | # install package building helpers 14 | apt-get -y --force-yes --fix-missing install dpkg-dev debhelper && \ 15 | apt-get -y build-dep pure-ftpd && \ 16 | cd /tmp && apt-get source pure-ftpd && \ 17 | cd pure-ftpd-* && \ 18 | sed -i '/^optflags=/ s/$/ --without-capabilities/g' ./debian/rules && \ 19 | sed -i 's/ --with-largefile//g' ./debian/rules && \ 20 | ./configure --without-capabilities --disable-largefile && \ 21 | dpkg-buildpackage -b -uc && \ 22 | dpkg -i /tmp/pure-ftpd-common*.deb && \ 23 | apt-get -y install openbsd-inetd && \ 24 | dpkg -i /tmp/pure-ftpd_*.deb && \ 25 | apt-mark hold pure-ftpd pure-ftpd-common && \ 26 | rm -rf /tmp/* && \ 27 | groupadd ftpgroup && \ 28 | useradd -g ftpgroup -d /dev/null -s /etc ftpuser && \ 29 | mkdir -p /home/ftpusers/travis && \ 30 | (echo travis; echo travis) | pure-pw useradd travis -d /home/ftpusers/travis -u ftpuser && \ 31 | pure-pw mkdb 32 | 33 | RUN chown -hR ftpuser:ftpgroup /home/ftpusers 34 | CMD pure-ftpd -c 1 -C 5 -l puredb:/etc/pure-ftpd/pureftpd.pdb -Ep 30000:30009 35 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTPimp Response Handler 3 | * @author Nicholas Riley 4 | * @module lib/command 5 | */ 6 | "use strict"; 7 | var ftp, 8 | cmd, 9 | dbg = function () { 10 | return undefined; 11 | }, 12 | CMD = function () { 13 | cmd = this; 14 | }; 15 | 16 | 17 | /** 18 | * Create and return a new CMD instance 19 | * @param {object} ftpObject - The FTP instance object 20 | * @returns New CMD object 21 | */ 22 | CMD.create = function (ftpObject) { 23 | ftp = ftpObject; 24 | if (ftp.config.debug) { 25 | dbg = function (msg) { 26 | console.log(msg); 27 | }; 28 | } 29 | return new CMD(); 30 | }; 31 | 32 | 33 | /** 34 | * List of command response codes and their 35 | * attributing event that will be fired; 36 | * you can add your own event listeners to 37 | * listen to these codes 38 | * @member {object} CMD#codes 39 | * @property {number} 150 - dataPortReady 40 | * @property {number} 220 - login 41 | * @property {number} 226 - transferComplete 42 | * @property {number} 227 - startPassive 43 | * @property {number} 230 - ready 44 | * @property {number} 250 - fileActionComplete [default: disabled] 45 | * @property {number} 257 - data capture [default: disabled] 46 | * @property {number} 331 - sendPass [default: disabled] 47 | * @property {number} 500 - unkownCommand 48 | * @property {number} 550 - transferError 49 | * @property {number} 553 - transferError 50 | */ 51 | CMD.prototype.codes = { 52 | 125: 'dataPortReady', 53 | 150: 'dataPortReady', 54 | 220: 'login', 55 | //we will call the cmd from the ftp function 56 | 226: 'transferComplete', 57 | 227: 'startPassive', 58 | 230: 'ready', 59 | //250: 'fileActionComplete', 60 | //257: data capture 61 | //331: 'sendPass', 62 | 500: 'unknownCommand', 63 | 550: 'transferError', 64 | 553: 'transferError' 65 | }; 66 | 67 | CMD.prototype.keys = CMD.prototype.codes; 68 | 69 | CMD.prototype.transferError = function (data) { 70 | ftp.emit('transferError', data); 71 | }; 72 | 73 | /** @fires FTP#fileTransferComplete */ 74 | CMD.prototype.fileActionComplete = function (data) { 75 | ftp.emit('fileActionComplete', data); 76 | }; 77 | 78 | 79 | /** 80 | * Emit a fileTransferComplete or dataTransferComplete event on the {@link FTP#events} object 81 | * @fires FTP#dataTransferComplete 82 | * @function CMD#transferComplete 83 | */ 84 | CMD.prototype.transferComplete = function (data) {//{{{ 85 | /** 86 | * Fired when we receive a remote acknowledgement 87 | * of the files successful transfer 88 | * @event FTP#fileTransferComplete 89 | */ 90 | //dbg('file transfer complete'); 91 | //ftp.emit('fileTransferComplete', data); 92 | if (ftp.cueDataTransfer) { 93 | dbg('CMD> data transfer complete'); 94 | ftp.cueDataTransfer = false; 95 | ftp.emit('dataTransferComplete');//, data); 96 | //ftp.emit('endproc'); 97 | } 98 | };//}}} 99 | 100 | 101 | /** 102 | * Sets the cueDataTransfer so we know we are 103 | * specifically performing data fetching 104 | * @function CMD#dataPortReady 105 | */ 106 | CMD.prototype.dataPortReady = function (data) {//{{{ 107 | dbg('---data port ready---'); 108 | ftp.cueDataTransfer = true; 109 | ftp.openPipes += 1; 110 | ftp.totalPipes += 1; 111 | //ftp.emit('dataPortReady'); 112 | };//}}} 113 | 114 | 115 | /** 116 | * Emit an error on the {@link FTP#socket} object 117 | * @fires FTP#socket#error 118 | * @function CMD#error 119 | */ 120 | CMD.prototype.error = function (data) {//{{{ 121 | /** 122 | * Fired at the onset of a socket error 123 | * @event FTP#socket#error 124 | */ 125 | ftp.socket.emit('error', data); 126 | };//}}} 127 | 128 | 129 | /** 130 | * Emit an error on the {@link FTP#socket} object 131 | * @fires FTP#socket#error 132 | * @function CMD#unknownCommand 133 | */ 134 | CMD.prototype.unknownCommand = CMD.prototype.error; 135 | 136 | 137 | /** 138 | * Emit a "ready" event on the {@link FTP#socket} object 139 | * @fires FTP#socket#ready 140 | * @function CMD#ready 141 | */ 142 | CMD.prototype.ready = function () {//{{{ 143 | /** 144 | * Fired at the onset of a socket error 145 | * @event FTP#socket#ready 146 | */ 147 | //ftp.emit('ready'); 148 | };//}}} 149 | 150 | 151 | /** 152 | * Log in to the FTP server with set configuration 153 | * @function CMD#login 154 | */ 155 | CMD.prototype.login = function () {//{{{ 156 | dbg('>Authenticating...'); 157 | ftp.user(ftp.config.user, function (err, data) { 158 | if (err) { 159 | dbg(err); 160 | dbg('an error occured sending the user'); 161 | return; 162 | } 163 | dbg(data); 164 | dbg('user sent'); 165 | ftp.pass(ftp.config.pass, function (err, data) { 166 | if (err) { 167 | dbg(err); 168 | return; 169 | } 170 | dbg('password sent'); 171 | ftp.raw('CWD', function (res) { 172 | var dir = res.indexOf('/'); 173 | dir = res.slice(dir - 1).trim(); 174 | ftp.cwd = ftp.baseDir = dir; 175 | dbg('current dir: ' + ftp.cwd); 176 | ftp.emit('ready'); 177 | ftp.isReady = true; 178 | }); 179 | }); 180 | }); 181 | };//}}} 182 | 183 | /** 184 | * Opens a passive (PASV) connection to the FTP server 185 | * with the data received from the socket that made the 186 | * "PASV" request 187 | * @function CMD#startPassive 188 | * @param {string} data - The returned socket data 189 | */ 190 | CMD.prototype.startPassive = function (data) {//{{{ 191 | var matches = data.match(/(([0-9]{1,3},){4})([0-9]{1,3}),([0-9]{1,3})?/), 192 | port; 193 | if (null === matches) { 194 | throw new Error('could not establish a passive connection'); 195 | } 196 | port = ftp.config.pasvPort = Number(matches[3] * 256) + Number(matches[4]); 197 | ftp.config.pasvString = matches[0]; 198 | ftp.pipeClosed = false; 199 | dbg('passive settings updated'); 200 | ftp.emit('commandComplete'); 201 | };//}}} 202 | 203 | 204 | 205 | module.exports = CMD; 206 | 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FTPimp [![Build Status](https://travis-ci.org/sparkida/ftpimp.svg?branch=master)](https://travis-ci.org/sparkida/ftpimp) 2 | ====== 3 | 4 | FTP client for Windows and OSX / Linux. 5 | 6 | FTPimp is an (imp)roved implementation of the FTP service API for NodeJS. It has unique features that you'd otherwise expect an FTP client to have... 7 | 8 | 9 | #### Supported Node Versions 10 | 11 | - 4.x 12 | - 5.x 13 | - 6.x 14 | - 7.x 15 | - 8.x 16 | 17 | Upgrading to 3.0 (Feb 28th, 2017) 18 | ================ 19 | 20 | Only one real breaking change for anyone using ftpimp < 3.0, **data returned is now a Buffer**. This may affect methods that try to perform special String methods on a Buffer object (ie String.prototype.split) 21 | 22 | Features 23 | -------- 24 | 25 | FTPimp has several major benefits in comparison to other Node FTP clients: 26 | - Recursively put files, and create directories 27 | - Recursively delete directories 28 | - Optional, automated login 29 | - Overrideable methods 30 | - UNIX and Windows 31 | - Easily work with every step of the event based FTP process 32 | - Propietary queue and helper methods for controlling flow and easily extending FTPimp's functionality 33 | 34 | 35 | 36 | API Documentation 37 | ----------------- 38 | 39 | [Documentation for ftp-imp](https://sparkida.github.io/ftpimp) can be found at the website [¬https://sparkida.github.io/ftpimp](https://sparkida.github.io/ftpimp) 40 | 41 | **Tests provide an example for every (practical) endpoint in the library** [¬see those here](https://github.com/sparkida/ftpimp/blob/master/test/index.js). 42 | 43 | 44 | Process flow and Queueing procedures 45 | ------------------------------------ 46 | 47 | **By default, every call is sequential.** To have more granular control, use the [Queue.RunLevels](https://sparkida.github.io/ftpimp/FTP.Queue.html#.RunLevels) 48 | 49 | You'll likely only need to use "Queue.RunNext" to prioritize a command over any subsequent commands. In 50 | the example below (**#1**), the sequence is [**mkdir**, **ls**, **put**] 51 | 52 | ***example #1:*** 53 | 54 | ```javascript 55 | //make a "foo" directory 56 | ftp.mkdir('foo', function (err, dir) { //runs first 57 | ftp.put(['bar.txt', 'foo/bar.txt'], function (err, filename) { //runs third 58 | }); 59 | }); 60 | 61 | ftp.ls('foo', function (err, filelist) { //runs second 62 | ... 63 | }); 64 | ``` 65 | 66 | 67 | While in the next example below(#2) we use [Queue.RunNext](https://sparkida.github.io/ftpimp/FTP.Queueu.html#.RunNext) 68 | to prioritize our "put", over that of the "ls", making our sequence [**mkdir**, **put**, **ls**] 69 | 70 | ***example #2:*** 71 | 72 | ```javascript 73 | +var Queue = FTP.Queue; 74 | //make a "foo" directory 75 | ftp.mkdir('foo', function (err, dir) { //runs first 76 | ftp.put(['bar.txt', 'foo/bar.txt'], function (err, filename) { //runs second 77 | - }); 78 | + }, Queue.RunNext); 79 | }); 80 | 81 | ftp.ls('foo', function (err, filelist) { //runs third 82 | ... 83 | }); 84 | ``` 85 | 86 | Examples 87 | -------- 88 | 89 | **Default config** 90 | 91 | ```javascript 92 | var config = { 93 | host: 'localhost', 94 | port: 21, 95 | user: 'root', 96 | pass: '', 97 | debug: false 98 | }; 99 | ``` 100 | 101 | **Automatically login to FTP and run callback when ready** 102 | 103 | ```javascript 104 | var FTP = require('ftpimp'), 105 | ftp = FTP.create(config), 106 | connected = function () { 107 | console.log('connected to remote FTP server'); 108 | }; 109 | 110 | ftp.once('ready', connected); 111 | ``` 112 | 113 | **Setup FTPimp and login whenever** 114 | 115 | ```javascript 116 | var FTP = require('ftpimp'), 117 | ftp = FTP.create(config, false); 118 | 119 | //do some stuff... 120 | ftp.connect(function () { 121 | console.log('Ftp connected'); 122 | }); 123 | ``` 124 | 125 | **Put file to remote server** 126 | 127 | ```javascript 128 | ftp.put(['path/to/localfile', 'remotepath'], function (err, filename) { 129 | console.log(err, filename); 130 | }); 131 | ``` 132 | 133 | **Get file from remote server** 134 | 135 | ```javascript 136 | ftp.save(['path/to/remotefile', 'path/to/local/savefile'], function (err, filename) { 137 | console.log(err, filename); 138 | }); 139 | ``` 140 | 141 | **Recursively create directories.** 142 | 143 | ```javascript 144 | ftp.mkdir('foo/deep/directory', function (err, created) { 145 | console.log(err, created); 146 | }, true); 147 | ``` 148 | 149 | **Recursively delete directories and their contents** 150 | 151 | ```javascript 152 | ftp.rmdir('foo', function (err, deleted) { 153 | console.log(err, deleted); 154 | }, true); 155 | ``` 156 | 157 | **List remote directory contents** 158 | 159 | ```javascript 160 | ftp.ls('foo', function (err, filelist) { 161 | console.log(err, filelist); 162 | }); 163 | ``` 164 | 165 | 166 | FTP Connection Types 167 | -------------------- 168 | 169 |

Passive vs Active

170 | 171 | By default, FTPimp uses passive connections for security purposes, but **you can override anything** you want pretty quickly to build a very robust FTP application. 172 | 173 | 174 | 175 | 176 | Find a Bug? 177 | ----------- 178 | 179 | Please let me know so that I can fix it ASAP, cheers 180 | [¬Report a Bug](https://github.com/sparkida/ftpimp/issues) 181 | 182 | 183 | 184 | 185 | Updates 186 | ------- 187 | * Oct 12, 2015 8:11am(PDT) - v2.2.2rc All tests passing; 188 | - Queueing order is sequential by default, this may break compatability, but resolves a lot of issues and removes barriers in progressive enhancements; 189 | - Readme simplified, more examples, less clutter thanks to ^; 190 | - Greatly refactored: put, rmdir, mkdir, setType; 191 | - ls and lsnames now return an empty array instead of false when no files are found 192 | * Sep 10, 2015 7:46am(PDT) - v2.0.0a! Alpha release. Major changes in architecture, documentation updated, testing suite moved to Mocha! 193 | - FTP.type now returns an error instead of throwing one 194 | - FTP.save no longer returns filename in the result parameter on error 195 | * May 14, 2015 8:50am(PDT) - v1.3! Addresses issues that occur when working in cross platform environment. This added automated switching between `binary` and `ascii(default)` 196 | * Apr 25, 2015 10:09am(PDT) - v1.2! Linted. Addressed issues that prevented data from transferring completely when using things like `ls` `lsnames` etc... 197 | * Aug 21, 2014 9:56am(PDT) - Fixed an issue where the ftp host and port were hard coded in, connection will now use ftp configuration as intended. Thanks to [broggeri](https://github.com/broggeri)! 198 | * July 9, 2014 8:08am(PDT) - **Major Update** - v1.0.0 - This is the pre-release candidate, everything has passed testing at this point, I will shift my focus to documentation and environment specific testing while tackling active and passive connection concerns. 199 | * July 8, 2014 3:38am(PDT) - v0.5.42 - The primary Queue **FTP.queue** will now emit a **"queueEmpty"** event when the last item in the queue completes. 200 | * July 8, 2014 3:21am(PDT) - v0.5.4 - **FTP.rename** will return an error if the file is not found 201 | * July 7, 2014 8:46am(PDT) - Fixed a queueing issue that occured when recursively removing directories using **FTP.rmdir**. 202 | * July 7, 2014 6:46am(PDT) - Fixed an issue that occurred when receiving data through ls, lsnames. 203 | * July 5, 2014 8:36am(PDT) - FTP.mkdir will now make recursive directories within the same queue group. Queue groups are a new feature as of **V0.5.0** 204 | * July 4, 2014 9:15pm(PDT) - **Major Update** Beta v0.5.0 **stable** 205 | - The primary queue that runs all methods - **(FTP.run)** - now provides full control over how you queue your processes with the use of two parameters
**runNow** - to run the next command immediately
&
**queueGroup** - this tells FTP.run that the command belongs to a queue group and which will escape the **endproc** that loads and fires the next queue in line. Queue groups are one level deep and exist until a command is used where the **queueGroup** parameter is false. 206 | * July 1, 2014 6:44am(PDT) - FTP.put method will no longer prioritize put requests. Execution order is now linear. 207 | * Jun 26, 2014 7:41am(PDT) - Methods can now be passed a runNow parameter, to bypass queueing 208 | * Jun 23, 2014 8:24pm(PDT) - Restructured queues to work within FTP.run 209 | * Jun 23, 2014 7:46am(PDT) - Fixed a queueing issue with mkdir 210 | * Jun 22, 2014 5:20am(PDT) - FTP.mkdir and FTP.rmdir both have the option to recursively create and delete directories. 211 | * Jun 21, 2014 3:43pm(PDT) - Fixed regular expression in FTP.ls to grab deep paths 212 | * Jun 21, 2014 12:16pm(PDT) - FTP.put will now return error if the local file is not found 213 | * Jun 20, 2014 6:56am(PDT) - Fixed an issue with errors not being sent to the callback method 214 | * Jun 19, 2014 4:15pm(PDT) - Fixed an issue that occured when 0 bytes are received in data transfers 215 | * Jun 19, 2014 1:35pm(PDT) - **Major Update** Beta v0.4.0 **stable** 216 | - **FTP.connect has been replaced for FTP.create** 217 | - Resolved all known issues with the queueing of commands and data transfers. Good to Go! 218 | * Jun 18, 2014 4:35am(PDT) - Fixed an issue with performing multiple data requests 219 | * Jun 18, 2014 10:35am(PDT) - Fixed an issue with the response handler failing at login 220 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * The FTPimp testing suite 4 | * (c) 2014 Nicholas Riley, Sparkida. All Rights Reserved. 5 | * @module test/index 6 | */ 7 | var assert = require('assert'), 8 | fs = require('fs'), 9 | FTP = require('../'), 10 | Queue = FTP.prototype.Queue, 11 | config = require('../config'), // jshint ignore:line 12 | path = require('path'), 13 | ftp; 14 | config.debug = process.argv.indexOf('--debug') > -1; 15 | describe('FTPimp', function () { 16 | //TODO - change to main 17 | before(function (done) { 18 | this.timeout(10000); 19 | /**create new FTP instance connection 20 | * and login are automated */ 21 | ftp = FTP.create(config, false); 22 | ftp.connect(done); 23 | }); 24 | 25 | describe('Simple commands have a "raw" property string of the command', function () { 26 | var com = { 27 | ls: 'LIST', 28 | lsnames: 'NLST', 29 | port: 'PORT', 30 | pasv: 'PASV', 31 | chdir: 'CWD', 32 | mkdir: 'MKD', 33 | rmdir: 'RMD', 34 | type: 'TYPE', 35 | rename: 'RNTO', 36 | get: 'RETR', 37 | filemtime: 'MDTM', 38 | unlink: 'DELE', 39 | getcwd: 'PWD', 40 | ping: 'NOOP', 41 | stat: 'STAT', 42 | info: 'SYST', 43 | abort: 'ABOR', 44 | quit: 'QUIT' 45 | }; 46 | Object.keys(com).forEach(function (key) { 47 | it('FTP.prototype.' + key + ' has raw ' + com[key], function () { 48 | assert.equal(FTP.prototype[key].raw, com[key]); 49 | }); 50 | }); 51 | }); 52 | 53 | var testDir = 'ftpimp.test.' + String(new Date().getTime()).slice(3) + '.tmp'; 54 | describe('mkdir#MKD: make a remote directory', function () { 55 | it ('succeeds', function (done) { 56 | ftp.mkdir(path.join(testDir, 'foo'), function (err, res) { 57 | assert.equal(res.length, 2, 'Could not add directories'); 58 | done(err); 59 | }, true); 60 | }); 61 | it ('succeeds at making directory with a character', function (done) { 62 | ftp.mkdir(path.join(testDir, '中文'), function (err, res) { 63 | assert.equal(res.length, 1, 'Could not add directories'); 64 | done(err); 65 | }, true); 66 | }); 67 | it ('succeeds at making a directory with a space', function (done) { 68 | ftp.mkdir(path.join(testDir, 'foo bar'), function (err, res) { 69 | assert.equal(res.length, 1, 'Could not add directories'); 70 | done(err); 71 | }, true); 72 | }); 73 | it ('fails', function (done) { 74 | ftp.mkdir('', function (err, res) { 75 | assert(err instanceof Error); 76 | assert(!res); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('chdir#CWD: change working directory', function () { 83 | it ('fails', function (done) { 84 | ftp.chdir('somebadlookup', function (err, res) { 85 | assert(err instanceof Error); 86 | assert(!res); 87 | done(); 88 | }); 89 | }); 90 | it ('succeeds, changing to testDir - ' + testDir, function (done) { 91 | ftp.chdir(testDir, function (err, res) {//testDir, function (err, res) { 92 | assert(typeof res === 'string'); 93 | done(err); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('type#TYPE: set transfer types', function () { 99 | it ('fails', function (done) { 100 | ftp.type('badTypeError', function (err, res) { 101 | assert(err instanceof Error); 102 | assert(!res); 103 | done(); 104 | }); 105 | }); 106 | it ('changed type to image (binary data)', function (done) { 107 | ftp.type('binary', function (err, res) { 108 | assert(res); 109 | done(err); 110 | }); 111 | }); 112 | it.skip ('changed type to EBCDIC text(not available)', function (done) { 113 | ftp.type('ebcdic', function (err, res) { 114 | assert(res); 115 | done(err); 116 | }); 117 | }); 118 | it ('changed type to local format', function (done) { 119 | ftp.type('local', function (err, res) { 120 | assert(res); 121 | done(err); 122 | }); 123 | }); 124 | it ('changed type to ASCII text', function (done) { 125 | ftp.type('ascii', function (err, res) { 126 | assert(res); 127 | done(err); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('setType: set transfer type based on file', function () { 133 | it ('uses ASCII as default', function (done) { 134 | ftp.setType('badTypeError', function (err, res) { 135 | assert(ftp.currentType, 'ascii'); 136 | done(); 137 | }); 138 | }); 139 | it ('changes to binary for images', function (done) { 140 | ftp.setType('ftpimp.jpg', function (err, res) { 141 | assert(ftp.currentType, 'binary'); 142 | done(); 143 | }); 144 | }); 145 | it ('changes to ASCII for text', function (done) { 146 | ftp.setType('index.js', function (err, res) { 147 | assert(ftp.currentType, 'ascii'); 148 | done(); 149 | }); 150 | }); 151 | }); 152 | 153 | describe('put: transfers files to remote', function () { 154 | this.timeout(5000); 155 | it ('succeeds', function (done) { 156 | ftp.put(['./test/index.js', 'index.js'], function (err, res) { 157 | assert.equal(ftp.currentType, 'ascii'); 158 | assert.equal(res, 'index.js'); 159 | assert(!err); 160 | }); 161 | ftp.put(['./test/ftpimp.jpg', 'ftpimp.jpg'], function (err, res) { 162 | assert.equal(ftp.currentType, 'binary'); 163 | assert.equal(res, 'ftpimp.jpg'); 164 | assert(!err); 165 | }); 166 | ftp.put(['./test/foo.pdf', 'foo.pdf'], function (err, res) { 167 | assert.equal(ftp.currentType, 'binary'); 168 | assert.equal(res, 'foo.pdf'); 169 | done(err); 170 | }); 171 | }); 172 | it ('fails', function (done) { 173 | ftp.put('badFileError', function (err, res) { 174 | assert(err instanceof Error); 175 | assert(!res); 176 | done(); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('rename#RNTO: rename to', function () { 182 | it ('succeeds', function (done) { 183 | ftp.rename(['index.js', 'ind.js'], function (err, res) { 184 | assert(!!res); 185 | done(err); 186 | }); 187 | }); 188 | it ('fails', function (done) { 189 | ftp.rename(['missingFile', 'foo'], function (err, res) { 190 | assert(err instanceof Error); 191 | assert(!res); 192 | done(); 193 | }); 194 | }); 195 | }); 196 | 197 | describe('get#RETR: retrieve remote file', function () { 198 | it ('succeeds at getting ASCII text file', function (done) { 199 | ftp.get('ind.js', function (err, res) { 200 | assert.equal(ftp.currentType, 'ascii'); 201 | assert(res instanceof Buffer); 202 | done(err); 203 | }); 204 | }); 205 | it ('succeeds at getting binary image file', function (done) { 206 | ftp.get('ftpimp.jpg', function (err, res) { 207 | assert.equal(ftp.currentType, 'binary'); 208 | if (err) { 209 | done(err); 210 | } else { 211 | assert(res instanceof Buffer); 212 | done(); 213 | } 214 | }); 215 | 216 | }); 217 | it ('fails', function (done) { 218 | ftp.get('fileNotFoundError', function (err, res) { 219 | assert(err instanceof Error); 220 | assert(!res); 221 | done(); 222 | }); 223 | }); 224 | }); 225 | 226 | describe('save: retrieve remote file and save to local', function () { 227 | var saved = []; 228 | after(function (done) { 229 | fs.unlink(saved[0], function (delError, res) { 230 | fs.unlink(saved[1], function (delError, res) { 231 | done(delError); 232 | }); 233 | }); 234 | }); 235 | it ('succeeds', function (done) { 236 | ftp.save(['ind.js', 'saved-ind.js'], function (err, res) { 237 | assert.equal(ftp.currentType, 'ascii'); 238 | assert(!!res); 239 | saved.push(res); 240 | }); 241 | ftp.save(['ftpimp.jpg', 'saved-ftpimp.jpg'], function (err, res) { 242 | assert.equal(ftp.currentType, 'binary'); 243 | assert(!!res); 244 | saved.push(res); 245 | assert.deepEqual(['saved-ind.js', 'saved-ftpimp.jpg'], saved); 246 | done(); 247 | }); 248 | }); 249 | it ('fails', function (done) { 250 | ftp.save(['missingFile', 'foo'], function (err, res) { 251 | assert(err instanceof Error); 252 | done(); 253 | }); 254 | }); 255 | }); 256 | 257 | describe('filemtime#MDTM: return the modification time of a remote file', function () { 258 | it ('succeeds', function (done) { 259 | ftp.filemtime('ind.js', function (err, res) { 260 | assert(!isNaN(Number(res))); 261 | done(err); 262 | }); 263 | }); 264 | it ('fails', function (done) { 265 | ftp.filemtime('fileNotFoundError', function (err, res) { 266 | assert(err instanceof Error); 267 | assert(!res); 268 | done(); 269 | }); 270 | }); 271 | }); 272 | 273 | describe('ls#LIST: list remote files', function () { 274 | it ('succeeds', function (done) { 275 | ftp.ls('', function (err, res) { 276 | assert(Array.isArray(res)); 277 | let charFound = false; 278 | res.forEach(function (stat) { 279 | if (stat.filename === '中文') { 280 | charFound = true; 281 | } 282 | }); 283 | assert(charFound, 'Could not find a file with the "#" character'); 284 | done(err); 285 | }); 286 | }); 287 | it ('fails', function (done) { 288 | ftp.ls('somebadlookup', function (err, res) { 289 | assert(!err); 290 | assert(Array.isArray(res), 'expected array result'); 291 | assert.equal(res.length, 0); 292 | done(); 293 | }); 294 | }); 295 | }); 296 | 297 | describe('lsnames#NLST: name list of remote directory', function () { 298 | it ('succeeds', function (done) { 299 | ftp.lsnames('', function (err, res) { 300 | assert(Array.isArray(res)); 301 | done(err); 302 | }); 303 | }); 304 | it ('fails', function (done) { 305 | ftp.lsnames('somebadlookup', function (err, res) { 306 | assert(!err); 307 | assert(Array.isArray(res), 'expected array result'); 308 | assert.equal(res.length, 0); 309 | done(); 310 | }); 311 | }); 312 | }); 313 | 314 | describe('unlink#DELE: delete remote file', function () { 315 | it ('succeeds', function (done) { 316 | ftp.unlink('ind.js', function (err, res) { 317 | assert.equal(res, 'ind.js'); 318 | ftp.unlink('ftpimp.jpg', function (err, res) { 319 | assert.equal(res, 'ftpimp.jpg'); 320 | done(err); 321 | }); 322 | }); 323 | }); 324 | it ('fails', function (done) { 325 | ftp.unlink('fileNotFoundError', function (err, res) { 326 | assert(err instanceof Error); 327 | assert(!res); 328 | done(); 329 | }); 330 | }); 331 | }); 332 | 333 | describe('root: changes to root directory', function () { 334 | it ('succeeds', function (done) { 335 | ftp.root(function (err, res) { 336 | assert(typeof res === 'string'); 337 | done(err); 338 | }); 339 | }); 340 | }); 341 | 342 | describe('rmdir#RMD: recursively remove remote directory', function () { 343 | this.timeout(10000); 344 | it ('should recursively remove the directory ' + testDir, function (done) { 345 | ftp.mkdir(path.join(testDir, 'foo'), function(){}, true); 346 | ftp.rmdir(testDir, function (err, res) { 347 | assert(!err, err); 348 | assert.equal(res.length, 5); 349 | done(); 350 | }, true); 351 | }); 352 | it ('should remove the directory even if it is the only object to be removed', function (done) { 353 | ftp.mkdir(testDir, function(){}, true); 354 | ftp.rmdir(testDir, function (err, res) { 355 | assert(!err, err); 356 | assert.equal(res.length, 1); 357 | done(); 358 | }, true); 359 | }); 360 | it ('should recursively remove the directory ' + testDir, function (done) { 361 | ftp.mkdir(path.join(testDir, 'foo'), function(){}, true); 362 | ftp.rmdir(testDir, function (err, res) { 363 | assert(!err, err); 364 | assert.equal(res.length, 2); 365 | done(); 366 | }, true); 367 | }); 368 | it ('should recursively remove the directory in queue order: ' + testDir, function (done) { 369 | ftp.mkdir(path.join(testDir, 'foo'), function(){ 370 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'ftpimp.jpg')], function(){}, Queue.RunNext); 371 | }, true); 372 | ftp.rmdir(testDir, function (err, res) { 373 | assert(!err, err); 374 | assert.equal(res.length, 3); 375 | done(); 376 | }, true); 377 | }); 378 | it ('should recursively remove files in the directory ' + testDir, function (done) { 379 | ftp.mkdir(path.join(testDir, 'foo'), function(){ 380 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'foo.jpg')], function(){}, Queue.RunNext); 381 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'foo1.jpg')], function(){}, Queue.RunNext); 382 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'foo2.jpg')], function(){}, Queue.RunNext); 383 | }, true); 384 | 385 | ftp.rmdir(testDir, function (err, res) { 386 | assert(!err, err); 387 | assert.equal(res.length, 5); 388 | done(); 389 | }, true); 390 | }); 391 | it ('should recursively remove all empty directories', function (done) { 392 | ftp.mkdir(path.join(testDir, 'foo', 'bar', 'who'), function(){ 393 | }, true); 394 | 395 | ftp.rmdir(testDir, function (err, res) { 396 | assert(!err, err); 397 | assert.equal(res.length, 4); 398 | done(); 399 | }, true); 400 | }); 401 | it ('should recursively remove files in the directory ' + testDir, function (done) { 402 | ftp.mkdir(path.join(testDir, 'foo'), function(){ 403 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'foo', 'foo.jpg')], function(){}, Queue.RunNext); 404 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'foo', 'foo1.jpg')], function(){}, Queue.RunNext); 405 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'ftpimp.jpg')], function(){}, Queue.RunNext); 406 | ftp.put(['./test/ftpimp.jpg', path.join(testDir, 'test1.jpg')], function(){}, Queue.RunNext); 407 | }, true); 408 | 409 | ftp.rmdir(testDir, function (err, res) { 410 | assert(!err, err); 411 | assert.equal(res.length, 6); 412 | done(); 413 | }, true); 414 | }); 415 | it ('fails', function (done) { 416 | ftp.rmdir('badDirectoryError', function (err, res) { 417 | assert(err instanceof Error); 418 | assert(!res); 419 | done(); 420 | }, true); 421 | }); 422 | }); 423 | 424 | describe('General FTP commands', function () { 425 | it ('ping#NOOP: do nothing, ping the remote server', function (done) { 426 | ftp.ping(done); 427 | }); 428 | it ('stat#STAT: get server status', function (done) { 429 | ftp.stat(function (err, res) { 430 | assert(typeof res === 'string'); 431 | done(err); 432 | }); 433 | }); 434 | it ('getcwd#PWD: gets current working directory', function (done) { 435 | ftp.getcwd(function (err, res) { 436 | assert(typeof res === 'string'); 437 | done(err); 438 | }); 439 | }); 440 | it ('info#SYST: return system type', function (done) { 441 | ftp.info(function (err, res) { 442 | assert(typeof res === 'string'); 443 | done(err); 444 | }); 445 | }); 446 | }); 447 | 448 | describe('Queue RunLevel Sequencing', function () { 449 | var order = []; 450 | it('should run in the order of 1,3,2,4', function (done) { 451 | ftp.ls('foo-1', function (err, res) { 452 | order.push(1); 453 | }); 454 | ftp.ls('foo-2', function (err, res) { 455 | order.push(2); 456 | }); 457 | ftp.ls('foo-3', function (err, res) { 458 | order.push(3); 459 | }, Queue.RunNext); 460 | ftp.ls('foo-4', function (err, res) { 461 | order.push(4); 462 | assert.deepEqual(order, [1,3,2,4]); 463 | done(); 464 | }); 465 | }); 466 | }); 467 | 468 | describe('Queue sequence tests', function () { 469 | var level = 0, 470 | msg = 'should be at level '; 471 | it ('should run in waterfall', function (done) { 472 | ftp.ping(function (err, res) { 473 | level += 1; 474 | //console.log(level); 475 | assert(level, 1, msg + level); 476 | ftp.runNow(ftp.ping.raw, function (err, res) { 477 | level += 1; 478 | //console.log(level); 479 | assert(level, 2, msg + level); 480 | }); 481 | ftp.ping(function (err, res) { 482 | level += 1; 483 | assert(level, 6, msg + level); 484 | }); 485 | ftp.runNext(ftp.ping.raw, function (err, res) { 486 | level += 1; 487 | //console.log(level); 488 | assert(level, 3, msg + level); 489 | }); 490 | }); 491 | ftp.ping(function (err, res) { 492 | level += 1; 493 | //console.log(level); 494 | assert.equal(level, 4, msg + level); 495 | ftp.ping(function (err, res) { 496 | level += 1; 497 | //console.log(level); 498 | assert.equal(level, 7, msg + level); 499 | done(); 500 | }); 501 | }); 502 | ftp.ping(function (err, res) { 503 | level += 1; 504 | //console.log(level); 505 | assert.equal(level, 5, msg + level); 506 | }); 507 | }); 508 | it ('should never run the last command', function (done) { 509 | ftp.ping(function (err, res) { 510 | level = 1; 511 | assert(level, 1, msg + level); 512 | setTimeout(function () { 513 | done(); 514 | }, 250); 515 | }, Queue.RunLast, true); 516 | ftp.ping(function (err, res) { 517 | done('This should not run!'); 518 | }); 519 | }); 520 | it ('should override the holdQueue from last command and quit', function (done) { 521 | ftp.quit(done, Queue.RunNow); 522 | }); 523 | }); 524 | }); 525 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTPimp 3 | * @author Nicholas Riley 4 | */ 5 | 6 | "use strict"; 7 | require('colors'); 8 | var net = require('net'),//{{{ 9 | fs = require('fs'), 10 | path = require('path'), 11 | /** 12 | * @mixin 13 | * @see {@link https://nodejs.org/api/events.html|Node.js API: EventEmitter} 14 | */ 15 | EventEmitter = require('events').EventEmitter, 16 | dbg, 17 | StatObject, 18 | Queue, 19 | handle, 20 | ftp, 21 | cmd, 22 | /** @constructor */ 23 | CMD = require('./lib/command'), 24 | /** 25 | * The main FTP API object 26 | * @constructor 27 | * @mixes EventEmitter 28 | * @param {null|object} config - The ftp connection settings (optional) 29 | * @param {boolean} connect - Whether or not to start the connection automatically; default is true; 30 | * @todo The major functions have been added and this current version 31 | * is more stable and geared for asynchronous NodeJS. The following commands need to be added: 32 | * @todo Add FTP.stou 33 | * @todo Add FTP.rein 34 | * @todo Add FTP.site 35 | * @todo Add FTP.mode 36 | * @todo Add FTP.acct 37 | * @todo Add FTP.appe 38 | * @todo Add FTP.help 39 | * @todo Add ability to opt into an active port connection for data transfers 40 | * 41 | * FTP extends the NodeJS EventEmitter - see 42 | */ 43 | FTP = function (cfg, connect) { 44 | ftp = this; 45 | connect = connect ? true : false; 46 | if (cfg) { 47 | Object.keys(cfg).forEach(function (key) { 48 | ftp.config[key] = cfg[key]; 49 | }); 50 | if (undefined !== cfg.ascii) { 51 | ftp.ascii = ftp.ascii.concat(cfg.ascii); 52 | } 53 | if (ftp.config.debug) { 54 | debug.enable(); 55 | } else { 56 | debug.disable(); 57 | } 58 | } 59 | 60 | //set new handler 61 | cmd = ftp.cmd = CMD.create(ftp); 62 | ftp.handle = ftp.Handle.create(); 63 | if (connect) { 64 | ftp.connect(); 65 | } 66 | }, 67 | /** 68 | * A debugger for developing and issue tracking 69 | * @namespace 70 | * @memberof FTP 71 | */ 72 | debug = { 73 | /** Disable debugging */ 74 | disable: function () { 75 | dbg = debug.disabled; 76 | }, 77 | /** Holds the disabled debugger */ 78 | disabled: function () { 79 | return undefined; 80 | }, 81 | /** Enable debugging */ 82 | enable: function () { 83 | dbg = debug.enabled; 84 | }, 85 | /** Holds the enabled debugger */ 86 | enabled: function () { 87 | console.log.apply(console, arguments); 88 | } 89 | };//}}} 90 | 91 | /** 92 | * Initializes the main FTP sequence 93 | * ftp will emit a ready event once 94 | * the server connection has been established 95 | * @param {null|object} config - The ftp connection settings (optional) 96 | * @param {boolean} connect - Whether or not to start the connection automatically; default is true; 97 | * @returns {object} FTP - return new FTP instance object 98 | */ 99 | FTP.create = function (cfg, connect) { 100 | return new FTP(cfg, connect); 101 | }; 102 | 103 | 104 | /** 105 | * The command module 106 | * @type {object} 107 | * @extends module:command 108 | */ 109 | FTP.CMD = CMD; 110 | 111 | FTP.prototype = new EventEmitter(); 112 | 113 | //expose debugger everywhere 114 | FTP.debug = debug; 115 | FTP.prototype.debug = debug; 116 | 117 | 118 | /** @constructor */ 119 | FTP.prototype.Handle = function () { 120 | return undefined; 121 | }; 122 | 123 | 124 | //TODO - document totalPipes && openPipes 125 | FTP.prototype.totalPipes = 0; 126 | FTP.prototype.openPipes = 0; 127 | 128 | /** 129 | * Holds the current file transfer type [ascii, binary, ecbdic, local] 130 | * @type {string} 131 | */ 132 | FTP.prototype.currentType = 'ascii'; 133 | 134 | /** 135 | * List of files to get and put in ASCII 136 | * @type {array} 137 | */ 138 | FTP.prototype.ascii = { 139 | am: 1, 140 | asp: 1, 141 | bat: 1, 142 | c: 1, 143 | cfm: 1, 144 | cgi: 1, 145 | conf: 1, 146 | cpp: 1, 147 | css: 1, 148 | dhtml: 1, 149 | diz: 1, 150 | h: 1, 151 | hpp: 1, 152 | htm: 1, 153 | html: 1, 154 | in: 1, 155 | inc: 1, 156 | java: 1, 157 | js: 1, 158 | jsp: 1, 159 | lua: 1, 160 | m4: 1, 161 | mak: 1, 162 | md5: 1, 163 | nfo: 1, 164 | nsi: 1, 165 | pas: 1, 166 | patch: 1, 167 | php: 1, 168 | phtml: 1, 169 | pl: 1, 170 | po: 1, 171 | py: 1, 172 | qmail: 1, 173 | sh: 1, 174 | shtml: 1, 175 | sql: 1, 176 | svg: 1, 177 | tcl: 1, 178 | tpl: 1, 179 | txt: 1, 180 | vbs: 1, 181 | xhtml: 1, 182 | xml: 1, 183 | xrc: 1 184 | }; 185 | 186 | /** 187 | * Set once the ftp connection is established 188 | * @type {boolean} 189 | */ 190 | FTP.prototype.isReady = false; 191 | 192 | /** 193 | * Refernce to the socket created for data transfers 194 | * @alias FTP#pipe 195 | * @type {object} 196 | */ 197 | FTP.prototype.pipe = null; 198 | 199 | /** 200 | * Set by the ftp.abort method to tell the pipe to close any open data connection 201 | * @type {object} 202 | * @alias FTP#pipeAborted 203 | */ 204 | FTP.prototype.pipeAborted = false; 205 | 206 | /** 207 | * Set by the ftp.openDataPort method to tell the process that the pipe has been closed 208 | * @type {object} 209 | * @alias FTP#pipeClosed 210 | */ 211 | FTP.prototype.pipeClosed = false; 212 | 213 | /** 214 | * Set by the ftp.put method while the pipe is connecting and while connected 215 | * @type {object} 216 | * @alias FTP#pipeActive 217 | */ 218 | FTP.prototype.pipeActive = false; 219 | 220 | /** 221 | * Refernce to the socket created for data transfers 222 | * @type {object} 223 | * @alias FTP#socket 224 | */ 225 | FTP.prototype.socket = null; 226 | 227 | /** 228 | * The FTP log in information. 229 | * @type {string} 230 | * @alias FTP#config 231 | */ 232 | FTP.prototype.config = { 233 | host: 'localhost', 234 | port: 21, 235 | user: 'root', 236 | pass: '', 237 | debug: false 238 | }; 239 | 240 | /** 241 | * Current working directory. 242 | * @type {string} 243 | * @alias FTP#cwd 244 | */ 245 | FTP.prototype.cwd = ''; 246 | 247 | /** 248 | * The user defined directory set by the FTP admin. 249 | * @type {string} 250 | * @alias FTP#baseDir 251 | */ 252 | FTP.prototype.baseDir = ''; 253 | 254 | /** 255 | * Creates and returns a new FTP connection handler 256 | * @returns {object} The new Handle instance 257 | */ 258 | FTP.prototype.Handle.create = function () { 259 | return new FTP.prototype.Handle(); 260 | }; 261 | 262 | handle = FTP.prototype.Handle.prototype; 263 | 264 | /** 265 | * Ran at beginning to start a connection, can be overriden 266 | * @example 267 | * //Overriding the ftpimp.init instance method 268 | * var FTP = require('ftpimp'), 269 | * //get connection settings 270 | * config = { 271 | * host: 'localhost', 272 | * port: 21, 273 | * user: 'root', 274 | * pass: '' 275 | * }, 276 | * ftp, 277 | * //override init 278 | * MyFTP = function(){ 279 | * this.init(); 280 | * }; 281 | * //override the prototype 282 | * MyFTP.prototype = FTP.prototype; 283 | * //override the init method 284 | * MyFTP.prototype.init = function () { 285 | * dbg('Initializing!'); 286 | * ftp.handle = ftp.Handle.create(); 287 | * ftp.connect(); 288 | * }; 289 | * //start new MyFTP instance 290 | * ftp = new MyFTP(config); 291 | */ 292 | FTP.prototype.init = function () {//{{{ 293 | //create a new socket and login 294 | ftp.connect(); 295 | };//}}} 296 | 297 | 298 | var ExeQueue = function (command, callback, runLevel, holdQueue) { 299 | var that = this, 300 | n, 301 | method = command.split(' ', 1)[0], 302 | bind = function (name) { 303 | that[name.slice(1)] = function () { 304 | if (name === '_responseHandler') { 305 | 306 | } 307 | dbg('calling : ' + name + ' > ' + command, arguments); 308 | that[name].apply(that, arguments); 309 | }; 310 | }; 311 | that.command = command; 312 | that.method = method; 313 | that.pipeData = []; 314 | that.pipeDataSize = 0; 315 | that.holdQueue = holdQueue; 316 | that.callback = callback; 317 | that.runLevel = runLevel; 318 | that.ended = false; 319 | that.ping = null; 320 | handle.data.waiting = true; 321 | 322 | for (n in ExeQueue.prototype) { 323 | if (ExeQueue.prototype.hasOwnProperty(n) && n.charAt(0) === '_' && ExeQueue.prototype.hasOwnProperty(n)) { 324 | //remove underscore and provide hook 325 | bind(n); 326 | } 327 | } 328 | ftp.once('response', that.responseHandler); 329 | that.started = Date.now(); 330 | ftp.socket.write(command + '\r\n', function () { 331 | dbg(('Run> command sent: ' + command).yellow); 332 | }); 333 | }; 334 | 335 | 336 | ExeQueue.create = function (command, callback, runLevel, holdQueue) { 337 | return new ExeQueue(command, callback, runLevel, holdQueue); 338 | }; 339 | 340 | 341 | //end the queue 342 | ExeQueue.prototype._end = function () { 343 | var that = this; 344 | that.checkProc(); 345 | }; 346 | 347 | //end the queue 348 | ExeQueue.prototype._endStopwatch = function () { 349 | var that = this; 350 | that.ended = Date.now(); 351 | that.ping = that.ended - that.started; 352 | }; 353 | 354 | ExeQueue.prototype.queueHolding = false; 355 | ExeQueue.prototype._checkProc = function () { 356 | var that = this; 357 | dbg('check process for end: ', that); 358 | if (that.holdQueue) { 359 | dbg(('ExeQueue> Ending process, holding queue: ' + that.command).yellow); 360 | } else { 361 | dbg(('ExeQueue> Ending process: ' + that.command).yellow); 362 | ftp.emit('endproc'); 363 | } 364 | }; 365 | 366 | 367 | ExeQueue.prototype._closeTransfer = function () { 368 | dbg('ExeQueue> closing transfer and ending Proc'.magenta); 369 | var exeQueue = this; 370 | exeQueue.closePipe(); 371 | //exeQueue.checkProc(); 372 | }; 373 | 374 | ExeQueue.prototype._closePipe = function () { 375 | dbg('ExeQueue> closing transfer pipe'.magenta); 376 | let exeQueue = this; 377 | let data = exeQueue.pipeData; 378 | let bufferSize = exeQueue.pipeDataSize; 379 | try { 380 | ftp.pipe.removeListener('data', exeQueue.receiveData); 381 | ftp.pipe.removeListener('end', exeQueue.closePipe); 382 | //check for buffers 383 | if (data.length && Array.isArray(data)) { 384 | data = Buffer.concat(data, bufferSize); 385 | } 386 | } catch (dataNotBoundError) { 387 | dbg('data not bound: ', dataNotBoundError); 388 | } 389 | dbg('ExeQueue> total size(' + (data ? data.length : 0) + ')'); 390 | exeQueue.callback(null, data); 391 | exeQueue.checkProc(); 392 | }; 393 | 394 | 395 | ExeQueue.prototype._responseHandler = function (code, data) { 396 | dbg(('Response handler: ' + code).cyan, data); 397 | var exeQueue = this; 398 | exeQueue.endStopwatch(); 399 | //dbg('pipe is ' + (ftp.pipeClosed ? 'closed' : 'open')); 400 | if (code >= 500 && code < 600) { 401 | dbg('handling error response code...'); 402 | dbg(exeQueue); 403 | //if we have an open pipe, wait for it to end 404 | //if (ftp.pipeClosed) { 405 | //end immediately 406 | try { 407 | dbg('killing pipe'); 408 | ftp.pipe.removeListener('data', exeQueue.receiveData); 409 | ftp.pipe.removeListener('end', exeQueue.closePipe); 410 | ftp.pipe.destroy(); 411 | dbg('---pipe down---'.red); 412 | } catch (dataNotBoundError) { 413 | dbg('data not bound: ', dataNotBoundError); 414 | } 415 | exeQueue.callback(new Error(data), null); 416 | exeQueue.checkProc(); 417 | } else if (code === 150 || code === 125) { 418 | if (exeQueue.method === 'STOR') { 419 | ftp.once('dataTransferComplete', exeQueue.closeTransfer); 420 | } else { 421 | dbg('listening for pipe data'.red); 422 | if (ftp.pipeClosed) { 423 | dbg('pipe already closed'.yellow); 424 | ftp.pipeClosed = false; 425 | exeQueue.closePipe(); 426 | return; 427 | } 428 | ftp.pipe.on('end', exeQueue.closePipe); 429 | ftp.pipe.on('data', exeQueue.receiveData); 430 | } 431 | } else { 432 | exeQueue.callback(null, data); 433 | if (code !== 227) { 434 | exeQueue.checkProc(); 435 | } 436 | } 437 | }; 438 | 439 | 440 | ExeQueue.prototype._receiveData = function (data) { 441 | let c = this; 442 | c.pipeDataSize += data.length; 443 | c.pipeData.push(data); 444 | }; 445 | 446 | 447 | /** 448 | * Run a raw ftp command and issue callback on success/error. 449 | * Same as {@link FTP#run} except this command will be 450 | * will be prioritized to be the next to run in the queue. 451 | * - calls made with this provide a sequential queue 452 | * 453 | * @param {string} command - The command that will be issued ie: "CWD foo" 454 | * @param {function} callback - The callback function to be issued on success/error 455 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 456 | */ 457 | FTP.prototype.runNext = function (command, callback, holdQueue) { 458 | ftp.run(command, callback, Queue.RunNext, holdQueue); 459 | }; 460 | 461 | /** 462 | * Run a raw ftp command and issue callback on success/error. 463 | * Same as {@link FTP#run} except this command will be ran immediately (in parallel) 464 | * and will overrun any current queue action in place. 465 | * 466 | * @param {string} command - The command that will be issued ie: "CWD foo" 467 | * @param {function} callback - The callback function to be issued on success/error 468 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 469 | */ 470 | 471 | FTP.prototype.runNow = function (command, callback, holdQueue) { 472 | ftp.run(command, callback, Queue.RunNow, holdQueue); 473 | }; 474 | 475 | /** 476 | * Run a raw ftp command and issue callback on success/error. 477 | *
478 | * Functions created with this provide a sequential queue 479 | * that is asynchronous, so items will be processed 480 | * in the order they are received, but this will happen 481 | * immediately. Meaning, if you make a dozen sequential calls 482 | * of "ftp.run('MDTM', callback);" they will all be read immediately, 483 | * queued in order, and then processed one after the other. Unless 484 | * you set the optional parameter runLevel to true 485 | * 486 | * @param {string} command - The command that will be issued ie: "CWD foo" 487 | * @param {function} callback - The callback function to be issued on success/error 488 | * @param {number} [runLevel=0] - TL;DR see {@link Queue.RunLevels} 489 | * FTP#run will invoke a queueing process, callbacks 490 | * will be stacked to maintain synchronicity. How they stack will depend on the value 491 | * you set for the runLevel 492 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 493 | */ 494 | FTP.prototype.run = function (command, callback, runLevel, holdQueue) {//{{{ 495 | runLevel = runLevel === undefined ? false : runLevel; 496 | holdQueue = holdQueue === undefined ? false : holdQueue; 497 | 498 | var callbackConstruct = function () { 499 | dbg('Run> running callbackConstruct'.yellow + ' ' + command); 500 | //if (command === 'QUIT') {...} 501 | dbg(command, runLevel, holdQueue); 502 | ExeQueue.create(command, callback, runLevel, holdQueue); 503 | }; 504 | 505 | if (undefined === command) { //TODO || cmd.allowed.indexOf(command.toLowerCase) { 506 | throw new Error('ftp.run > parameter 1 expected command{string}'); 507 | } else if (undefined === callback || typeof callback !== 'function') { 508 | throw new Error('ftp.run > parameter 2 expected a callback function'); 509 | } 510 | dbg('ftp.Run: ' + [, holdQueue, command].join(' ').cyan); 511 | ftp.queue.register(callbackConstruct, runLevel); 512 | };//}}} 513 | 514 | /** 515 | * Establishes a queue to provide synchronicity to ftp 516 | * processes that would otherwise fail from concurrency. 517 | * This function is done automatically when using 518 | * the {@link FTP#run} method to queue commands. 519 | * @fires FTP#queueEmpty 520 | * @member {object} FTP#queue 521 | * @property {array} queue._queue - Stores registered procedures 522 | * and holds them until called by the queue.run method 523 | * @property {boolean} queue.processing - Returns true if there 524 | * are items running in the queue 525 | * @property {function} queue.register - Registers a new callback 526 | * function to be triggered after the queued command completes 527 | * @property {function} queue.run - If there is something in the 528 | * queue and queue.processing is false, than the first item 529 | * stored in queue._queue will be removed from the queue 530 | * and processed. 531 | */ 532 | FTP.prototype.queue = {//{{{ 533 | _queue: [], 534 | processing: false, 535 | reset: function () { 536 | //...resets the queue 537 | ftp.queue._queue = []; 538 | }, 539 | register: function (callback, runLevel) { 540 | dbg('Queue> Registering callback...'); 541 | dbg(('Queue> processing: ' + ftp.queue.processing + '; size: ' + ftp.queue._queue.length).cyan); 542 | runLevel = runLevel === undefined ? false : runLevel; 543 | if (runLevel) { 544 | //run next 545 | if (runLevel === Queue.RunNext) { 546 | ftp.queue._queue.unshift(callback); 547 | } else { 548 | //run now 549 | callback(); 550 | return; 551 | } 552 | } else { 553 | ftp.queue._queue.push(callback); 554 | } 555 | 556 | if (!ftp.queue.processing) { 557 | ftp.queue.run(); 558 | //ftp.emit('endproc'); 559 | } 560 | }, 561 | run: function () { 562 | dbg('Queue> Loading queue'.yellow); 563 | if (ftp.queue._queue.length > 0) { 564 | ftp.queue.processing = true; 565 | dbg('Queue> Loaded...running'); 566 | ftp.queue.currentProc = ftp.queue._queue.shift(); 567 | if (!ftp.error) { 568 | ftp.queue.currentProc.call(ftp.queue.currentProc); 569 | } 570 | } else { 571 | /** 572 | * Fired when the primary queue is empty 573 | * @event FTP#queueEmpty 574 | */ 575 | ftp.emit('queueEmpty'); 576 | ftp.queue.processing = false; 577 | dbg('--queue empty--'.yellow); 578 | } 579 | } 580 | }; 581 | 582 | 583 | FTP.prototype.on('endproc', function () { 584 | dbg('Event> endproc'.magenta); 585 | }); 586 | 587 | /** @todo - this needs to be defined */ 588 | FTP.prototype.on('endproc', FTP.prototype.queue.run);//}}} 589 | 590 | 591 | /** 592 | * Provides a factory to create a simple queue procedure. Look 593 | * at the example below to see how we override the callback 594 | * function to perform additional actions before exiting 595 | * the queue and loading the next one.
596 | * Functions created with this provide a synchronized queue 597 | * that is asynchronous in itself, so items will be processed 598 | * in the order they are received, but this will happen 599 | * immediately. Meaning, if you make a dozen sequential calls 600 | * to {@link FTP#filemtime} they will all be read immediately, 601 | * queued in order, and then processed one after the other. 602 | * @constructor 603 | * @memberof FTP 604 | * @see {@link Queue} 605 | * @param {string} command - The command that will be issued ie: "CWD foo" 606 | * @returns {function} queueManager - The simple queue manager 607 | * @TODO - documentation needs to be updated rewrite 608 | */ 609 | var Queue = function (command) {//{{{ 610 | 611 | var queue = this; 612 | queue.command = command; 613 | 614 | var builder = queue.builder(); 615 | builder.raw = command; 616 | 617 | return builder; 618 | };//}}} 619 | 620 | 621 | 622 | /** 623 | * The queue manager returned when creating a new {@link Queue} object 624 | * @memberof Queue 625 | * @inner 626 | * @param {string} filepath - The location of the remote file to process the set command. 627 | * @param {function} callback - The callback function to be issued. 628 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. Careful, concurrent connections 629 | * will likely end in a socket error. This is meant for fine grained control over certain 630 | * scenarios wherein the process is part of a running queue and you need to perform an ftp 631 | * action prior to the {@link FTP#endproc} event firing and execing the next queue. 632 | */ 633 | Queue.prototype.builder = function () { 634 | var queue = this, 635 | command = queue.command; 636 | return function (filepath, callback, runLevel, holdQueue) { 637 | var hook = (undefined === queue[command + 'Hook']) ? null : queue[command + 'Hook'], 638 | portHandler = function () { 639 | dbg('Queue.builder: checking hook -> ' + typeof hook); 640 | //hook data into custom instance function 641 | ftp.runNow(command + ' ' + filepath, function (err, data) { 642 | if (typeof hook === 'function') { 643 | data = hook(data); 644 | } 645 | callback(err, data); 646 | if (!holdQueue) { 647 | ftp.emit('endproc'); 648 | } 649 | }, true); 650 | }; 651 | dbg(['Queue.builder::', command, filepath, '> setting '].join(' ').cyan); 652 | dbg(runLevel, holdQueue); 653 | //TODO add list of commands that don't need to change type, or should be a certain type 654 | //ie ls:LIST 655 | ftp.setType(filepath, function () { 656 | dbg('type set'); 657 | ftp.openDataPort(portHandler, Queue.RunNow, true); 658 | }, runLevel, true); 659 | }; 660 | }; 661 | 662 | 663 | 664 | /** 665 | * Static Queue value passed in the runLevel param of methods to control the priority of those methods. 666 | *
667 | * - default RunLevel {@link FTP.Queue.RunLast}
668 | * - Most methods use the {@link FTP#run} call.
669 | * - Every {@link FTP#run} call issued is stacked in a series queue by default. To change to a waterfall 670 | * or run in parallel.

671 | * 672 | * RunLevels are a design of FTPimp to control process flow only.
673 | * When implementing parallel actions, parallel calls should only be issued inside the callback 674 | * of a parent waterfall or series queue. Otherwise, the FTP service itself likely will break 675 | * from the concurrent connection attempts. 676 | * @class 677 | * @readonly 678 | * @enum {number} 679 | * @see {@link FTP#mkdir}, {@link FTP#rmdir}, {@link FTP#put} for examples 680 | * @see {@link FTP#run} for series, {@link FTP#runNext} for waterfall, {@link FTP#runNow} for parallel 681 | * @example 682 | * //series 683 | * ftp.ping(function () { //runs first 684 | * ftp.ping(function () { //runs third 685 | * }); 686 | * }); 687 | * ftp.ping(function () { //runs second 688 | * }); 689 | * 690 | * //waterfall 691 | * var runNext = FTP.Queue.RunNext; 692 | * ftp.ping(function () { //runs first 693 | * //add runNow to the call 694 | * ftp.ping(function () { //runs second 695 | * }, runNow); 696 | * }); 697 | * ftp.ping(function () { //runs third 698 | * }); 699 | * 700 | * //parallel 701 | * var runNow = FTP.Queue.RunNow; 702 | * ftp.put('foo', function () { //runs first 703 | * }); 704 | * ftp.put('foo', function () { //runs second 705 | * }, runNow); 706 | */ 707 | Queue.RunLevels = { 708 | /** {@link FTP.Queue.RunLast} will push the command to the end of the queue; */ 709 | last: 0, 710 | /** {@link FTP.Queue.RunNow} will run the command immediately; will overrun a current processing queue */ 711 | now: 1, 712 | /** {@link FTP.Queue.RunNext} will run after current queue completes */ 713 | next: 2 714 | }; 715 | 716 | 717 | /** 718 | * @readonly 719 | * @property {number} RunNext - value needed for runLevel parameter to run commands immediately after the current queue; 720 | * @see {@link FTP.Queue.RunLevels.next} 721 | * @see {@link FTP#run} 722 | */ 723 | Queue.RunNext = Queue.RunLevels.next; 724 | 725 | /** 726 | * @readonly 727 | * @property {number} RunNow - value needed for runLevel parameter to run commands immediately, overrunning any current queue process; 728 | * @see {@link FTP.Queue.RunLevels.now} 729 | * @see {@link FTP#run} 730 | */ 731 | Queue.RunNow = Queue.RunLevels.now; 732 | 733 | /** 734 | * @readonly 735 | * @property {number} RunLast - value needed for runLevel parameter, will add command to the end of the queue; default Queue.RunLevel; 736 | * @see {@link FTP.Queue.RunLevels.last} 737 | * @see {@link FTP#run} 738 | */ 739 | Queue.RunLast = Queue.RunLevels.last; 740 | 741 | /** 742 | * Create a new {@link Queue} instance for the command type. 743 | * @param {string} command - The command that will be issued, no parameters, ie: "CWD" 744 | */ 745 | Queue.create = function (command) {//{{{ 746 | return new Queue(command); 747 | };//}}} 748 | 749 | /** 750 | * Register a data hook function to intercept received data 751 | * on the command (parameter 1) 752 | * @param {string} command - The command that will be issued, no parameters, ie: "CWD" 753 | * @param {function} callback - The callback function to be issued. 754 | */ 755 | Queue.registerHook = function (command, callback) {//{{{ 756 | if (undefined !== Queue.prototype[command + 'Hook']) { 757 | throw new Error('Handle.Queue already has hook registered: ' + command + 'Hook'); 758 | } 759 | Queue.prototype[command + 'Hook'] = callback; 760 | };//}}} 761 | 762 | 763 | /** 764 | * Called once the socket has established 765 | * a connection to the host 766 | */ 767 | let failedAttempts = []; 768 | const failedTimeThreshold = 10 * 1000; 769 | const maxFailedAttempts = 3; 770 | handle.connected = function () {//{{{ 771 | dbg('socket connected!'); 772 | if (!ftp.socket.remoteAddress) { 773 | let now = Date.now(); 774 | failedAttempts = failedAttempts.filter((time) => { 775 | return time > (now - failedTimeThreshold); 776 | }); 777 | if (failedAttempts.length > maxFailedAttempts) { 778 | throw new Error('Max failed attempts reached trying to reconnect to FTP server'); 779 | } 780 | failedAttempts.push(now); 781 | setTimeout(ftp.connect, 1000); 782 | return; 783 | } 784 | process.once('exit', ftp.exit); 785 | process.once('SIGINT', ftp.exit); 786 | ftp.config.pasvAddress = ftp.socket.remoteAddress.split('.').join(','); 787 | ftp.socket.on('data', ftp.handle.data); 788 | //process.once('uncaughtException', handle.uncaughtException); 789 | };//}}} 790 | 791 | 792 | /** 793 | * Called anytime an uncaughtException error is thrown 794 | */ 795 | handle.uncaughtException = function (err) {//{{{ 796 | dbg(('!' + err.toString()).red); 797 | ftp.exit(); 798 | };//}}} 799 | 800 | 801 | /** 802 | * Simple way to parse incoming data, and determine 803 | * if we should run any commands from it. Commands 804 | * are found in the lib/command.js file 805 | */ 806 | handle.data = function (data) {//{{{ 807 | dbg('....................'); 808 | var strData = data.toString().trim(), 809 | strParts, 810 | commandCodes = [], 811 | commandData = {}, 812 | cmdName, 813 | code, 814 | i, 815 | end = function () { 816 | dbg('handle.data.waiting: ' + handle.data.waiting, code); 817 | if (handle.data.waiting) { 818 | dbg('handle.data.waiting:: ' + code + ' ' + strData); 819 | if (!handle.data.start) { 820 | handle.data.waiting = false; 821 | /*if (code === 150) { 822 | dbg('holding for data transfer'.yellow); 823 | } else {*/ 824 | ftp.emit('response', code, strData); 825 | //} 826 | } else { 827 | handle.data.waiting = true; 828 | handle.data.start = false; 829 | } 830 | } else if (code === 553) { 831 | 832 | } 833 | }, 834 | run = function () { 835 | if (undefined !== cmd.keys[code]) { 836 | if (code === 227) { 837 | handle.data.waiting = true; 838 | ftp.once('commandComplete', end); 839 | } 840 | cmdName = cmd.keys[code]; 841 | dbg('>executing command: ' + cmdName); 842 | cmd[cmdName](strData); 843 | //only open once per ftp instance 844 | } 845 | //we will handle data transfer codes with the openDataPort 846 | if (code !== 227 && code !== 226) { 847 | end(); 848 | } 849 | }; 850 | 851 | dbg(('\n>>>\n' + strData + '\n>>>\n').grey); 852 | strData = strData.split(/[\r|\n]/).filter(Boolean); 853 | strParts = strData.length; 854 | 855 | for (i = 0; i < strParts; i++) { 856 | code = strData[i].substr(0, 3); 857 | //make sure its a number and not yet stored 858 | if (code.search(/^[0-9]{3}/) > -1) { 859 | if (commandCodes.indexOf(code) < 0) { 860 | commandCodes.push(code); 861 | commandData[code] = ''; 862 | } 863 | commandData[code] += strData[i].substr(3); 864 | } 865 | } 866 | dbg(commandCodes.join(', ').grey); 867 | for (i = 0; i < commandCodes.length; i++) { 868 | code = Number(commandCodes[i]); 869 | strData = commandData[code].trim(); 870 | dbg('------------------'); 871 | dbg('CODE -> ' + code); 872 | dbg('DATA -> ' + strData); 873 | dbg('------------------'); 874 | run(); 875 | } 876 | };//}}} 877 | 878 | 879 | /** 880 | * Waiting for response from server 881 | * @property FTP#Handle#data.waiting 882 | */ 883 | handle.data.waiting = true; 884 | handle.data.start = true; 885 | 886 | 887 | /** 888 | * Logout from the ftp server 889 | * @param {number} sig - the signal code, if not 0, then socket will 890 | * be destroyed to force closing 891 | */ 892 | FTP.prototype.exit = function (sig) {//{{{ 893 | if (undefined !== sig && sig === 0) { 894 | ftp.socket.end(); 895 | } else { 896 | //ftp.pipe.close(); 897 | ftp.socket.destroy(); 898 | if (ftp.pipeActive) { 899 | ftp.pipeAborted = false; 900 | ftp.pipeActive = false; 901 | } 902 | } 903 | };//}}} 904 | 905 | 906 | /** 907 | * Creates a new socket connection for sending commands 908 | * to the ftp server and runs an optional callback when logged in 909 | * @param {function} callback - The callback function to be issued. (optional) 910 | */ 911 | FTP.prototype.connect = function (callback) {//{{{ 912 | /** 913 | * Holds the connected socket object 914 | * @member FTP#socket 915 | */ 916 | ftp.socket = net.createConnection(ftp.config.port, ftp.config.host); 917 | ftp.socket.on('connect', handle.connected); 918 | if (typeof callback === 'function') { 919 | ftp.once('ready', callback); 920 | } 921 | dbg('connected: ' + ftp.config.host + ':' + ftp.config.port); 922 | ftp.socket.on('close', function () { 923 | dbg('**********socket CLOSED**************'); 924 | }); 925 | ftp.socket.on('end', function () { 926 | dbg('**********socket END**************'); 927 | }); 928 | };//}}} 929 | 930 | 931 | /** 932 | * Opens a new data port to the remote server - pasv connection 933 | * which allows for file transfers 934 | * @param {function} callback - The callback function to be issued 935 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 936 | * @TODO Add in useActive parameter to choose how to handle data transfers 937 | */ 938 | FTP.prototype.openDataPort = function (callback, runLevel, holdQueue) {//{{{ 939 | holdQueue = !!holdQueue; 940 | dbg('holdQ: ', holdQueue); 941 | var dataHandler = function (err, data) { 942 | if (err) { 943 | dbg('error opening data port with PASV'); 944 | dbg(err); 945 | return; 946 | } 947 | dbg('opening data port...'.cyan); 948 | dbg(ftp.socket.remoteAddress); 949 | dbg(ftp.config.pasvPort); 950 | //ftp.on('dataPortReady', callback); 951 | ftp.pipe = net.createConnection( 952 | ftp.config.pasvPort, 953 | ftp.socket.remoteAddress 954 | ); 955 | //trigger callback once the server has confirmed the port is open 956 | ftp.pipe.once('connect', function () { 957 | dbg('passive connection established ... running callback'.green); 958 | //dbg(callback.toString()); 959 | callback.call(ftp); 960 | }); 961 | ftp.pipe.once('end', function () { 962 | dbg('----> pipe end ----'); 963 | ftp.pipeClosed = true; 964 | ftp.openPipes -= 1; 965 | ftp.emit('dataPortClosed'); 966 | }); 967 | /*if (ftp.config.debug) { 968 | ftp.pipe.on('data', function (data) { 969 | dbg(data.toString().green); 970 | }); 971 | }*/ 972 | /* 973 | ftp.pipe.once('error', function (err) { 974 | dbg(('pipe error: ' + err.errno).red); 975 | dbg(ftp.openPipes); 976 | });*/ 977 | }; 978 | ftp.pasv(dataHandler, runLevel, holdQueue); 979 | };//}}} 980 | 981 | 982 | /** 983 | * Asynchronously queues files for transfer, and transfers them in order to the server. 984 | * @function 985 | * @param {string|array} paths - The path to read and send the file, 986 | * if you are sending to the same (relative) location you are reading from then 987 | * you can supply a string as a shortcut. Otherwise, use an array [from, to] 988 | * @param {function} callback - The callback function to be issued once the file 989 | * has been successfully written to the remote 990 | * @TODO - rewrite needed, can be simplified at this point 991 | */ 992 | FTP.prototype.put = (function () {//{{{ 993 | var running = false, 994 | //TODO - test this further 995 | runQueue; 996 | runQueue = function (curQueue) { 997 | dbg('FTP::put> running the pipe queue'.green, running); 998 | var callback, 999 | data, 1000 | dataTransfer, 1001 | checkAborted = function () { 1002 | if (ftp.pipeAborted) { 1003 | dbg('ftp pipe aborted!---'.yellow); 1004 | ftp.pipeActive = false; 1005 | ftp.pipeAborted = false; 1006 | running = false; 1007 | ftp.emit('pipeAborted'); 1008 | runQueue(true); 1009 | return true; 1010 | } 1011 | return false; 1012 | }; 1013 | 1014 | if (running) { 1015 | throw new Error('Put> already running'.yellow); 1016 | } 1017 | ftp.pipeActive = running = true; 1018 | 1019 | //if the local path wasnt found 1020 | if (!curQueue.path) { 1021 | dbg('Put> error'); 1022 | running = false; 1023 | callback = curQueue.callback; 1024 | data = curQueue.data; 1025 | callback.call(callback, data, null); 1026 | /*if(!checkAborted()) { 1027 | runQueue(true); 1028 | }*/ 1029 | ftp.emit('endproc'); 1030 | return; 1031 | } 1032 | dataTransfer = function (runLevel) { 1033 | var callback = curQueue.callback, 1034 | remotePath = curQueue.path; 1035 | 1036 | if (checkAborted()) { 1037 | dbg('Put was aborted'); 1038 | return; 1039 | } 1040 | ftp.runNow('STOR ' + remotePath, function (err, data) { 1041 | ftp.pipeActive = running = false; 1042 | if (curQueue.err) { 1043 | dbg('STOR: error occured'); 1044 | callback(curQueue.err, null); 1045 | } else { 1046 | dbg('STOR: file saved ' + remotePath); 1047 | callback(null, remotePath); 1048 | } 1049 | dbg('file put successful', curQueue.data); 1050 | if (!curQueue.holdQueue) { 1051 | ftp.emit('endproc'); 1052 | } 1053 | }, true); 1054 | //write file data to remote data socket 1055 | curQueue.stream = fs.createReadStream(curQueue.data); 1056 | curQueue.stream.pipe(ftp.pipe); 1057 | }; 1058 | //make sure pipe wasn't aborted 1059 | ftp.once('pipeAborted', checkAborted); 1060 | ftp.once('transferError', function (err) { 1061 | curQueue.err = err; 1062 | ftp.removeListener('pipeAborted', checkAborted); 1063 | }); 1064 | 1065 | ftp.setType(curQueue.path, function () { 1066 | ftp.openDataPort(dataTransfer, Queue.RunNow, true); 1067 | }, Queue.RunNow, true); 1068 | }; 1069 | 1070 | return function (paths, callback, runLevel, holdQueue) { 1071 | //todo add unique id to string 1072 | var isString = typeof paths === 'string', 1073 | localPath, 1074 | remotePath, 1075 | pipeFile, 1076 | queue; 1077 | 1078 | if (!isString && !(paths instanceof Array)) { 1079 | throw new Error('ftp.put > parameter 1 expected an array or string'); 1080 | } else if (paths.length === 0) { 1081 | throw new Error('ftp.put > parameter 1 expected recvd empty array'); 1082 | } 1083 | 1084 | if (isString || paths.length === 1) { 1085 | localPath = remotePath = isString ? paths : paths[0]; 1086 | } else { 1087 | localPath = paths[0]; 1088 | remotePath = paths[1]; 1089 | } 1090 | //create an index so we know the order... 1091 | //the files may be read at different times 1092 | //into the pipeFile callback 1093 | dbg('>queueing file for put process: "' + localPath + '" to "' + remotePath + '"'); 1094 | pipeFile = function (err, stat) { 1095 | dbg(('>piping file: ' + localPath).green); 1096 | if (!err && stat.isDirectory()) { 1097 | err = new Error('Cannot put directory @', localPath); 1098 | } else if (err) { 1099 | dbg('>file read error', err); 1100 | queue = { 1101 | callback: callback, 1102 | data: err, 1103 | path: false, 1104 | runLevel: runLevel, 1105 | holdQueue: holdQueue 1106 | }; 1107 | } else { 1108 | dbg('>queueing file: "' + localPath + '" to "' + remotePath + '"'); 1109 | queue = { 1110 | callback: callback, 1111 | data: localPath, 1112 | path: remotePath, 1113 | runLevel: runLevel, 1114 | holdQueue: holdQueue 1115 | }; 1116 | } 1117 | runQueue(queue); 1118 | }; 1119 | //enqueue a call; preprend call to the ftp queue of commands 1120 | //so we don't break order of operations 1121 | ftp.queue.register(function () { 1122 | fs.stat(localPath, pipeFile); 1123 | }, runLevel); 1124 | }; 1125 | }());//}}} 1126 | 1127 | 1128 | /** 1129 | * Issues a single raw request to the server and 1130 | * calls the callback function once data is received 1131 | * @param {string} command - The command to send to the FTP server 1132 | * @example 1133 | * //say hi to the server 1134 | * var FTP = require('ftpimp'), 1135 | * config = require('./myconfig'), 1136 | * ftp = FTP.connect(config); 1137 | * 1138 | * ftp.on('ready', function () { 1139 | * ftp.raw('NOOP', function (data) { 1140 | * dbg(data); 1141 | * }); 1142 | * }); 1143 | * @param {function} callback - The callback function 1144 | * to be fired once on a data event 1145 | */ 1146 | FTP.prototype.raw = function (command, callback) {//{{{ 1147 | var parser = function (data) { 1148 | callback.call(callback, data.toString()); 1149 | }; 1150 | ftp.socket.once('data', parser); 1151 | ftp.socket.write(command + '\r\n'); 1152 | };//}}} 1153 | 1154 | 1155 | /** 1156 | * Changes the current directory to the root / restricted directory 1157 | * @param {function} callback - The callback function to be issued. 1158 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1159 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1160 | */ 1161 | FTP.prototype.root = function (callback, runLevel, holdQueue) {//{{{ 1162 | var dir = ftp.baseDir; 1163 | if (dir === '/') { 1164 | dir = ''; 1165 | } 1166 | ftp.chdir(ftp.baseDir, callback); 1167 | };//}}} 1168 | 1169 | 1170 | /** 1171 | * Runs the FTP command MKD - Make a remote directory. 1172 | * Creates a directory and returns the directory name. 1173 | * Optionally creates directories recursively. 1174 | * @param {string} dirpath - The directory name to be created. 1175 | * @param {function} callback - The callback function to be issued. 1176 | * @param {boolean} recursive - Recursively create directories. (default: false) 1177 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1178 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1179 | */ 1180 | FTP.prototype.mkdir = function (dirpath, callback, recursive, runLevel, holdQueue) {//{{{ 1181 | //TODO add in error handling for parameters 1182 | dbg('building mkdir request for: ' + dirpath); 1183 | recursive = undefined === recursive ? false : recursive; 1184 | var created = [], 1185 | mkdirHandler = function (err, data) { 1186 | if (!err) { 1187 | data = data.match(/"(.*)"/)[1]; 1188 | } 1189 | if (typeof callback === 'function') { 1190 | callback.call(callback, err, data); 1191 | } 1192 | }, 1193 | isRoot, 1194 | paths, 1195 | pathsLength, 1196 | cur, 1197 | i, 1198 | makeNext, 1199 | continueMake, 1200 | addPaths, 1201 | endRecursion; 1202 | 1203 | //hijack mkdirHandler 1204 | if (recursive) { 1205 | //check if path provided starts with root / 1206 | isRoot = (dirpath.charAt(0) === path.sep); 1207 | paths = dirpath.split(path.sep).filter(Boolean); 1208 | pathsLength = paths.length; 1209 | cur = ''; 1210 | created = []; 1211 | i = 0; 1212 | makeNext = function () { 1213 | var index = i; 1214 | i += 1; 1215 | cur += paths[index] + path.sep; 1216 | dbg('making directory: ' + cur); 1217 | if (index === pathsLength - 1) { 1218 | dbg('Queue> ending recursion'.red); 1219 | //release the holdQueue 1220 | ftp.run(FTP.prototype.mkdir.raw + ' ' + (isRoot ? path.sep : '') + cur, endRecursion, index !== 0, holdQueue); 1221 | } else { 1222 | //runLevel if not first item, first item must be used to start the queue 1223 | ftp.run(FTP.prototype.mkdir.raw + ' ' + (isRoot ? path.sep : '') + cur, continueMake, index !== 0, true); 1224 | } 1225 | }; 1226 | continueMake = function (err, data) { 1227 | addPaths(err, data); 1228 | makeNext(); 1229 | }; 1230 | addPaths = function (err, data) { 1231 | if (!err) { 1232 | dbg(('adding path:' + data).blue); 1233 | data = data.match(/"(.*)"/)[1]; 1234 | created.push(data); 1235 | } 1236 | }; 1237 | endRecursion = function (err, data) { 1238 | dbg('Queue> running endRecursion', err, data); 1239 | addPaths(err, data); 1240 | if (typeof callback === 'function') { 1241 | callback(err, created); 1242 | } 1243 | }; 1244 | makeNext(); 1245 | } else { 1246 | ftp.run(FTP.prototype.mkdir.raw + ' ' + dirpath, mkdirHandler, runLevel, holdQueue); 1247 | } 1248 | };//}}} 1249 | FTP.prototype.mkdir.raw = 'MKD'; 1250 | 1251 | 1252 | /** 1253 | * Runs the FTP command RMD - Remove a remote directory 1254 | * @param {string} dirpath - The location of the directory to be deleted. 1255 | * @param {function} callback - The callback function to be issued. 1256 | * @param {string} recursive - Recursively delete files and subfolders. 1257 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1258 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1259 | */ 1260 | FTP.prototype.rmdir = function (dirpath, callback, recursive, runLevel, holdQueue) {//{{{ 1261 | recursive = !!recursive; 1262 | var deleted = [], 1263 | pending = {}, 1264 | depth = [], 1265 | currentPending = '', 1266 | currentDepthPath = '', 1267 | filterPaths = function (statObject) { 1268 | return statObject.filename !== '.' && statObject.filename !== '..'; 1269 | }, 1270 | checkProc = function () { 1271 | if (!holdQueue) { 1272 | ftp.emit('endproc'); 1273 | } 1274 | }, 1275 | removeDir = function () { 1276 | dbg('removing directory: '.magenta, currentDepthPath); 1277 | ftp.runNow(ftp.rmdir.raw + ' ' + currentDepthPath, function (err, res) { 1278 | dbg('Directory deleted: '.green, currentDepthPath); 1279 | if (err) { 1280 | throw new Error('could not delete dir @' + currentDepthPath); 1281 | } else { 1282 | //stack deleted and remove from queues 1283 | deleted.push(currentDepthPath); 1284 | depth.pop(); 1285 | delete pending[currentPending]; 1286 | //continue deleting as necessary 1287 | if (depth.length > 0) { 1288 | //update depth to last item in depth array 1289 | currentPending = depth[depth.length - 1]; 1290 | currentDepthPath = depth.join(path.sep); 1291 | unlinkPending(); 1292 | } else { 1293 | callback(err, deleted); 1294 | checkProc(); 1295 | } 1296 | } 1297 | }); 1298 | }, 1299 | bindUnlinkHandler = function (item) { 1300 | var cmd = item.isDirectory ? ftp.rmdir.raw : ftp.unlink.raw, 1301 | filepath, 1302 | handleDeleteResponse = function (err) { 1303 | if (err) { 1304 | dbg('scanning directory: '.cyan, item.filename); 1305 | //restack item 1306 | checkDir(err, item.filename); 1307 | return; 1308 | } else { 1309 | dbg('file unlinked: '.yellow, item.filename); 1310 | deleted.push(filepath); 1311 | //continue deleting as necessary 1312 | if (pending[currentPending].length > 0) { 1313 | unlinkPending(); 1314 | } else { 1315 | //remove the directory if it is empty 1316 | removeDir(); 1317 | } 1318 | } 1319 | }; 1320 | 1321 | filepath = path.join(currentDepthPath, item.filename); 1322 | dbg('rmdir: removing "' + filepath + '"'); 1323 | ftp.runNow(cmd + ' ' + filepath, handleDeleteResponse, true); 1324 | }, 1325 | unlinkPending = function () { 1326 | dbg('unlinking file object'); 1327 | if (pending[currentPending].length > 0) { 1328 | bindUnlinkHandler(pending[currentPending].pop()); 1329 | } else { 1330 | removeDir(); 1331 | } 1332 | }, 1333 | handleFileList = function (err, data) { 1334 | dbg('rmdir: handleFileList...'.magenta, err, data.length - 2); 1335 | if (err) { 1336 | callback(err, data); 1337 | } else { 1338 | if (data.length === 0) { 1339 | callback(new Error('Directory does not exist'), null); 1340 | return checkProc(); 1341 | } 1342 | pending[currentPending] = data.filter(filterPaths); 1343 | unlinkPending(); 1344 | } 1345 | }, 1346 | checkDir = function (err, data) { 1347 | dbg('checking dir:'.cyan, err, data); 1348 | if (!recursive) { 1349 | dbg('ending !recursive'.red); 1350 | return callback(err, deleted); 1351 | } 1352 | if (!err) { 1353 | pending -= 1; 1354 | dbg('rmdir> directory deleted'.yellow, dirpath); 1355 | deleted.push(dirpath); 1356 | callback(err, deleted); 1357 | checkProc(); 1358 | } else { 1359 | data = data ? data : dirpath; 1360 | dbg('need to ls directory', data); 1361 | //add file to list of items needed to be removed, increases depth 1362 | depth.push(data); 1363 | //create new array of pending items for directory 1364 | pending[data] = []; 1365 | //open the queue immediately after this callback 1366 | currentPending = data; 1367 | currentDepthPath = depth.join(path.sep); 1368 | ftp.ls(currentDepthPath, handleFileList, Queue.RunNow, true); 1369 | } 1370 | }; 1371 | ftp.run(ftp.rmdir.raw + ' ' + dirpath, checkDir, runLevel, true); 1372 | };//}}} 1373 | FTP.prototype.rmdir.raw = 'RMD'; 1374 | 1375 | /** 1376 | * Runs the FTP command PWD - Print Working Directory 1377 | * @param {function} callback - The callback function to be issued. 1378 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1379 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1380 | */ 1381 | FTP.prototype.getcwd = function (callback, runLevel, holdQueue) {//{{{ 1382 | ftp.run(FTP.prototype.getcwd.raw, function (err, data) { 1383 | if (!err) { 1384 | data = data.match(/"(.*?)"/)[1]; 1385 | ftp.cwd = data; 1386 | } 1387 | callback.call(callback, err, data); 1388 | }, runLevel, holdQueue); 1389 | };//}}} 1390 | FTP.prototype.getcwd.raw = 'PWD'; 1391 | 1392 | /** 1393 | * Runs the FTP command CWD - Change Working Directory 1394 | * @param {string} dirpath - The directory name to change to. 1395 | * @param {function} callback - The callback function to be issued. 1396 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1397 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1398 | */ 1399 | FTP.prototype.chdir = function (dirname, callback, runLevel, holdQueue) {//{{{ 1400 | ftp.run(FTP.prototype.chdir.raw + ' ' + dirname, function (err, data) { 1401 | if (!err) { 1402 | dirname = data.match(/"(.*)"/); 1403 | if (null !== dirname) { 1404 | ftp.cwd = dirname[1]; 1405 | } else { 1406 | ftp.cwd = data; 1407 | } 1408 | } 1409 | callback.call(callback, err, data); 1410 | }, runLevel, holdQueue); 1411 | };//}}} 1412 | FTP.prototype.chdir.raw = 'CWD'; 1413 | 1414 | /** 1415 | * Runs the FTP command DELE - Delete remote file 1416 | * @param {string} filepath - The location of the file to be deleted. 1417 | * @param {function} callback - The callback function to be issued. 1418 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1419 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1420 | */ 1421 | FTP.prototype.unlink = function (filepath, callback, runLevel, holdQueue) {//{{{ 1422 | ftp.run(FTP.prototype.unlink.raw + ' ' + filepath, function (err, data) { 1423 | if (!err) { 1424 | data = data.match(/(del\w+) *(.*)/i)[2]; 1425 | } 1426 | callback.call(callback, err, data); 1427 | }, runLevel, holdQueue); 1428 | };//}}} 1429 | FTP.prototype.unlink.raw = 'DELE'; 1430 | 1431 | /** 1432 | * Runs the FTP command ABOR - Abort a file transfer, this takes place in parallel 1433 | * @param {function} callback - The callback function to be issued. 1434 | */ 1435 | FTP.prototype.abort = function (callback) {//{{{ 1436 | ftp.raw('ABOR', function (err, data) { 1437 | dbg('--------abort-------'); 1438 | dbg(ftp.pipeActive, ftp.pipeAborted); 1439 | dbg(err, data); 1440 | if (ftp.pipeActive) { 1441 | dbg('pipe was active'.blue); 1442 | ftp.pipeAborted = true; 1443 | ftp.pipe.end(); 1444 | } 1445 | if (!err) { 1446 | data = data.length > 0; 1447 | } 1448 | callback.call(callback, err, data); 1449 | }); 1450 | };//}}} 1451 | FTP.prototype.abort.raw = 'ABOR'; 1452 | 1453 | 1454 | /** 1455 | * Runs the FTP command RETR - Retrieve a remote file 1456 | * @function 1457 | * @param {string} filepath - The location of the remote file to fetch. 1458 | * @param {function} callback - The callback function to be issued. 1459 | */ 1460 | FTP.prototype.get = Queue.create('RETR'); 1461 | 1462 | 1463 | /** 1464 | * Runs the FTP command RETR - Retrieve a remote file and 1465 | * then saves the file locally 1466 | * @param {string|array} paths - An array of [from, to] paths, 1467 | * as in read from "remote/location/foo.txt" and save 1468 | * to "local/path/bar.txt"
1469 | * if you are saving to the same relative location you are reading 1470 | * from then you can supply a string as a shortcut. 1471 | * @param {function} callback - The callback function to be issued. 1472 | */ 1473 | FTP.prototype.save = function (paths, callback) {//{{{ 1474 | var isString = typeof paths === 'string', 1475 | localPath, 1476 | remotePath, 1477 | dataHandler; 1478 | if (!isString && !(paths instanceof Array)) { 1479 | throw new Error('ftp.put > parameter 1 expected an array or string'); 1480 | } else if (paths.length === 0) { 1481 | throw new Error('ftp.put > parameter 1 expected recvd empty array'); 1482 | } 1483 | if (isString || paths.length === 1) { 1484 | localPath = remotePath = isString ? paths : paths[0]; 1485 | } else { 1486 | remotePath = paths[0]; 1487 | localPath = paths[1]; 1488 | } 1489 | 1490 | dbg('>saving file: ' + remotePath + ' to ' + localPath); 1491 | dataHandler = function (err, data) { 1492 | if (!err) { 1493 | try { 1494 | fs.writeFileSync(localPath, data); 1495 | } catch (e) { 1496 | err = e; 1497 | } 1498 | } 1499 | callback.call(callback, err, localPath); 1500 | }; 1501 | ftp.get(remotePath, dataHandler); 1502 | };//}}} 1503 | 1504 | 1505 | /** 1506 | * Creates a new file stat object similar to Node's fs.stat method. 1507 | * @constructor 1508 | * @memberof FTP 1509 | * @param {string} stat - The stat string of the file or directory 1510 | * i.e.
"drwxr-xr-x 2 userfoo groupbar 4096 Jun 12:43 filename" 1511 | */ 1512 | StatObject = function (stat) {//{{{ 1513 | var that = this, 1514 | currentDate = new Date(); 1515 | that.isDirectory = stat[1] === 'd'; 1516 | that.isSymbolicLink = stat[1] === 'l'; 1517 | that.isFile = stat[1] === '-'; 1518 | that.permissions = StatObject.parsePermissions(stat[2]); 1519 | that.nlink = stat[3]; 1520 | that.owner = stat[4]; 1521 | that.group = stat[5]; 1522 | that.size = stat[6]; 1523 | that.mtime = Date.parse(currentDate.getFullYear() + ' ' + stat[7]); 1524 | //symbolic links will capture their pointing location 1525 | if (that.isSymbolicLink) { 1526 | stat[8] = stat[8].split(' -> '); 1527 | that.linkTarget = stat[8][1]; 1528 | } 1529 | that.filename = that.isSymbolicLink ? stat[8][0] : stat[8]; 1530 | };//}}} 1531 | 1532 | /** 1533 | * Creates a new file stat object similar to Node's fs.stat method, this 1534 | * dummy object is ideal for manipulating your own StatObjects 1535 | * @constructor 1536 | * @memberof FTP 1537 | * @param {string} filename - the filename to set for the stat object 1538 | * i.e.
"drwxr-xr-x 2 userfoo groupbar 4096 Jun 12:43 filename" 1539 | */ 1540 | StatObject.Dummy = function (filename) { 1541 | this.filename = filename ? filename : ''; 1542 | }; 1543 | 1544 | StatObject.Dummy.prototype = StatObject.prototype; 1545 | 1546 | FTP.prototype.StatObject = StatObject; 1547 | 1548 | /** @lends StatObject */ 1549 | StatObject.prototype = {//{{{ 1550 | /** 1551 | * The regular expression used to parse the stat string 1552 | * @type {object} 1553 | */ 1554 | _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+(.*)/, 1555 | //TODO -- raw 1556 | /** 1557 | * The actual response string 1558 | * @instance 1559 | * @type {string} 1560 | */ 1561 | raw: '', 1562 | /** 1563 | * Set to true if path is a directory 1564 | * @instance 1565 | * @type {boolean} 1566 | */ 1567 | isDirectory: false, 1568 | /** 1569 | * Set to true if path is a symbolic link 1570 | * @instance 1571 | * @type {boolean} 1572 | */ 1573 | isSymbolicLink: false, 1574 | /** 1575 | * Set to true if path is a file 1576 | * @instance 1577 | * @type {boolean} 1578 | */ 1579 | isFile: false, 1580 | /** 1581 | * A number representing the set file permissions; ie: 755 1582 | * @instance 1583 | * @type {null|number} 1584 | */ 1585 | permissions: null, 1586 | /** 1587 | * The number of hardlinks pointing to the file or directory 1588 | * @instance 1589 | * @type {number} 1590 | */ 1591 | nlink: 0, 1592 | /** 1593 | * The owner of the current file or directory 1594 | * @instance 1595 | * @type {string} 1596 | */ 1597 | owner: '', 1598 | /** 1599 | * The group belonging to the file or directory 1600 | * @instance 1601 | * @type {string} 1602 | */ 1603 | group: '', 1604 | /** 1605 | * The size of the file in bytes 1606 | * @instance 1607 | * @type {number} 1608 | */ 1609 | size: 0, 1610 | /** 1611 | * The files relative* modification time. *Since stat strings only 1612 | * give us accuracy to the minute, it's more accurate to perform a 1613 | * {@link FTP#filemtime} on your file if you wish to compare 1614 | * modified times more accurately. 1615 | * @instance 1616 | * @type {number} 1617 | */ 1618 | mtime: 0, 1619 | /** 1620 | * If the filepath points to a symbolic link then this 1621 | * will hold a reference to the link's target 1622 | * @instance 1623 | * @type {null|string} 1624 | */ 1625 | linkTarget: null, 1626 | /** 1627 | * The name of the directory, file, or symbolic link 1628 | * @instance 1629 | * @type {string} 1630 | */ 1631 | filename: '' 1632 | };//}}} 1633 | 1634 | 1635 | /** 1636 | * Create and return a new {@link StatObject} instance 1637 | * @param {string} stat - The stat string of the file or directory. 1638 | * @returns {object} StatObject - New StatObject 1639 | */ 1640 | StatObject.create = function (stat) {//{{{ 1641 | return new StatObject(stat); 1642 | };//}}} 1643 | 1644 | 1645 | /** 1646 | * Parses a permission string into it's relative number value 1647 | * @param {string} permissionString - The string of permissions i.e. "drwxrwxrwx" 1648 | * @returns {number} permissions - The number value of the given permissionString 1649 | */ 1650 | StatObject.parsePermissions = function (permissionString) {//{{{ 1651 | var i = 0, 1652 | c, 1653 | perm, 1654 | val = [], 1655 | lap = 0, 1656 | str = ''; 1657 | for (i; i < permissionString.length; i += 3) { 1658 | str = permissionString.slice(i, i + 3); 1659 | perm = 0; 1660 | for (c = 0; c < str.length; c++) { 1661 | if (str[c] === '-') { 1662 | continue; 1663 | } 1664 | perm += StatObject.values[str[c]]; 1665 | } 1666 | val.push(perm); 1667 | lap += 1; 1668 | } 1669 | return Number(val.join('')); 1670 | };//}}} 1671 | 1672 | 1673 | /** 1674 | * Holds the relative number values for parsing permissions 1675 | * with {@link StatObject.parsePermissions} 1676 | * @static StatObject.values 1677 | * @type {object} 1678 | */ 1679 | StatObject.values = {//{{{ 1680 | 'r': 4, 1681 | 'w': 2, 1682 | 'x': 1 1683 | };//}}} 1684 | 1685 | 1686 | Queue.registerHook('LIST', function (data, reg) {//{{{ 1687 | reg = reg || StatObject.prototype._reg; 1688 | dbg('ls:hook> data: '.magenta, data); 1689 | let list = []; 1690 | if (!data) { 1691 | return list; 1692 | } 1693 | data = data.toString().split('\r\n').filter(Boolean); 1694 | data.reduce((acc, stat) => { 1695 | stat = stat.match(reg); 1696 | if (stat) { 1697 | acc.push(StatObject.create(stat)); 1698 | } 1699 | return acc; 1700 | }, list); 1701 | return list; 1702 | });//}}} 1703 | 1704 | 1705 | /** 1706 | * Runs the FTP command LIST - List remote files 1707 | * @function 1708 | * @param {string} filepath - The location of the remote file or directory to list. 1709 | * @param {function} callback - The callback function to be issued. 1710 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1711 | * 1712 | */ 1713 | FTP.prototype.ls = Queue.create('LIST'); 1714 | 1715 | 1716 | /** 1717 | * Runs the FTP command MDTM - Return the modification time of a file 1718 | * @param {string} filepath - The location of the remote file to stat. 1719 | * @param {function} callback - The callback function to be issued. 1720 | * @returns {number} filemtime - File modified time in milliseconds 1721 | * @example 1722 | * //getting a date object from the file modified time 1723 | * ftp.filemtime('foo.js', function (err, filemtime) { 1724 | * if (err) { 1725 | * dbg(err); 1726 | * } else { 1727 | * dbg(filemtime); 1728 | * //1402849093000 1729 | * var dateObject = new Date(filemtime); 1730 | * //Sun Jun 15 2014 09:18:13 GMT-0700 (PDT) 1731 | * } 1732 | * }); 1733 | */ 1734 | FTP.prototype.filemtime = function (filepath, callback, runLevel, holdQueue) {//{{{ 1735 | ftp.run('MDTM ' + filepath, function (err, data) { 1736 | if (!err) { 1737 | data = data.match(/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})/); 1738 | if (null !== data) { 1739 | data = Date.parse(data[1] + '-' + data[2] + '-' + data[3] + ' ' + 1740 | data[4] + ':' + data[5] + ':' + data[6]); 1741 | } 1742 | } 1743 | callback.call(callback, err, data); 1744 | }); 1745 | };//}}} 1746 | 1747 | FTP.prototype.filemtime.raw = 'MDTM'; 1748 | 1749 | 1750 | Queue.registerHook('NLST', function (data) {//{{{ 1751 | if (null === data) { 1752 | return []; 1753 | } 1754 | var filter = function (elem) { 1755 | return elem.length > 0 && elem !== '.' && elem !== '..'; 1756 | }; 1757 | data = data.toString().split('\r\n').filter(filter); 1758 | return data; 1759 | });//}}} 1760 | 1761 | /** 1762 | * Runs the FTP command NLST - Name list of remote directory. 1763 | * @function 1764 | * @param {string} dirpath - The location of the remote directory to list. 1765 | * @param {function} callback - The callback function to be issued. 1766 | */ 1767 | FTP.prototype.lsnames = Queue.create('NLST'); 1768 | 1769 | 1770 | /** 1771 | * Runs the FTP command SIZE - Get size of remote file 1772 | * @function 1773 | * @param {string} filepath - The location of the file to retrieve size from. 1774 | * @param {function} callback - The callback function to be issued. 1775 | */ 1776 | FTP.prototype.size = Queue.create('SIZE'); 1777 | 1778 | 1779 | /** 1780 | * Runs the FTP command USER - Send username. 1781 | * @param {string} username - The name of the user to log in. 1782 | * @param {function} callback - The callback function to be issued. 1783 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1784 | */ 1785 | FTP.prototype.user = function (user, callback, runLevel, holdQueue) { 1786 | ftp.run(FTP.prototype.user.raw + ' ' + user, callback, runLevel, holdQueue); 1787 | }; 1788 | FTP.prototype.user.raw = 'USER'; 1789 | 1790 | 1791 | /** 1792 | * Runs the FTP command PASS - Send password. 1793 | * @param {string} pass - The password for the user. 1794 | * @param {function} callback - The callback function to be issued. 1795 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1796 | */ 1797 | FTP.prototype.pass = function (pass, callback, runLevel, holdQueue) { 1798 | ftp.run(FTP.prototype.pass.raw + ' ' + pass, callback, runLevel, holdQueue); 1799 | }; 1800 | FTP.prototype.pass.raw = 'PASS'; 1801 | 1802 | 1803 | /** 1804 | * Runs the FTP command PASV - Open a data port in passive mode. 1805 | * @param {string} pasv - The pasv parameter a1,a2,a3,a4,p1,p2 1806 | * where a1.a2.a3.a4 is the IP address and p1*256+p2 is the port number 1807 | * @param {function} callback - The callback function to be issued. 1808 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1809 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1810 | */ 1811 | FTP.prototype.pasv = function (callback, runLevel, holdQueue) { 1812 | ftp.run(FTP.prototype.pasv.raw, callback, runLevel, holdQueue); 1813 | }; 1814 | FTP.prototype.pasv.raw = 'PASV'; 1815 | 1816 | 1817 | /** 1818 | * Runs the FTP command PORT - Open a data port in active mode. 1819 | * @param {string} port - The port parameter a1,a2,a3,a4,p1,p2. 1820 | * This is interpreted as IP address a1.a2.a3.a4, port p1*256+p2. 1821 | * @param {function} callback - The callback function to be issued. 1822 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1823 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1824 | */ 1825 | FTP.prototype.port = function (port, callback, runLevel, holdQueue) { 1826 | ftp.run(FTP.prototype.port.raw + ' ' + port, callback, runLevel, holdQueue); 1827 | }; 1828 | FTP.prototype.port.raw = 'PORT'; 1829 | 1830 | 1831 | /** 1832 | * Runs the FTP command QUIT - Terminate the connection. 1833 | * @param {function} callback - The callback function to be issued. 1834 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1835 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1836 | */ 1837 | FTP.prototype.quit = function (callback, runLevel, holdQueue) { 1838 | ftp.run(FTP.prototype.quit.raw, callback, runLevel, holdQueue); 1839 | }; 1840 | FTP.prototype.quit.raw = 'QUIT'; 1841 | 1842 | 1843 | /** 1844 | * Runs the FTP command NOOP - Do nothing; Keeps the connection from timing out; 1845 | * determine latency(ms), the latency will be passed as the data(second) 1846 | * parameter of the callback 1847 | * @param {function} callback - The callback function to be issued. 1848 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1849 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1850 | */ 1851 | FTP.prototype.ping = function (callback, runLevel, holdQueue) { 1852 | ftp.run(FTP.prototype.ping.raw, function (err) { 1853 | callback.call(this, err, this.ping); 1854 | }, runLevel, holdQueue); 1855 | }; 1856 | FTP.prototype.ping.raw = 'NOOP'; 1857 | 1858 | 1859 | /** 1860 | * Runs the FTP command STAT - Return server status 1861 | * @param {function} callback - The callback function to be issued. 1862 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1863 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1864 | */ 1865 | FTP.prototype.stat = function (callback, runLevel, holdQueue) { 1866 | ftp.run(FTP.prototype.stat.raw, callback, runLevel, holdQueue); 1867 | }; 1868 | FTP.prototype.stat.raw = 'STAT'; 1869 | 1870 | 1871 | /** 1872 | * Runs the FTP command SYST - return system type 1873 | * @param {function} callback - The callback function to be issued. 1874 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1875 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1876 | */ 1877 | FTP.prototype.info = function (callback, runLevel, holdQueue) { 1878 | ftp.run(FTP.prototype.info.raw, callback, runLevel, holdQueue); 1879 | }; 1880 | FTP.prototype.info.raw = 'SYST'; 1881 | 1882 | 1883 | /** 1884 | * Runs the FTP command RNFR and RNTO - Rename from and rename to; Rename a remote file 1885 | * @param {array} paths - The path of the current file and the path you wish 1886 | * to rename it to; eg: ['from', 'to'] 1887 | * @param {function} callback - The callback function to be issued. 1888 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1889 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1890 | */ 1891 | FTP.prototype.rename = function (paths, callback, holdQueue) {//{{{ 1892 | if (!(paths instanceof Array)) { 1893 | throw new Error('ftp.rename > parameter 1 expected array; [from, to]'); 1894 | } 1895 | var from = paths[0], 1896 | to = paths[1]; 1897 | 1898 | //run this in a queue 1899 | ftp.run('RNFR ' + from, function (err, data) { 1900 | if (err) { 1901 | callback.call(callback, err, data); 1902 | ftp.emit('endproc'); 1903 | } else { 1904 | //run rename to command immediately 1905 | ftp.run(FTP.prototype.rename.raw + ' ' + to, callback, true, holdQueue); 1906 | } 1907 | }, false, true); 1908 | };//}}} 1909 | FTP.prototype.rename.raw = 'RNTO'; 1910 | 1911 | 1912 | /** 1913 | * Runs the FTP command TYPE - Set transfer type (default ASCII) - will be added in next patch 1914 | * @param {string} type - set to this type: 'ascii', 'ebcdic', 'binary', 'local' 1915 | * @param {string} secondType - 'nonprint', 'telnet', 'asa' 1916 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1917 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1918 | */ 1919 | FTP.prototype.type = function (type, secondType, callback, runLevel, holdQueue) { 1920 | var that = this, 1921 | cmd = ''; 1922 | if (typeof secondType === 'function') { 1923 | holdQueue = runLevel; 1924 | runLevel = callback; 1925 | callback = secondType; 1926 | secondType = undefined; 1927 | } 1928 | if (undefined === type || undefined === that.typeMap[type]) { 1929 | return callback(new Error('ftp.type > parameter 1 expected valid FTP TYPE; [ascii, binary, ebcdic, local]'), null); 1930 | } 1931 | cmd = that.typeMap[type]; 1932 | if (undefined !== secondType && undefined === that.secondTypeMap[secondType]) { 1933 | cmd += ' ' + that.secondTypeMap[secondType]; 1934 | } 1935 | //update currentType and run 1936 | var done = function (err, data) { 1937 | ftp.currentType = type; 1938 | callback.call(callback, err, data); 1939 | }; 1940 | ftp.run(FTP.prototype.type.raw + ' ' + cmd, done, runLevel, holdQueue); 1941 | }; 1942 | FTP.prototype.type.raw = 'TYPE'; 1943 | 1944 | 1945 | /** 1946 | * Sets the type of file transfer that should be used 1947 | * based on the path provided 1948 | * @params {string} filepath - the path to the file being transferred 1949 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 1950 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 1951 | */ 1952 | FTP.prototype.setType = function (filepath, callback, runLevel, holdQueue) { 1953 | var ext, 1954 | hijack = function () { 1955 | callback(); 1956 | if (!holdQueue) { 1957 | ftp.emit('endproc'); 1958 | } 1959 | }, 1960 | args = [undefined, callback, runLevel, holdQueue], 1961 | changeType = function (type) { 1962 | args[0] = type; 1963 | ftp.type.apply(ftp, args); 1964 | }; 1965 | let dotIndex = filepath.indexOf('.'); 1966 | let ensureType = () => { 1967 | if (ftp.currentType !== 'ascii') { 1968 | changeType('ascii'); 1969 | } else { 1970 | ftp.queue.register(hijack, runLevel); 1971 | } 1972 | }; 1973 | //dot files eg .htaccess or no extension 1974 | if (dotIndex < 1) { 1975 | return ensureType(); 1976 | } 1977 | ext = filepath.split('.').pop(); 1978 | if (ftp.ascii[ext]) { 1979 | ensureType(); 1980 | } else if (ftp.currentType === 'ascii') { 1981 | changeType('binary'); 1982 | } else { 1983 | ftp.queue.register(hijack, runLevel); 1984 | } 1985 | }; 1986 | 1987 | 1988 | 1989 | /** 1990 | * Values that are used with {@link FTP#setType} and {@link FTP#type} 1991 | * to set the transfer type of data 1992 | * @readonly 1993 | * @class 1994 | * @enum {string} 1995 | */ 1996 | FTP.prototype.typeMap = { 1997 | ascii: 'A', 1998 | binary: 'I', 1999 | ebcdic: 'E', 2000 | local: 'L' 2001 | }; 2002 | 2003 | /** 2004 | * @readonly 2005 | * @class 2006 | * @enum {string} 2007 | */ 2008 | FTP.prototype.secondTypeMap = { 2009 | nonprint: 'N', 2010 | telnet: 'T', 2011 | asa: 'C' 2012 | }; 2013 | 2014 | /* 2015 | ftp.raw('TYPE A N', function (err, data) { 2016 | dbg(err, data); 2017 | }); 2018 | */ 2019 | 2020 | 2021 | /** 2022 | * Runs the FTP command MODE - Set transfer mode (default Stream) - will be added in next patch 2023 | * @param {string} type - set to this type: 'stream', 'block', 'compressed' 2024 | * @todo - This still needs to be added - should create an object of methods 2025 | */ 2026 | FTP.prototype.mode = function () { 2027 | dbg('not yet implemented'); 2028 | }; 2029 | 2030 | /** 2031 | * Runs the FTP command SITE - Run site specific command - will be added in next patch 2032 | * @param {string} command - The command that will be issued 2033 | * @param {string} parameters - The parameters to be passed with the command 2034 | * @todo - This still needs to be added - should create an object of methods 2035 | * @param {boolean} runLevel - execution priority; @see {@link FTP.Queue.RunLevels}. 2036 | * @param {boolean} [holdQueue=false] - Prevents the queue from firing an endproc event, user must end manually 2037 | */ 2038 | FTP.prototype.site = function () { 2039 | dbg('not yet implemented'); 2040 | }; 2041 | 2042 | 2043 | 2044 | /** 2045 | * @class DEPRECATED! use {@link FTP.Queue} 2046 | */ 2047 | FTP.prototype.SimpleQueue = Queue; 2048 | FTP.prototype.Queue = Queue; 2049 | FTP.Queue = Queue; 2050 | 2051 | module.exports = FTP; 2052 | 2053 | --------------------------------------------------------------------------------