w-ftp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +54 -0
- package/.jsdoc +25 -0
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +14 -0
- package/docs/WFtp.mjs.html +665 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/global.html +439 -0
- package/docs/index.html +81 -0
- package/docs/jsftp.js.html +925 -0
- package/docs/scripts/collapse.js +20 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +83 -0
- package/docs/styles/jsdoc.css +765 -0
- package/docs/styles/prettify.css +79 -0
- package/g-download.mjs +84 -0
- package/g-upload.mjs +65 -0
- package/package.json +30 -0
- package/src/WFtp.mjs +596 -0
- package/src/jsftp.js +856 -0
- package/test/all.test.mjs +10 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +4 -0
- package/toolg/gDistRollup.mjs +27 -0
- package/toolg/modifyReadme.mjs +4 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>jsftp.js - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
|
|
21
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
22
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
23
|
+
<div class="navicon"></div>
|
|
24
|
+
</label>
|
|
25
|
+
|
|
26
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
27
|
+
|
|
28
|
+
<nav >
|
|
29
|
+
|
|
30
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WFtp">WFtp</a></li></ul>
|
|
31
|
+
</nav>
|
|
32
|
+
|
|
33
|
+
<div id="main">
|
|
34
|
+
|
|
35
|
+
<h1 class="page-title">jsftp.js</h1>
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
<section>
|
|
44
|
+
<article>
|
|
45
|
+
<pre class="prettyprint source linenums"><code>//fork from: https://github.com/sergi/jsftp
|
|
46
|
+
|
|
47
|
+
'use strict'
|
|
48
|
+
|
|
49
|
+
const createConnection = require('net').createConnection
|
|
50
|
+
const EventEmitter = require('events').EventEmitter
|
|
51
|
+
const inherits = require('util').inherits
|
|
52
|
+
const stream = require('stream')
|
|
53
|
+
const fs = require('fs')
|
|
54
|
+
const combine = require('stream-combiner')
|
|
55
|
+
|
|
56
|
+
const ResponseParser = require('ftp-response-parser')
|
|
57
|
+
const ListingParser = require('parse-listing')
|
|
58
|
+
const once = require('once')
|
|
59
|
+
const nfc = require('unorm').nfc
|
|
60
|
+
|
|
61
|
+
const debug = require('debug')('jsftp:general')
|
|
62
|
+
const dbgCommand = require('debug')('jsftp:command')
|
|
63
|
+
const dbgResponse = require('debug')('jsftp:response')
|
|
64
|
+
|
|
65
|
+
const FTP_HOST = 'localhost'
|
|
66
|
+
const FTP_PORT = 21
|
|
67
|
+
const TIMEOUT = 10 * 60 * 1000
|
|
68
|
+
const IDLE_TIME = 30000
|
|
69
|
+
const NOOP = function() {}
|
|
70
|
+
|
|
71
|
+
const expectedMarks = {
|
|
72
|
+
marks: [125, 150],
|
|
73
|
+
ignore: 226
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const RE_PASV = /([-\d]+,[-\d]+,[-\d]+,[-\d]+),([-\d]+),([-\d]+)/
|
|
77
|
+
const FTP_NEWLINE = /\r\n|\n/
|
|
78
|
+
|
|
79
|
+
function runCmd(name, ...params) {
|
|
80
|
+
let callback = NOOP
|
|
81
|
+
let completeCmd = name + ' '
|
|
82
|
+
|
|
83
|
+
if (typeof params[params.length - 1] === 'function') {
|
|
84
|
+
callback = params.pop()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
completeCmd += params.join(' ')
|
|
88
|
+
this.execute(completeCmd.trim(), callback)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Ftp(cfg) {
|
|
92
|
+
this.host = cfg.host || FTP_HOST
|
|
93
|
+
this.port = cfg.port || FTP_PORT
|
|
94
|
+
this.user = cfg.user || 'anonymous'
|
|
95
|
+
this.pass = cfg.pass || '@anonymous'
|
|
96
|
+
this.createSocket = cfg.createSocket
|
|
97
|
+
// True if the server doesn't support the `stat` command. Since listing a
|
|
98
|
+
// directory or retrieving file properties is quite a common operation, it is
|
|
99
|
+
// more efficient to avoid the round-trip to the server.
|
|
100
|
+
this.useList = cfg.useList || false
|
|
101
|
+
|
|
102
|
+
this.commandQueue = []
|
|
103
|
+
|
|
104
|
+
EventEmitter.call(this)
|
|
105
|
+
|
|
106
|
+
this.on('data', dbgResponse)
|
|
107
|
+
this.on('error', dbgResponse)
|
|
108
|
+
|
|
109
|
+
this._createSocket(this.port, this.host)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
inherits(Ftp, EventEmitter)
|
|
113
|
+
|
|
114
|
+
// Generate generic methods from parameter names. they can easily be
|
|
115
|
+
// overriden if we need special behavior. they accept any parameters given,
|
|
116
|
+
// it is the responsibility of the user to validate the parameters.
|
|
117
|
+
Ftp.prototype.raw = function() {
|
|
118
|
+
runCmd.apply(this, arguments)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Ftp.prototype.reemit = function(event) {
|
|
122
|
+
return data => {
|
|
123
|
+
this.emit(event, data)
|
|
124
|
+
debug(`event:${event}`, data || {})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Ftp.prototype._createSocket = function(port, host, firstAction = NOOP) {
|
|
129
|
+
if (this.socket && this.socket.destroy) {
|
|
130
|
+
this.socket.destroy()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.resParser) {
|
|
134
|
+
this.resParser.end()
|
|
135
|
+
}
|
|
136
|
+
this.resParser = new ResponseParser()
|
|
137
|
+
|
|
138
|
+
this.authenticated = false
|
|
139
|
+
this.socket = this.createSocket
|
|
140
|
+
? this.createSocket({ port, host }, firstAction)
|
|
141
|
+
: createConnection(port, host, firstAction)
|
|
142
|
+
this.socket.on('connect', this.reemit('connect'))
|
|
143
|
+
this.socket.on('timeout', this.reemit('timeout'))
|
|
144
|
+
|
|
145
|
+
this.pipeline = combine(this.socket, this.resParser)
|
|
146
|
+
|
|
147
|
+
this.pipeline.on('data', data => {
|
|
148
|
+
this.emit('data', data)
|
|
149
|
+
dbgResponse(data.text)
|
|
150
|
+
this.parseResponse(data)
|
|
151
|
+
})
|
|
152
|
+
this.pipeline.on('error', this.reemit('error'))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Ftp.prototype.parseResponse = function(response) {
|
|
156
|
+
if (this.commandQueue.length === 0) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if ([220].indexOf(response.code) > -1) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const next = this.commandQueue[0].callback
|
|
164
|
+
if (response.isMark) {
|
|
165
|
+
// If we receive a Mark and it is not expected, we ignore that command
|
|
166
|
+
if (
|
|
167
|
+
!next.expectsMark ||
|
|
168
|
+
next.expectsMark.marks.indexOf(response.code) === -1
|
|
169
|
+
) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// We might have to ignore the command that comes after the mark.
|
|
174
|
+
if (next.expectsMark.ignore) {
|
|
175
|
+
this.ignoreCmdCode = next.expectsMark.ignore
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this.ignoreCmdCode === response.code) {
|
|
180
|
+
this.ignoreCmdCode = null
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.parse(response, this.commandQueue.shift())
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Sends a new command to the server.
|
|
189
|
+
*
|
|
190
|
+
* @param {String} command Command to write in the FTP socket
|
|
191
|
+
*/
|
|
192
|
+
Ftp.prototype.send = function(command) {
|
|
193
|
+
if (!command) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
dbgCommand(command)
|
|
198
|
+
this.pipeline.write(command + '\r\n')
|
|
199
|
+
|
|
200
|
+
dbgCommand(command)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Ftp.prototype.nextCmd = function() {
|
|
204
|
+
const cmd = this.commandQueue[0]
|
|
205
|
+
if (!this.inProgress && cmd) {
|
|
206
|
+
this.send(cmd.action)
|
|
207
|
+
this.inProgress = true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check whether the ftp user is authenticated at the moment of the
|
|
213
|
+
* enqueing. ideally this should happen in the `push` method, just
|
|
214
|
+
* before writing to the socket, but that would be complicated,
|
|
215
|
+
* since we would have to 'unshift' the auth chain into the queue
|
|
216
|
+
* or play the raw auth commands (that is, without enqueuing in
|
|
217
|
+
* order to not mess up the queue order. ideally, that would be
|
|
218
|
+
* built into the queue object. all this explanation to justify a
|
|
219
|
+
* slight slopiness in the code flow.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} action
|
|
222
|
+
* @param {function} callback
|
|
223
|
+
*/
|
|
224
|
+
Ftp.prototype.execute = function(action, callback = NOOP) {
|
|
225
|
+
if (this.socket && this.socket.writable) {
|
|
226
|
+
return this.runCommand({ action, callback })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.authenticated = false
|
|
230
|
+
this._createSocket(this.port, this.host, () => {
|
|
231
|
+
this.runCommand({ action, callback })
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
Ftp.prototype.runCommand = function(cmd) {
|
|
236
|
+
if (this.authenticated || /^(feat|syst|user|pass)/.test(cmd.action)) {
|
|
237
|
+
this.commandQueue.push(cmd)
|
|
238
|
+
this.nextCmd()
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.getFeatures(() => {
|
|
243
|
+
this.auth(this.user, this.pass, () => {
|
|
244
|
+
this.commandQueue.push(cmd)
|
|
245
|
+
this.nextCmd()
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Parse is called each time that a comand and a request are paired
|
|
252
|
+
* together. That is, each time that there is a round trip of actions
|
|
253
|
+
* between the client and the server.
|
|
254
|
+
*
|
|
255
|
+
* @param {Object} response Response from the server (contains text and code)
|
|
256
|
+
* @param {Array} command Contains the command executed and a callback (if any)
|
|
257
|
+
*/
|
|
258
|
+
Ftp.prototype.parse = function(response, command) {
|
|
259
|
+
let err = null
|
|
260
|
+
if (response.isError) {
|
|
261
|
+
err = new Error(response.text || 'Unknown FTP error.')
|
|
262
|
+
err.code = response.code
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.inProgress = false
|
|
266
|
+
command.callback(err, response)
|
|
267
|
+
this.nextCmd()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Ftp.prototype.getPasvPort = function(text) {
|
|
271
|
+
const match = RE_PASV.exec(text)
|
|
272
|
+
if (!match) {
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let host = match[1].replace(/,/g, '.')
|
|
277
|
+
if (host === '127.0.0.1') {
|
|
278
|
+
host = this.host
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
host,
|
|
283
|
+
port: (parseInt(match[2], 10) & 255) * 256 + (parseInt(match[3], 10) & 255)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns true if the current server has the requested feature.
|
|
289
|
+
*
|
|
290
|
+
* @param {String} feature Feature to look for
|
|
291
|
+
* @return {Boolean} Whether the current server has the feature
|
|
292
|
+
*/
|
|
293
|
+
Ftp.prototype.hasFeat = function(feature) {
|
|
294
|
+
return !!feature && this.features.indexOf(feature.toLowerCase()) > -1
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Returns an array of features supported by the current FTP server
|
|
299
|
+
*
|
|
300
|
+
* @param {String} features Server response for the 'FEAT' command
|
|
301
|
+
* @return {String[]} Array of feature names
|
|
302
|
+
*/
|
|
303
|
+
Ftp.prototype._parseFeats = function(features) {
|
|
304
|
+
// Split and ignore header and footer
|
|
305
|
+
const featureLines = features.split(FTP_NEWLINE).slice(1, -1)
|
|
306
|
+
return featureLines
|
|
307
|
+
.map(feat => feat.trim().toLowerCase())
|
|
308
|
+
.filter(feat => !!feat)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Below this point all the methods are action helpers for FTP that compose
|
|
312
|
+
// several actions in one command
|
|
313
|
+
Ftp.prototype.getFeatures = function(callback) {
|
|
314
|
+
if (this.features) {
|
|
315
|
+
return callback(null, this.features)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.raw('feat', (err, response) => {
|
|
319
|
+
this.features = err ? [] : this._parseFeats(response.text)
|
|
320
|
+
this.raw('syst', (err, res) => {
|
|
321
|
+
if (!err && res.code === 215) {
|
|
322
|
+
this.system = res.text.toLowerCase()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
callback(null, this.features)
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Authenticates the user.
|
|
332
|
+
*
|
|
333
|
+
* @param {String} user Username
|
|
334
|
+
* @param {String} pass Password
|
|
335
|
+
* @param {Function} callback Follow-up function.
|
|
336
|
+
*/
|
|
337
|
+
Ftp.prototype.auth = function(user, pass, callback) {
|
|
338
|
+
if (this.authenticating === true) {
|
|
339
|
+
return callback(new Error('This client is already authenticating'))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (typeof user !== 'string') {
|
|
343
|
+
user = this.user
|
|
344
|
+
}
|
|
345
|
+
if (typeof pass !== 'string') {
|
|
346
|
+
pass = this.pass
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this.authenticating = true
|
|
350
|
+
this.raw('user', user, (err, res) => {
|
|
351
|
+
if (err || [230, 331, 332].indexOf(res.code) === -1) {
|
|
352
|
+
this.authenticating = false
|
|
353
|
+
callback(err)
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
this.raw('pass', pass, (err, res) => {
|
|
357
|
+
this.authenticating = false
|
|
358
|
+
|
|
359
|
+
if (err) {
|
|
360
|
+
callback(err)
|
|
361
|
+
}
|
|
362
|
+
else if ([230, 202].indexOf(res.code) > -1) {
|
|
363
|
+
this.authenticated = true
|
|
364
|
+
this.user = user
|
|
365
|
+
this.pass = pass
|
|
366
|
+
this.raw('type', 'I', () => {
|
|
367
|
+
callback(undefined, res)
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
else if (res.code === 332) {
|
|
371
|
+
this.raw('acct', '') // ACCT not really supported
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
Ftp.prototype.setType = function(type, callback) {
|
|
378
|
+
type = type.toUpperCase()
|
|
379
|
+
if (this.type === type) {
|
|
380
|
+
return callback()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.raw('type', type, (err, data) => {
|
|
384
|
+
if (!err) {
|
|
385
|
+
this.type = type
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
callback(err, data)
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Lists a folder's contents using a passive connection.
|
|
394
|
+
*
|
|
395
|
+
* @param {String} path Remote path for the file/folder to retrieve
|
|
396
|
+
* @param {Function} callback Function to call with errors or results
|
|
397
|
+
*/
|
|
398
|
+
Ftp.prototype.list = function(path, callback) {
|
|
399
|
+
if (arguments.length === 1) {
|
|
400
|
+
callback = arguments[0]
|
|
401
|
+
path = ''
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let listing = ''
|
|
405
|
+
callback = once(callback)
|
|
406
|
+
|
|
407
|
+
this.getPasvSocket((err, socket) => {
|
|
408
|
+
if (err) {
|
|
409
|
+
return callback(err)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
socket.setEncoding('utf8')
|
|
413
|
+
socket.on('data', data => {
|
|
414
|
+
listing += data
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
this.pasvTimeout(socket, callback)
|
|
418
|
+
|
|
419
|
+
socket.once('close', err => {
|
|
420
|
+
if (err) {
|
|
421
|
+
return callback(err)
|
|
422
|
+
}
|
|
423
|
+
else if (!listing) {
|
|
424
|
+
// Some servers return empty string
|
|
425
|
+
return callback({
|
|
426
|
+
code: 451,
|
|
427
|
+
text: `Could not retrieve a file listing for ${path}.`,
|
|
428
|
+
isMark: false,
|
|
429
|
+
isError: true
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
callback(null, listing)
|
|
433
|
+
})
|
|
434
|
+
socket.once('error', callback)
|
|
435
|
+
|
|
436
|
+
function cmdCallback(err, res) {
|
|
437
|
+
if (err) {
|
|
438
|
+
return callback(err)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const isExpectedMark = expectedMarks.marks.some(
|
|
442
|
+
mark => mark === res.code
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
if (!isExpectedMark) {
|
|
446
|
+
callback(
|
|
447
|
+
new Error(
|
|
448
|
+
`Expected marks ${expectedMarks.toString()} instead of: ${res.text}`
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
cmdCallback.expectsMark = expectedMarks
|
|
455
|
+
|
|
456
|
+
this.execute(`list ${path || ''}`, cmdCallback)
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
Ftp.prototype.emitProgress = function(data) {
|
|
461
|
+
this.emit('progress', {
|
|
462
|
+
filename: data.filename,
|
|
463
|
+
action: data.action,
|
|
464
|
+
total: data.totalSize || 0,
|
|
465
|
+
transferred:
|
|
466
|
+
data.socket[data.action === 'get' ? 'bytesRead' : 'bytesWritten']
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Depending on the number of parameters, returns the content of the specified
|
|
472
|
+
* file or directly saves a file into the specified destination. In the latter
|
|
473
|
+
* case, an optional callback can be provided, which will receive the error in
|
|
474
|
+
* case the operation was not successful.
|
|
475
|
+
*
|
|
476
|
+
* @param {String} remotePath File to be retrieved from the FTP server
|
|
477
|
+
* @param {Function|String} localPath Local path where we create the new file
|
|
478
|
+
* @param {Function} [callback] Gets called on either success or failure
|
|
479
|
+
*/
|
|
480
|
+
Ftp.prototype.get = function(remotePath, localPath, callback = NOOP) {
|
|
481
|
+
let finalCallback
|
|
482
|
+
const typeofLocalPath = typeof localPath
|
|
483
|
+
|
|
484
|
+
if (typeofLocalPath === 'function') {
|
|
485
|
+
finalCallback = localPath
|
|
486
|
+
}
|
|
487
|
+
else if (typeofLocalPath === 'string') {
|
|
488
|
+
callback = once(callback)
|
|
489
|
+
finalCallback = (err, socket) => {
|
|
490
|
+
if (err) {
|
|
491
|
+
return callback(err)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const writeStream = fs.createWriteStream(localPath)
|
|
495
|
+
writeStream.on('error', callback)
|
|
496
|
+
|
|
497
|
+
socket.on('readable', () => {
|
|
498
|
+
this.emitProgress({
|
|
499
|
+
filename: remotePath,
|
|
500
|
+
action: 'get',
|
|
501
|
+
socket: socket
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
// This ensures that any expected outcome is handled. There is no
|
|
506
|
+
// danger of the callback being executed several times, because it is
|
|
507
|
+
// wrapped in `once`.
|
|
508
|
+
socket.on('error', callback)
|
|
509
|
+
socket.on('end', callback)
|
|
510
|
+
socket.on('close', callback)
|
|
511
|
+
|
|
512
|
+
socket.pipe(writeStream)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.getGetSocket(remotePath, once(finalCallback))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Returns a socket for a get (RETR) on a path. The socket is ready to be
|
|
521
|
+
* streamed, but it is returned in a paused state. It is left to the user to
|
|
522
|
+
* resume it.
|
|
523
|
+
*
|
|
524
|
+
* @param {String} path Path to the file to be retrieved
|
|
525
|
+
* @param {Function} callback Function to call when finalized, with the socket
|
|
526
|
+
* as a parameter
|
|
527
|
+
*/
|
|
528
|
+
Ftp.prototype.getGetSocket = function(path, callback) {
|
|
529
|
+
callback = once(callback)
|
|
530
|
+
this.getPasvSocket((err, socket) => {
|
|
531
|
+
if (err) {
|
|
532
|
+
return cmdCallback(err)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
socket.on('error', err => {
|
|
536
|
+
if (err.code === 'ECONNREFUSED') {
|
|
537
|
+
err.msg = 'Probably trying a PASV operation while one is in progress'
|
|
538
|
+
}
|
|
539
|
+
cmdCallback(err)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
this.pasvTimeout(socket, cmdCallback)
|
|
543
|
+
socket.pause()
|
|
544
|
+
|
|
545
|
+
function cmdCallback(err, res) {
|
|
546
|
+
if (err) {
|
|
547
|
+
if (socket) {
|
|
548
|
+
// close the socket since it won't be used
|
|
549
|
+
socket.destroy()
|
|
550
|
+
}
|
|
551
|
+
return callback(err)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!socket) {
|
|
555
|
+
return callback(new Error('Error when retrieving PASV socket'))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (res.code === 125 || res.code === 150) {
|
|
559
|
+
return callback(null, socket)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// close the socket since it won't be used
|
|
563
|
+
socket.destroy()
|
|
564
|
+
|
|
565
|
+
return callback(new Error('Unexpected command ' + res.text))
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
cmdCallback.expectsMark = expectedMarks
|
|
569
|
+
this.execute('retr ' + path, cmdCallback)
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Uploads contents on a FTP server. The `from` parameter can be a Buffer or the
|
|
575
|
+
* path for a local file to be uploaded.
|
|
576
|
+
*
|
|
577
|
+
* @param {String|Buffer} from Contents to be uploaded.
|
|
578
|
+
* @param {String} destination path for the remote destination.
|
|
579
|
+
* @param {Function} callback Function to execute on error or success.
|
|
580
|
+
*/
|
|
581
|
+
Ftp.prototype.put = function(from, destination, callback) {
|
|
582
|
+
const putReadable = (from, to, totalSize) => {
|
|
583
|
+
from.on('readable', () => {
|
|
584
|
+
this.emitProgress({
|
|
585
|
+
filename: to,
|
|
586
|
+
action: 'put',
|
|
587
|
+
socket: from,
|
|
588
|
+
totalSize
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
this.getPutSocket(from, to, callback)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (from instanceof Buffer) {
|
|
596
|
+
// this.getPutSocket(from, destination, callback)
|
|
597
|
+
// let self = this
|
|
598
|
+
this.getPutSocket(from, destination, (err, socket) => {
|
|
599
|
+
if (err) {
|
|
600
|
+
return callback(new Error(err))
|
|
601
|
+
}
|
|
602
|
+
let pointer = 0
|
|
603
|
+
let SLICE = 1024 * 1024
|
|
604
|
+
let done = false
|
|
605
|
+
let newBuf
|
|
606
|
+
|
|
607
|
+
newBuf = from.slice(pointer, pointer + SLICE)
|
|
608
|
+
pointer += SLICE
|
|
609
|
+
callback(null, newBuf)
|
|
610
|
+
socket.write(from.slice(0, SLICE))
|
|
611
|
+
|
|
612
|
+
//check
|
|
613
|
+
if (newBuf.length === from.length) {
|
|
614
|
+
socket.end()
|
|
615
|
+
callback(null, [])
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// socket.on('error', (msg) => {
|
|
619
|
+
// console.log('socket err', msg)
|
|
620
|
+
// })
|
|
621
|
+
// socket.on('end', (msg) => {
|
|
622
|
+
// console.log('socket end', msg)
|
|
623
|
+
// })
|
|
624
|
+
// socket.on('close', (msg) => {
|
|
625
|
+
// console.log('socket close', msg)
|
|
626
|
+
// })
|
|
627
|
+
|
|
628
|
+
socket.on('drain', () => {
|
|
629
|
+
console.log('drain')
|
|
630
|
+
if (from.length > pointer + SLICE) {
|
|
631
|
+
newBuf = from.slice(pointer, pointer + SLICE)
|
|
632
|
+
pointer += SLICE
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
newBuf = from.slice(pointer)
|
|
636
|
+
done = true
|
|
637
|
+
// console.log('done', done)
|
|
638
|
+
}
|
|
639
|
+
callback(null, newBuf)
|
|
640
|
+
socket.write(newBuf)
|
|
641
|
+
|
|
642
|
+
// self.emit('progress', {
|
|
643
|
+
// filename: 'BUFFER',
|
|
644
|
+
// action: 'put',
|
|
645
|
+
// transferred: pointer,
|
|
646
|
+
// total: from.length
|
|
647
|
+
// })
|
|
648
|
+
|
|
649
|
+
//check
|
|
650
|
+
if (done) {
|
|
651
|
+
socket.end()
|
|
652
|
+
callback(null, [])
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
}
|
|
660
|
+
else if (typeof from === 'string') {
|
|
661
|
+
fs.stat(from, (err, stats) => {
|
|
662
|
+
if (err && err.code === 'ENOENT') {
|
|
663
|
+
return callback(new Error('Local file doesn\'t exist.'))
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (stats.isDirectory()) {
|
|
667
|
+
return callback(new Error('Local path cannot be a directory'))
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const totalSize = err ? 0 : stats.size
|
|
671
|
+
putReadable(fs.createReadStream(from), destination, totalSize)
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
else if (from instanceof stream.Readable) {
|
|
675
|
+
putReadable(from, destination, 0)
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
callback(
|
|
679
|
+
new Error('Expected `from` parameter to be a Buffer, Stream, or a String')
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
Ftp.prototype.getPutSocket = function(from, path, next) {
|
|
685
|
+
next = once(next || NOOP)
|
|
686
|
+
|
|
687
|
+
this.getPasvSocket((err, socket) => {
|
|
688
|
+
if (err) {
|
|
689
|
+
if (socket) {
|
|
690
|
+
// close the socket since it won't be used
|
|
691
|
+
socket.destroy()
|
|
692
|
+
}
|
|
693
|
+
return next(err)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
socket.on('close', next)
|
|
697
|
+
socket.on('error', next)
|
|
698
|
+
|
|
699
|
+
// socket.on('error', (msg) => {
|
|
700
|
+
// console.log('s error', msg)
|
|
701
|
+
// })
|
|
702
|
+
// socket.on('end', (msg) => {
|
|
703
|
+
// console.log('s end', msg)
|
|
704
|
+
// })
|
|
705
|
+
// socket.on('close', (msg) => {
|
|
706
|
+
// console.log('s close', msg)
|
|
707
|
+
// })
|
|
708
|
+
// socket.on('data', (msg) => {
|
|
709
|
+
// console.log('s data', msg)
|
|
710
|
+
// })
|
|
711
|
+
|
|
712
|
+
const callback = once((err, res) => {
|
|
713
|
+
if (err) {
|
|
714
|
+
if (socket) {
|
|
715
|
+
// close the socket since it won't be used
|
|
716
|
+
socket.destroy()
|
|
717
|
+
}
|
|
718
|
+
return next(err)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Mark 150 indicates that the 'STOR' socket is ready to receive data.
|
|
722
|
+
// Anything else is not relevant.
|
|
723
|
+
if (res.code === 125 || res.code === 150) {
|
|
724
|
+
this.pasvTimeout(socket, next)
|
|
725
|
+
if (from instanceof Buffer) {
|
|
726
|
+
next(null, socket)
|
|
727
|
+
// socket.end(from)
|
|
728
|
+
}
|
|
729
|
+
else if (from instanceof stream.Readable) {
|
|
730
|
+
next(null, socket)
|
|
731
|
+
from.pipe(socket)
|
|
732
|
+
// //resume
|
|
733
|
+
// socket.resume()
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
if (socket) {
|
|
738
|
+
// close the socket since it won't be used
|
|
739
|
+
socket.destroy()
|
|
740
|
+
}
|
|
741
|
+
return next(new Error('Unexpected command ' + res.text))
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
callback.expectsMark = expectedMarks
|
|
746
|
+
|
|
747
|
+
this.execute(`stor ${path}`, callback)
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
Ftp.prototype.pasvTimeout = function(socket, callback) {
|
|
752
|
+
socket.once('timeout', () => {
|
|
753
|
+
debug('PASV socket timeout')
|
|
754
|
+
this.emit('timeout')
|
|
755
|
+
socket.end()
|
|
756
|
+
callback(new Error('Passive socket timeout'))
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
Ftp.prototype.getPasvSocket = function(callback = NOOP) {
|
|
761
|
+
callback = once(callback)
|
|
762
|
+
|
|
763
|
+
this.execute('pasv', (err, res) => {
|
|
764
|
+
if (err) {
|
|
765
|
+
return callback(err)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const options = this.getPasvPort(res.text)
|
|
769
|
+
if (!options) {
|
|
770
|
+
return callback(new Error('Bad passive host/port combination'))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const socket = (this._pasvSocket = this.createSocket
|
|
774
|
+
? this.createSocket(options)
|
|
775
|
+
: createConnection(options))
|
|
776
|
+
socket.setTimeout(this.timeout || TIMEOUT)
|
|
777
|
+
socket.once('close', () => {
|
|
778
|
+
this._pasvSocket = undefined
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
callback(null, socket)
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Provides information about files. It lists a directory contents or
|
|
787
|
+
* a single file and yields an array of file objects. The file objects
|
|
788
|
+
* contain several properties. The main difference between this method and
|
|
789
|
+
* 'list' or 'stat' is that it returns objects with the file properties
|
|
790
|
+
* already parsed.
|
|
791
|
+
*
|
|
792
|
+
* Example of file object:
|
|
793
|
+
*
|
|
794
|
+
* {
|
|
795
|
+
* name: 'README.txt',
|
|
796
|
+
* type: 0,
|
|
797
|
+
* time: 996052680000,
|
|
798
|
+
* size: '2582',
|
|
799
|
+
* owner: 'sergi',
|
|
800
|
+
* group: 'staff',
|
|
801
|
+
* userPermissions: { read: true, write: true, exec: false },
|
|
802
|
+
* groupPermissions: { read: true, write: false, exec: false },
|
|
803
|
+
* otherPermissions: { read: true, write: false, exec: false }
|
|
804
|
+
* }
|
|
805
|
+
*
|
|
806
|
+
* The constants used in the object are defined in ftpParser.js
|
|
807
|
+
*
|
|
808
|
+
* @param {String} filePath Path to the file or directory to list
|
|
809
|
+
* @param {Function} callback Function to call with the proper data when
|
|
810
|
+
* the listing is finished.
|
|
811
|
+
*/
|
|
812
|
+
Ftp.prototype.ls = function(filePath, callback) {
|
|
813
|
+
function entriesToList(err, entries) {
|
|
814
|
+
if (err) {
|
|
815
|
+
return callback(err)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
ListingParser.parseFtpEntries(entries.text || entries, (err, files) => {
|
|
819
|
+
if (err) {
|
|
820
|
+
return callback(err)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
files.forEach(file => {
|
|
824
|
+
// Normalize UTF8 doing canonical decomposition, followed by
|
|
825
|
+
// canonical Composition
|
|
826
|
+
file.name = nfc(file.name)
|
|
827
|
+
})
|
|
828
|
+
callback(null, files)
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (this.useList) {
|
|
833
|
+
this.list(filePath, entriesToList)
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.raw('stat', filePath, (err, data) => {
|
|
837
|
+
// We might be connected to a server that doesn't support the
|
|
838
|
+
// 'STAT' command, which is set as default. We use 'LIST' instead,
|
|
839
|
+
// and we set the variable `useList` to true, to avoid extra round
|
|
840
|
+
// trips to the server to check.
|
|
841
|
+
const errored = err && (err.code === 502 || err.code === 500)
|
|
842
|
+
const isHummingbird =
|
|
843
|
+
this.system && this.system.indexOf('hummingbird') > -1
|
|
844
|
+
if (errored || isHummingbird) {
|
|
845
|
+
// Not sure if the 'hummingbird' system check ^^^ is still
|
|
846
|
+
// necessary. If they support any standards, the 500 error
|
|
847
|
+
// should have us covered. Let's leave it for now.
|
|
848
|
+
this.useList = true
|
|
849
|
+
this.list(filePath, entriesToList)
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
entriesToList(err, data)
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
Ftp.prototype.rename = function(from, to, callback) {
|
|
859
|
+
this.raw('rnfr', from, err => {
|
|
860
|
+
if (err) {
|
|
861
|
+
return callback(err)
|
|
862
|
+
}
|
|
863
|
+
this.raw('rnto', to, callback)
|
|
864
|
+
})
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
Ftp.prototype.keepAlive = function(wait) {
|
|
868
|
+
if (this._keepAliveInterval) {
|
|
869
|
+
clearInterval(this._keepAliveInterval)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
this._keepAliveInterval = setInterval(
|
|
873
|
+
this.raw.bind(this, 'noop'),
|
|
874
|
+
wait || IDLE_TIME
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
Ftp.prototype.destroy = function() {
|
|
879
|
+
if (this._keepAliveInterval) {
|
|
880
|
+
clearInterval(this._keepAliveInterval)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (this.socket && this.socket.writable) {
|
|
884
|
+
this.socket.end()
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (this._pasvSocket && this._pasvSocket.writable) {
|
|
888
|
+
this._pasvSocket.end()
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
this.resParser.end()
|
|
892
|
+
|
|
893
|
+
this.socket = undefined
|
|
894
|
+
this._pasvSocket = undefined
|
|
895
|
+
|
|
896
|
+
this.features = null
|
|
897
|
+
this.authenticated = false
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
module.exports = Ftp
|
|
901
|
+
</code></pre>
|
|
902
|
+
</article>
|
|
903
|
+
</section>
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
</div>
|
|
911
|
+
|
|
912
|
+
<br class="clear">
|
|
913
|
+
|
|
914
|
+
<footer>
|
|
915
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.10</a> on Sat Jul 23 2022 13:03:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
916
|
+
</footer>
|
|
917
|
+
|
|
918
|
+
<script>prettyPrint();</script>
|
|
919
|
+
<script src="scripts/polyfill.js"></script>
|
|
920
|
+
<script src="scripts/linenumber.js"></script>
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
</body>
|
|
925
|
+
</html>
|