├── 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 [](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 |
--------------------------------------------------------------------------------