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