ssh2-sftp-client 7.0.3 → 7.2.1

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/README.md CHANGED
@@ -61,9 +61,9 @@ an SFTP client for node.js, a wrapper around [SSH2](https://github.com/mscdex/ss
61
61
 
62
62
  Documentation on the methods and available options in the underlying modules can be found on the [SSH2](https://github.com/mscdex/ssh2) project pages.
63
63
 
64
- Current stable release is **v7.0.3**.
64
+ Current stable release is **v7.2.1**.
65
65
 
66
- Code has been tested against Node versions 12.22.1, 14.17.0 and 16.2.0
66
+ Code has been tested against Node versions 14.18.2, 16.13.1 and 17.2.0
67
67
 
68
68
  Node versions < 10.x are not supported.
69
69
 
@@ -99,6 +99,8 @@ sftp.connect({
99
99
 
100
100
  - **Breaking Change** Expanded option handling for `get()` and `put()` methods. A number of use cases were identified where setting specific options on the read and write streams and the pipe operation are necessary. For example, disabling `autoClose` on streams or the `end` event in pipes. The `options` argument for `get()` and `put()` calls now supports properties for `readStreamOptions`, `writeStreamOptions` and `pipeOptions`. Note that options are only applied to streams created by the `get()` and `put()` methods. Streams passed into these methods are under the control of the client code and therefore cannot have options supplied in arguments to those streams (you would apply such options when you create the streams). Options are typically only necessary in special use cases. Most of the time, no options are required. However, if you are currently using options to either `put()` or `get()`, you will need to update your code to map these options to the new structure.
101
101
 
102
+ - **Breaking Change 7.1.0** A race condition was identified when using a put() call with a writeStream option of `autoClose: false`. In some situations, the promise would be resolved before the final close of the write stream. This could result in errors if you immediately attempt to access the uploaded file. To avoid this situatioin, the promise is now resolved once a `close` event is emitted. This means that setting `autoClose: false` can no longer be supported. The write stream for `put()` will autoClose once data writing has completed.
103
+
102
104
  - Improved event handling. A listener for a global error event is now defined to catch errors which occur in-between method calls i.e. connection lost in-between calls to the library methods. A new mechanism has also been added for removal of listeners when no longer required.
103
105
 
104
106
  # Documentation<a id="sec-5"></a>
@@ -497,11 +499,12 @@ Upload data from local system to remote server. If the `src` argument is a strin
497
499
  flags: 'w', // w - write and a - append
498
500
  encoding: null, // use null for binary files
499
501
  mode: 0o666, // mode to use for created file (rwx)
500
- autoClose: true // automatically close the write stream when finished
501
502
  }}
502
503
  ```
503
504
 
504
505
  The most common options to use are mode and encoding. The values shown above are the defaults. You do not have to set encoding to utf-8 for text files, null is fine for all file types. However, using utf-8 encoding for binary files will often result in data corruption.
506
+
507
+ Note that you cannot set `autoClose: false` for `writeStreamOptions`. If you attempt to set this property to false, it will be ignored. This is necessary to avoid a race condition which may exist when setting `autoClose` to false on the writeStream. As there is no easy way to access the writeStream once the promise has been resolved, setting this to autoClose false is not terribly useful as there is no easy way to manually close the stream after the promise has been resolved.
505
508
 
506
509
  2. Example Use
507
510
 
@@ -1356,3 +1359,5 @@ Thanks to the following for their contributions -
1356
1359
  - **anton-erofeev:** Documentation fix
1357
1360
  - **Ladislav Jacho:** Contributed solution explanation for connections hanging when transferring larger files.
1358
1361
  - **Emma Milner:** Contributed fix for put() bug
1362
+ - **Witni Davis:** Contributed PR to fix put() RCE when using 'finish' rather than 'close' to resolve promise
1363
+ - **Maik Marschner:** Contributed fix for connect() not returning sftp object. Also included test to check for this regression in future.
package/README.org CHANGED
@@ -9,9 +9,9 @@ convenience abstraction as well as a Promise based API.
9
9
  Documentation on the methods and available options in the underlying modules can
10
10
  be found on the [[https://github.com/mscdex/ssh2][SSH2]] project pages.
11
11
 
12
- Current stable release is *v7.0.3*.
12
+ Current stable release is *v7.2.1*.
13
13
 
14
- Code has been tested against Node versions 12.22.1, 14.17.0 and 16.2.0
14
+ Code has been tested against Node versions 14.18.2, 16.13.1 and 17.2.0
15
15
 
16
16
  Node versions < 10.x are not supported.
17
17
 
@@ -62,6 +62,14 @@ npm install ssh2-sftp-client
62
62
  currently using options to either ~put()~ or ~get()~, you will need to update
63
63
  your code to map these options to the new structure.
64
64
 
65
+ - *Breaking Change 7.1.0* A race condition was identified when using a put()
66
+ call with a writeStream option of ~autoClose: false~. In some situations, the
67
+ promise would be resolved before the final close of the write stream. This
68
+ could result in errors if you immediately attempt to access the uploaded
69
+ file. To avoid this situatioin, the promise is now resolved once a ~close~
70
+ event is emitted. This means that setting ~autoClose: false~ can no longer be
71
+ supported. The write stream for ~put()~ will autoClose once data writing has completed.
72
+
65
73
  - Improved event handling. A listener for a global error event is now defined to
66
74
  catch errors which occur in-between method calls i.e. connection lost
67
75
  in-between calls to the library methods. A new mechanism has also been added
@@ -546,7 +554,6 @@ option value. For example, you might use the following to set ~writeStream~ opti
546
554
  flags: 'w', // w - write and a - append
547
555
  encoding: null, // use null for binary files
548
556
  mode: 0o666, // mode to use for created file (rwx)
549
- autoClose: true // automatically close the write stream when finished
550
557
  }}
551
558
  #+end_src
552
559
 
@@ -555,6 +562,14 @@ the defaults. You do not have to set encoding to utf-8 for text files, null is
555
562
  fine for all file types. However, using utf-8 encoding for binary files will
556
563
  often result in data corruption.
557
564
 
565
+ Note that you cannot set ~autoClose: false~ for ~writeStreamOptions~. If you
566
+ attempt to set this property to false, it will be ignored. This is necessary to
567
+ avoid a race condition which may exist when setting ~autoClose~ to false on the
568
+ writeStream. As there is no easy way to access the writeStream once the promise
569
+ has been resolved, setting this to autoClose false is not terribly useful as
570
+ there is no easy way to manually close the stream after the promise has been
571
+ resolved.
572
+
558
573
  **** Example Use
559
574
 
560
575
  #+begin_src javascript
@@ -1765,3 +1780,7 @@ Thanks to the following for their contributions -
1765
1780
  - Ladislav Jacho :: Contributed solution explanation for connections hanging
1766
1781
  when transferring larger files.
1767
1782
  - Emma Milner :: Contributed fix for put() bug
1783
+ - Witni Davis :: Contributed PR to fix put() RCE when using 'finish' rather than
1784
+ 'close' to resolve promise
1785
+ - Maik Marschner :: Contributed fix for connect() not returning sftp object.
1786
+ Also included test to check for this regression in future.
package/package.json CHANGED
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "name": "ssh2-sftp-client",
3
- "version": "7.0.3",
3
+ "version": "7.2.1",
4
4
  "description": "ssh2 sftp client for node",
5
5
  "main": "src/index.js",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/theophilusx/ssh2-sftp-client"
9
9
  },
10
- "keywords": ["sftp", "nodejs", "promises"],
10
+ "keywords": [
11
+ "sftp",
12
+ "nodejs",
13
+ "promises"
14
+ ],
11
15
  "scripts": {
12
16
  "test": "mocha",
13
17
  "coverage": "nyc npm run test",
14
- "lint": "eslint \"src/**/*.js\" --quiet"
18
+ "lint": "eslint \"src/**/*.js\" \"test/**/*.js\""
19
+ },
20
+ "engines": {
21
+ "node": ">=10.24.1"
15
22
  },
16
23
  "author": "Tim Cross",
17
24
  "email": "theophilusx@gmail.com",
@@ -25,17 +32,24 @@
25
32
  "dependencies": {
26
33
  "concat-stream": "^2.0.0",
27
34
  "promise-retry": "^2.0.1",
28
- "ssh2": "^1.2.0"
35
+ "ssh2": "^1.5.0"
29
36
  },
30
37
  "devDependencies": {
31
- "chai": "^4.2.0",
38
+ "chai": "^4.3.4",
32
39
  "chai-as-promised": "^7.1.1",
33
40
  "chai-subset": "^1.6.0",
34
41
  "checksum": "^1.0.0",
35
42
  "dotenv": "^10.0.0",
36
- "mocha": "^9.0.2",
43
+ "eslint": "^8.5.0",
44
+ "eslint-config-prettier": "^8.3.0",
45
+ "eslint-plugin-mocha": "^10.0.3",
46
+ "eslint-plugin-node": "^11.1.0",
47
+ "eslint-plugin-promise": "^6.0.0",
48
+ "eslint-plugin-unicorn": "^39.0.0",
49
+ "mocha": "^9.1.2",
37
50
  "moment": "^2.29.1",
38
51
  "nyc": "^15.1.0",
52
+ "prettier": "^2.5.0",
39
53
  "through2": "^4.0.2",
40
54
  "winston": "^3.3.3"
41
55
  }
package/src/constants.js CHANGED
@@ -1,12 +1,10 @@
1
- 'use strict';
2
-
3
1
  const errorCode = {
4
2
  generic: 'ERR_GENERIC_CLIENT',
5
3
  connect: 'ERR_NOT_CONNECTED',
6
4
  badPath: 'ERR_BAD_PATH',
7
5
  permission: 'EACCES',
8
6
  notexist: 'ENOENT',
9
- notdir: 'ENOTDIR'
7
+ notdir: 'ENOTDIR',
10
8
  };
11
9
 
12
10
  const targetType = {
@@ -15,10 +13,10 @@ const targetType = {
15
13
  writeDir: 3,
16
14
  readDir: 4,
17
15
  readObj: 5,
18
- writeObj: 6
16
+ writeObj: 6,
19
17
  };
20
18
 
21
19
  module.exports = {
22
20
  errorCode,
23
- targetType
21
+ targetType,
24
22
  };
package/src/index.js CHANGED
@@ -34,6 +34,7 @@ class SftpClient {
34
34
  this.remotePathSep = '/';
35
35
  this.remotePlatform = 'unix';
36
36
  this.debug = undefined;
37
+ this.tempListeners = {};
37
38
 
38
39
  this.client.on('close', () => {
39
40
  if (this.endCalled || this.closeHandled) {
@@ -114,7 +115,7 @@ class SftpClient {
114
115
  *
115
116
  * @param {Object} config - an SFTP configuration object
116
117
  *
117
- * @return {Promise} which will resolve to an sftp client object
118
+ * @return {Promise<Object>} which will resolve to an sftp client object
118
119
  *
119
120
  */
120
121
  getConnection(config) {
@@ -133,13 +134,12 @@ class SftpClient {
133
134
  // .catch((err) => {
134
135
  // return Promise.reject(err);
135
136
  // })
136
- .finally(async (resp) => {
137
+ .finally(async () => {
137
138
  this.debugMsg('getConnection: finally clause fired');
138
139
  await sleep(500);
139
140
  this.removeListener('ready', doReady);
140
141
  removeTempListeners(this, 'getConnection');
141
142
  this._resetEventFlags();
142
- return resp;
143
143
  })
144
144
  );
145
145
  }
@@ -151,6 +151,7 @@ class SftpClient {
151
151
  this.client.sftp((err, sftp) => {
152
152
  if (err) {
153
153
  this.debugMsg(`getSftpChannel: SFTP Channel Error: ${err.message}`);
154
+ this.client.end();
154
155
  reject(fmtError(err, 'getSftpChannel', err.code));
155
156
  } else {
156
157
  this.debugMsg('getSftpChannel: SFTP channel established');
@@ -158,7 +159,7 @@ class SftpClient {
158
159
  resolve(sftp);
159
160
  }
160
161
  });
161
- }).finally((resp) => {
162
+ }).finally(() => {
162
163
  this.debugMsg('getSftpChannel: finally clause fired');
163
164
  removeTempListeners(this, 'getSftpChannel');
164
165
  this._resetEventFlags();
@@ -174,7 +175,7 @@ class SftpClient {
174
175
  *
175
176
  * @param {Object} config - an SFTP configuration object
176
177
  *
177
- * @return {Promise} which will resolve to an sftp client object
178
+ * @return {Promise<Object>} which will resolve to an sftp client object
178
179
  *
179
180
  */
180
181
  async connect(config) {
@@ -205,7 +206,7 @@ class SftpClient {
205
206
  minTimeout: config.retry_minTimeout || 1000,
206
207
  }
207
208
  );
208
- await this.getSftpChannel();
209
+ return this.getSftpChannel();
209
210
  } catch (err) {
210
211
  this.debugMsg(`connect: Error ${err.message}`);
211
212
  this._resetEventFlags();
@@ -222,7 +223,7 @@ class SftpClient {
222
223
  * Returns undefined if the path does not exists.
223
224
  *
224
225
  * @param {String} remotePath - remote path, may be relative
225
- * @returns {Promise} - remote absolute path or undefined
226
+ * @returns {Promise<String>} - remote absolute path or ''
226
227
  */
227
228
  realPath(remotePath) {
228
229
  return new Promise((resolve, reject) => {
@@ -244,13 +245,19 @@ class SftpClient {
244
245
  resolve(absPath);
245
246
  });
246
247
  }
247
- }).finally((rsp) => {
248
+ }).finally(() => {
248
249
  removeTempListeners(this, 'realPath');
249
250
  this._resetEventFlags();
250
- return rsp;
251
251
  });
252
252
  }
253
253
 
254
+ /**
255
+ * @async
256
+ *
257
+ * Return the current workding directory path
258
+ *
259
+ * @returns {Promise<String>} - current remote working directory
260
+ */
254
261
  cwd() {
255
262
  return this.realPath('.');
256
263
  }
@@ -259,7 +266,7 @@ class SftpClient {
259
266
  * Retrieves attributes for path
260
267
  *
261
268
  * @param {String} remotePath - a string containing the path to a file
262
- * @return {Promise} stats - attributes info
269
+ * @return {Promise<Object>} stats - attributes info
263
270
  */
264
271
  async stat(remotePath) {
265
272
  const _stat = (aPath) => {
@@ -302,9 +309,8 @@ class SftpClient {
302
309
  resolve(result);
303
310
  }
304
311
  });
305
- }).finally((rsp) => {
306
- removeTempListeners(this, 'stat');
307
- return rsp;
312
+ }).finally(() => {
313
+ removeTempListeners(this, '_stat');
308
314
  });
309
315
  };
310
316
 
@@ -326,7 +332,7 @@ class SftpClient {
326
332
  *
327
333
  * @param {string} remotePath - path to the object on the sftp server.
328
334
  *
329
- * @return {Promise} returns false if object does not exist. Returns type of
335
+ * @return {Promise<Boolean|String>} returns false if object does not exist. Returns type of
330
336
  * object if it does
331
337
  */
332
338
  async exists(remotePath) {
@@ -385,8 +391,7 @@ class SftpClient {
385
391
  *
386
392
  * @param {String} remotePath - path to remote directory
387
393
  * @param {RegExp} pattern - regular expression to match filenames
388
- * @returns {Promise} array of file description objects
389
- * @throws {Error}
394
+ * @returns {Promise<Array>} array of file description objects
390
395
  */
391
396
  list(remotePath, pattern = /.*/) {
392
397
  return new Promise((resolve, reject) => {
@@ -404,15 +409,15 @@ class SftpClient {
404
409
  if (fileList) {
405
410
  newList = fileList.map((item) => {
406
411
  return {
407
- type: item.longname.substr(0, 1),
412
+ type: item.longname.slice(0, 1),
408
413
  name: item.filename,
409
414
  size: item.attrs.size,
410
415
  modifyTime: item.attrs.mtime * 1000,
411
416
  accessTime: item.attrs.atime * 1000,
412
417
  rights: {
413
- user: item.longname.substr(1, 3).replace(reg, ''),
414
- group: item.longname.substr(4, 3).replace(reg, ''),
415
- other: item.longname.substr(7, 3).replace(reg, ''),
418
+ user: item.longname.slice(1, 4).replace(reg, ''),
419
+ group: item.longname.slice(4, 7).replace(reg, ''),
420
+ other: item.longname.slice(7, 10).replace(reg, ''),
416
421
  },
417
422
  owner: item.attrs.uid,
418
423
  group: item.attrs.gid,
@@ -433,10 +438,9 @@ class SftpClient {
433
438
  }
434
439
  });
435
440
  }
436
- }).finally((rsp) => {
441
+ }).finally(() => {
437
442
  removeTempListeners(this, 'list');
438
443
  this._resetEventFlags();
439
- return rsp;
440
444
  });
441
445
  }
442
446
 
@@ -453,7 +457,7 @@ class SftpClient {
453
457
  * @param {Object} options - options object with supported properties of readStreamOptions,
454
458
  * writeStreamOptions and pipeOptions.
455
459
  *
456
- * @return {Promise}
460
+ * @return {Promise<String|Stream|Buffer>}
457
461
  */
458
462
  get(
459
463
  remotePath,
@@ -511,35 +515,52 @@ class SftpClient {
511
515
  )
512
516
  );
513
517
  });
514
- rdr.once('end', () => {
515
- if (typeof dst === 'string') {
516
- resolve(dst);
517
- } else {
518
- resolve(wtr);
519
- }
520
- });
518
+ if (
519
+ Object.hasOwnProperty.call(options, 'pipeOptions') &&
520
+ Object.hasOwnProperty.call(options.pipeOptions, 'end') &&
521
+ !options.pipeOptions.end
522
+ ) {
523
+ rdr.once('end', () => {
524
+ this.debugMsg('get resolved on reader end event');
525
+ if (typeof dst === 'string') {
526
+ resolve(dst);
527
+ } else {
528
+ resolve(wtr);
529
+ }
530
+ });
531
+ } else {
532
+ wtr.once('finish', () => {
533
+ this.debugMsg('get resolved on writer finish event');
534
+ if (typeof dst === 'string') {
535
+ resolve(dst);
536
+ } else {
537
+ resolve(wtr);
538
+ }
539
+ });
540
+ }
521
541
  }
522
542
  rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
523
543
  }
524
- }).finally((rsp) => {
544
+ }).finally(() => {
525
545
  removeTempListeners(this, 'get');
526
546
  this._resetEventFlags();
527
547
  if (
528
548
  rdr &&
529
- options.readStreamOptions &&
549
+ Object.hasOwnProperty.call(options, 'readStreamOptions') &&
550
+ Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
530
551
  options.readStreamOptions.autoClose === false
531
552
  ) {
532
553
  rdr.destroy();
533
554
  }
534
555
  if (
535
556
  wtr &&
536
- options.writeStreamOptions &&
557
+ Object.hasOwnProperty.call(options, 'writeStreamOptions') &&
558
+ Object.hasOwnProperty.call(options.writeStreamOptions, 'autoClose') &&
537
559
  options.writeStreamOptions.autoClose === false &&
538
560
  typeof dst === 'string'
539
561
  ) {
540
562
  wtr.destroy();
541
563
  }
542
- return rsp;
543
564
  });
544
565
  }
545
566
 
@@ -548,13 +569,10 @@ class SftpClient {
548
569
  * Downloads a file at remotePath to localPath using parallel reads
549
570
  * for faster throughput.
550
571
  *
551
- * See 'fastGet' at
552
- * https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md
553
- *
554
572
  * @param {String} remotePath
555
573
  * @param {String} localPath
556
574
  * @param {Object} options
557
- * @return {Promise} the result of downloading the file
575
+ * @return {Promise<String>} the result of downloading the file
558
576
  */
559
577
  async fastGet(remotePath, localPath, options) {
560
578
  try {
@@ -574,7 +592,7 @@ class SftpClient {
574
592
  err.code = errorCode.badPath;
575
593
  throw err;
576
594
  }
577
- await new Promise((resolve, reject) => {
595
+ let rslt = await new Promise((resolve, reject) => {
578
596
  if (haveConnection(this, 'fastGet', reject)) {
579
597
  this.debugMsg(
580
598
  `fastGet -> remote: ${remotePath} local: ${localPath} `,
@@ -589,10 +607,10 @@ class SftpClient {
589
607
  resolve(`${remotePath} was successfully download to ${localPath}!`);
590
608
  });
591
609
  }
592
- }).finally((rsp) => {
610
+ }).finally(() => {
593
611
  removeTempListeners(this, 'fastGet');
594
- return rsp;
595
612
  });
613
+ return rslt;
596
614
  } catch (err) {
597
615
  this._resetEventFlags();
598
616
  throw fmtError(err, 'fastGet');
@@ -610,7 +628,7 @@ class SftpClient {
610
628
  * @param {String} localPath
611
629
  * @param {String} remotePath
612
630
  * @param {Object} options
613
- * @return {Promise} the result of downloading the file
631
+ * @return {Promise<String>} the result of downloading the file
614
632
  */
615
633
  fastPut(localPath, remotePath, options) {
616
634
  this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
@@ -654,10 +672,9 @@ class SftpClient {
654
672
  resolve(`${localPath} was successfully uploaded to ${remotePath}!`);
655
673
  });
656
674
  }
657
- }).finally((rsp) => {
675
+ }).finally(() => {
658
676
  removeTempListeners(this, 'fastPut');
659
677
  this._resetEventFlags();
660
- return rsp;
661
678
  });
662
679
  }
663
680
 
@@ -671,12 +688,16 @@ class SftpClient {
671
688
  * @param {Object} options - options used for read, write stream and pipe configuration
672
689
  * value supported by node. Allowed properties are readStreamOptions,
673
690
  * writeStreamOptions and pipeOptions.
674
- * @return {Promise}
691
+ * @return {Promise<String>}
675
692
  */
676
693
  put(
677
694
  localSrc,
678
695
  remotePath,
679
- options = { readStreamOptions: {}, writeStreamOptions: {}, pipeOptions: {} }
696
+ options = {
697
+ readStreamOptions: {},
698
+ writeStreamOptions: { autoClose: true },
699
+ pipeOptions: {},
700
+ }
680
701
  ) {
681
702
  let wtr, rdr;
682
703
 
@@ -698,13 +719,15 @@ class SftpClient {
698
719
  addTempListeners(this, 'put', reject);
699
720
  wtr = this.sftp.createWriteStream(
700
721
  remotePath,
701
- options.writeStreamOptions ? options.writeStreamOptions : {}
722
+ options.writeStreamOptions
723
+ ? { ...options.writeStreamOptions, autoClose: true }
724
+ : {}
702
725
  );
703
726
  wtr.once('error', (err) => {
704
727
  this.debugMsg(`put: write stream error ${err.message}`);
705
728
  reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
706
729
  });
707
- wtr.once('finish', () => {
730
+ wtr.once('close', () => {
708
731
  this.debugMsg('put: promise resolved');
709
732
  resolve(`Uploaded data stream to ${remotePath}`);
710
733
  });
@@ -737,25 +760,18 @@ class SftpClient {
737
760
  rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
738
761
  }
739
762
  }
740
- }).finally((resp) => {
763
+ }).finally(() => {
741
764
  removeTempListeners(this, 'put');
742
765
  this._resetEventFlags();
743
766
  if (
744
767
  rdr &&
745
- options.readStreamOptions &&
768
+ Object.hasOwnProperty.call(options, 'readStreamOptions') &&
769
+ Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
746
770
  options.readStreamOptions.autoClose === false &&
747
771
  typeof localSrc === 'string'
748
772
  ) {
749
773
  rdr.destroy();
750
774
  }
751
- if (
752
- wtr &&
753
- options.writeStreamOptions &&
754
- options.writeStreamOptions.autoClose === false
755
- ) {
756
- wtr.destroy();
757
- }
758
- return resp;
759
775
  });
760
776
  }
761
777
 
@@ -765,49 +781,45 @@ class SftpClient {
765
781
  * @param {Buffer|stream} input
766
782
  * @param {String} remotePath
767
783
  * @param {Object} options
768
- * @return {Promise}
784
+ * @return {Promise<String>}
769
785
  */
770
- append(input, remotePath, options = {}) {
771
- return this.exists(remotePath).then((fileType) => {
772
- if (fileType && fileType === 'd') {
773
- return Promise.reject(
774
- fmtError(
775
- `Bad path: ${remotePath}: cannot append to a directory`,
776
- 'append',
777
- errorCode.badPath
778
- )
779
- );
780
- }
781
- return new Promise((resolve, reject) => {
782
- if (haveConnection(this, 'append', reject)) {
783
- if (typeof input === 'string') {
784
- reject(fmtError('Cannot append one file to another', 'append'));
786
+ async append(input, remotePath, options = {}) {
787
+ const fileType = await this.exists(remotePath);
788
+ if (fileType && fileType === 'd') {
789
+ throw fmtError(
790
+ `Bad path: ${remotePath}: cannot append to a directory`,
791
+ 'append',
792
+ errorCode.badPath
793
+ );
794
+ }
795
+ return await new Promise((resolve, reject) => {
796
+ if (haveConnection(this, 'append', reject)) {
797
+ if (typeof input === 'string') {
798
+ reject(fmtError('Cannot append one file to another', 'append'));
799
+ } else {
800
+ this.debugMsg(`append -> remote: ${remotePath} `, options);
801
+ addTempListeners(this, 'append', reject);
802
+ options.flags = 'a';
803
+ let stream = this.sftp.createWriteStream(remotePath, options);
804
+ stream.on('error', (err_1) => {
805
+ reject(
806
+ fmtError(`${err_1.message} ${remotePath}`, 'append', err_1.code)
807
+ );
808
+ });
809
+ stream.on('finish', () => {
810
+ resolve(`Appended data to ${remotePath}`);
811
+ });
812
+ if (input instanceof Buffer) {
813
+ stream.write(input);
814
+ stream.end();
785
815
  } else {
786
- this.debugMsg(`append -> remote: ${remotePath} `, options);
787
- addTempListeners(this, 'append', reject);
788
- options.flags = 'a';
789
- let stream = this.sftp.createWriteStream(remotePath, options);
790
- stream.on('error', (err) => {
791
- reject(
792
- fmtError(`${err.message} ${remotePath}`, 'append', err.code)
793
- );
794
- });
795
- stream.on('finish', () => {
796
- resolve(`Appended data to ${remotePath}`);
797
- });
798
- if (input instanceof Buffer) {
799
- stream.write(input);
800
- stream.end();
801
- } else {
802
- input.pipe(stream);
803
- }
816
+ input.pipe(stream);
804
817
  }
805
818
  }
806
- }).finally((rsp) => {
807
- removeTempListeners(this, 'append');
808
- this._resetEventFlags();
809
- return rsp;
810
- });
819
+ }
820
+ }).finally(() => {
821
+ removeTempListeners(this, 'append');
822
+ this._resetEventFlags();
811
823
  });
812
824
  }
813
825
 
@@ -818,7 +830,7 @@ class SftpClient {
818
830
  *
819
831
  * @param {string} remotePath - remote directory path.
820
832
  * @param {boolean} recursive - if true, recursively create directories
821
- * @return {Promise}
833
+ * @return {Promise<String>}
822
834
  */
823
835
  async mkdir(remotePath, recursive = false) {
824
836
  const _mkdir = (p) => {
@@ -847,16 +859,23 @@ class SftpClient {
847
859
  resolve(`${p} directory created`);
848
860
  }
849
861
  });
850
- }).finally((rsp) => {
862
+ }).finally(() => {
851
863
  removeTempListeners(this, '_mkdir');
852
864
  this._resetEventFlags();
853
- return rsp;
854
865
  });
855
866
  };
856
867
 
857
868
  try {
858
869
  haveConnection(this, 'mkdir');
859
870
  let rPath = await normalizeRemotePath(this, remotePath);
871
+ let targetExists = await this.exists(rPath);
872
+ if (targetExists && targetExists !== 'd') {
873
+ let error = new Error(`Bad path: ${rPath} already exists as a file`);
874
+ error.code = errorCode.badPath;
875
+ throw error;
876
+ } else if (targetExists) {
877
+ return `${rPath} already exists`;
878
+ }
860
879
  if (!recursive) {
861
880
  return await _mkdir(rPath);
862
881
  }
@@ -885,7 +904,7 @@ class SftpClient {
885
904
  * @param {string} remotePath - path to directory to be removed
886
905
  * @param {boolean} recursive - if true, remove directories/files in target
887
906
  * directory
888
- * @return {Promise}
907
+ * @return {Promise<String>}
889
908
  */
890
909
  async rmdir(remotePath, recursive = false) {
891
910
  const _rmdir = (p) => {
@@ -899,9 +918,8 @@ class SftpClient {
899
918
  }
900
919
  resolve('Successfully removed directory');
901
920
  });
902
- }).finally((rsp) => {
921
+ }).finally(() => {
903
922
  removeTempListeners(this, 'rmdir');
904
- return rsp;
905
923
  });
906
924
  };
907
925
 
@@ -939,7 +957,7 @@ class SftpClient {
939
957
  * @param {string} remotePath - path to the file to delete
940
958
  * @param {boolean} notFoundOK - if true, ignore errors for missing target.
941
959
  * Default is false.
942
- * @return {Promise} with string 'Successfully deleted file' once resolved
960
+ * @return {Promise<String>} with string 'Successfully deleted file' once resolved
943
961
  *
944
962
  */
945
963
  delete(remotePath, notFoundOK = false) {
@@ -962,10 +980,9 @@ class SftpClient {
962
980
  resolve(`Successfully deleted ${remotePath}`);
963
981
  });
964
982
  }
965
- }).finally((rsp) => {
983
+ }).finally(() => {
966
984
  removeTempListeners(this, 'delete');
967
985
  this._resetEventFlags();
968
- return rsp;
969
986
  });
970
987
  }
971
988
 
@@ -977,7 +994,7 @@ class SftpClient {
977
994
  * @param {string} fromPath - path to the file to be renamed.
978
995
  * @param {string} toPath - path to the new name.
979
996
  *
980
- * @return {Promise}
997
+ * @return {Promise<String>}
981
998
  *
982
999
  */
983
1000
  rename(fromPath, toPath) {
@@ -999,10 +1016,9 @@ class SftpClient {
999
1016
  resolve(`Successfully renamed ${fromPath} to ${toPath}`);
1000
1017
  });
1001
1018
  }
1002
- }).finally((rsp) => {
1019
+ }).finally(() => {
1003
1020
  removeTempListeners(this, 'rename');
1004
1021
  this._resetEventFlags();
1005
- return rsp;
1006
1022
  });
1007
1023
  }
1008
1024
 
@@ -1015,7 +1031,7 @@ class SftpClient {
1015
1031
  * @param {string} fromPath - path to the file to be renamed.
1016
1032
  * @param {string} toPath - path the new name.
1017
1033
  *
1018
- * @return {Promise}
1034
+ * @return {Promise<String>}
1019
1035
  *
1020
1036
  */
1021
1037
  posixRename(fromPath, toPath) {
@@ -1037,10 +1053,9 @@ class SftpClient {
1037
1053
  resolve(`Successful POSIX rename ${fromPath} to ${toPath}`);
1038
1054
  });
1039
1055
  }
1040
- }).finally((rsp) => {
1056
+ }).finally(() => {
1041
1057
  removeTempListeners(this, 'posixRename');
1042
1058
  this._resetEventFlags();
1043
- return rsp;
1044
1059
  });
1045
1060
  }
1046
1061
 
@@ -1052,7 +1067,7 @@ class SftpClient {
1052
1067
  * @param {string} remotePath - path to the remote target object.
1053
1068
  * @param {number | string} mode - the new octal mode to set
1054
1069
  *
1055
- * @return {Promise}
1070
+ * @return {Promise<String>}
1056
1071
  */
1057
1072
  chmod(remotePath, mode) {
1058
1073
  return new Promise((resolve, reject) => {
@@ -1064,10 +1079,9 @@ class SftpClient {
1064
1079
  }
1065
1080
  resolve('Successfully change file mode');
1066
1081
  });
1067
- }).finally((rsp) => {
1082
+ }).finally(() => {
1068
1083
  removeTempListeners(this, 'chmod');
1069
1084
  this._resetEventFlags();
1070
- return rsp;
1071
1085
  });
1072
1086
  }
1073
1087
 
@@ -1081,8 +1095,7 @@ class SftpClient {
1081
1095
  * @param {String} dstDir - remote destination directory
1082
1096
  * @param {RegExp} filter - (Optional) a regular expression used to select
1083
1097
  * files and directories to upload
1084
- * @returns {String}
1085
- * @throws {Error}
1098
+ * @returns {Promise<String>}
1086
1099
  */
1087
1100
  async uploadDir(srcDir, dstDir, filter = /.*/) {
1088
1101
  try {
@@ -1141,8 +1154,7 @@ class SftpClient {
1141
1154
  * @param {String} dstDir - local destination directory
1142
1155
  * @param {RegExp} filter - (Optional) a regular expression used to select
1143
1156
  * files and directories to upload
1144
- * @returns {Promise}
1145
- * @throws {Error}
1157
+ * @returns {Promise<String>}
1146
1158
  */
1147
1159
  async downloadDir(srcDir, dstDir, filter = /.*/) {
1148
1160
  try {
@@ -1193,6 +1205,7 @@ class SftpClient {
1193
1205
  *
1194
1206
  * End the SFTP connection
1195
1207
  *
1208
+ * @returns {Promise<Boolean>}
1196
1209
  */
1197
1210
  end() {
1198
1211
  let endCloseHandler;
@@ -1209,13 +1222,12 @@ class SftpClient {
1209
1222
  this.debugMsg('end: Have connection - calling end()');
1210
1223
  this.client.end();
1211
1224
  }
1212
- }).finally((resp) => {
1225
+ }).finally(() => {
1213
1226
  this.debugMsg('end: finally clause fired');
1214
1227
  removeTempListeners(this, 'end');
1215
1228
  this.removeListener('close', endCloseHandler);
1216
1229
  this.endCalled = false;
1217
1230
  this._resetEventFlags();
1218
- return resp;
1219
1231
  });
1220
1232
  }
1221
1233
  }
package/src/utils.js CHANGED
@@ -58,7 +58,13 @@ function fmtError(err, name = 'sftp', eCode, retryCount) {
58
58
  return newError;
59
59
  }
60
60
 
61
- let tempListeners = [];
61
+ function addToTempListenerList(obj, name, evt, fn) {
62
+ if (name in obj.tempListeners) {
63
+ obj.tempListeners[name].push([evt, fn]);
64
+ } else {
65
+ obj.tempListeners[name] = [[evt, fn]];
66
+ }
67
+ }
62
68
 
63
69
  /**
64
70
  * Simple default error listener. Will reformat the error message and
@@ -83,7 +89,7 @@ function errorListener(client, name, reject) {
83
89
  }
84
90
  }
85
91
  };
86
- tempListeners.push(['error', fn]);
92
+ addToTempListenerList(client, name, 'error', fn);
87
93
  return fn;
88
94
  }
89
95
 
@@ -104,7 +110,7 @@ function endListener(client, name, reject) {
104
110
  }
105
111
  }
106
112
  };
107
- tempListeners.push(['end', fn]);
113
+ addToTempListenerList(client, name, 'end', fn);
108
114
  return fn;
109
115
  }
110
116
 
@@ -125,7 +131,7 @@ function closeListener(client, name, reject) {
125
131
  }
126
132
  }
127
133
  };
128
- tempListeners.push(['close', fn]);
134
+ addToTempListenerList(client, name, 'close', fn);
129
135
  return fn;
130
136
  }
131
137
 
@@ -138,10 +144,12 @@ function addTempListeners(obj, name, reject) {
138
144
 
139
145
  function removeTempListeners(obj, name) {
140
146
  obj.debugMsg(`${name}: Removing temp event listeners`);
141
- tempListeners.forEach(([e, fn]) => {
142
- obj.client.removeListener(e, fn);
143
- });
144
- tempListeners = [];
147
+ if (name in obj.tempListeners) {
148
+ obj.tempListeners[name].forEach(([e, fn]) => {
149
+ obj.client.removeListener(e, fn);
150
+ });
151
+ obj.tempListeners = [];
152
+ }
145
153
  }
146
154
 
147
155
  /**
@@ -203,29 +211,33 @@ function haveLocalAccess(filePath, mode = 'r') {
203
211
  code: 0,
204
212
  };
205
213
  } catch (err) {
206
- if (err.errno === -2) {
207
- return {
208
- status: false,
209
- type: null,
210
- details: 'not exist',
211
- code: -2,
212
- };
213
- } else if (err.errno === -13) {
214
- const type = localExists(filePath);
215
- return {
216
- status: false,
217
- type: type,
218
- details: 'permission denied',
219
- code: -13,
220
- };
221
- } else if (err.errno === -20) {
222
- return {
223
- status: false,
224
- type: null,
225
- details: 'parent not a directory',
226
- };
227
- } else {
228
- throw err;
214
+ switch (err.errno) {
215
+ case -2:
216
+ return {
217
+ status: false,
218
+ type: null,
219
+ details: 'not exist',
220
+ code: -2,
221
+ };
222
+ case -13:
223
+ return {
224
+ status: false,
225
+ type: localExists(filePath),
226
+ details: 'permission denied',
227
+ code: -13,
228
+ };
229
+ case -20:
230
+ return {
231
+ status: false,
232
+ type: null,
233
+ details: 'parent not a directory',
234
+ };
235
+ default:
236
+ return {
237
+ status: false,
238
+ type: null,
239
+ details: err.message,
240
+ };
229
241
  }
230
242
  }
231
243
  }
@@ -281,10 +293,10 @@ async function normalizeRemotePath(client, aPath) {
281
293
  try {
282
294
  if (aPath.startsWith('..')) {
283
295
  let root = await client.realPath('..');
284
- return root + client.remotePathSep + aPath.substring(3);
296
+ return root + client.remotePathSep + aPath.slice(3);
285
297
  } else if (aPath.startsWith('.')) {
286
298
  let root = await client.realPath('.');
287
- return root + client.remotePathSep + aPath.substring(2);
299
+ return root + client.remotePathSep + aPath.slice(2);
288
300
  }
289
301
  return aPath;
290
302
  } catch (err) {
@@ -333,6 +345,7 @@ function sleep(ms) {
333
345
 
334
346
  module.exports = {
335
347
  fmtError,
348
+ addToTempListenerList,
336
349
  errorListener,
337
350
  endListener,
338
351
  closeListener,