ssh2-sftp-client 7.0.2 → 7.2.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/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.2**.
64
+ Current stable release is **v7.2.0**.
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
 
@@ -1355,3 +1358,6 @@ Thanks to the following for their contributions -
1355
1358
  - **kennylbj:** Contributed example of using a throttle stream to limit upload/download bandwidth.
1356
1359
  - **anton-erofeev:** Documentation fix
1357
1360
  - **Ladislav Jacho:** Contributed solution explanation for connections hanging when transferring larger files.
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.2*.
12
+ Current stable release is *v7.2.0*.
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
@@ -1764,3 +1779,8 @@ Thanks to the following for their contributions -
1764
1779
  - anton-erofeev :: Documentation fix
1765
1780
  - Ladislav Jacho :: Contributed solution explanation for connections hanging
1766
1781
  when transferring larger files.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh2-sftp-client",
3
- "version": "7.0.2",
3
+ "version": "7.2.0",
4
4
  "description": "ssh2 sftp client for node",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -11,7 +11,10 @@
11
11
  "scripts": {
12
12
  "test": "mocha",
13
13
  "coverage": "nyc npm run test",
14
- "lint": "eslint \"src/**/*.js\" --quiet"
14
+ "lint": "eslint \"src/**/*.js\" \"test/**/*.js\""
15
+ },
16
+ "engines": {
17
+ "node": ">=10.24.1"
15
18
  },
16
19
  "author": "Tim Cross",
17
20
  "email": "theophilusx@gmail.com",
@@ -25,17 +28,24 @@
25
28
  "dependencies": {
26
29
  "concat-stream": "^2.0.0",
27
30
  "promise-retry": "^2.0.1",
28
- "ssh2": "^1.2.0"
31
+ "ssh2": "^1.5.0"
29
32
  },
30
33
  "devDependencies": {
31
- "chai": "^4.2.0",
34
+ "chai": "^4.3.4",
32
35
  "chai-as-promised": "^7.1.1",
33
36
  "chai-subset": "^1.6.0",
34
37
  "checksum": "^1.0.0",
35
38
  "dotenv": "^10.0.0",
36
- "mocha": "^9.0.2",
39
+ "eslint": "^8.3.0",
40
+ "eslint-config-prettier": "^8.3.0",
41
+ "eslint-plugin-mocha": "^9.0.0",
42
+ "eslint-plugin-node": "^11.1.0",
43
+ "eslint-plugin-promise": "^5.2.0",
44
+ "eslint-plugin-unicorn": "^39.0.0",
45
+ "mocha": "^9.1.2",
37
46
  "moment": "^2.29.1",
38
47
  "nyc": "^15.1.0",
48
+ "prettier": "^2.5.0",
39
49
  "through2": "^4.0.2",
40
50
  "winston": "^3.3.3"
41
51
  }
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,
@@ -521,7 +525,7 @@ class SftpClient {
521
525
  }
522
526
  rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
523
527
  }
524
- }).finally((rsp) => {
528
+ }).finally(() => {
525
529
  removeTempListeners(this, 'get');
526
530
  this._resetEventFlags();
527
531
  if (
@@ -539,7 +543,6 @@ class SftpClient {
539
543
  ) {
540
544
  wtr.destroy();
541
545
  }
542
- return rsp;
543
546
  });
544
547
  }
545
548
 
@@ -548,13 +551,10 @@ class SftpClient {
548
551
  * Downloads a file at remotePath to localPath using parallel reads
549
552
  * for faster throughput.
550
553
  *
551
- * See 'fastGet' at
552
- * https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md
553
- *
554
554
  * @param {String} remotePath
555
555
  * @param {String} localPath
556
556
  * @param {Object} options
557
- * @return {Promise} the result of downloading the file
557
+ * @return {Promise<String>} the result of downloading the file
558
558
  */
559
559
  async fastGet(remotePath, localPath, options) {
560
560
  try {
@@ -574,7 +574,7 @@ class SftpClient {
574
574
  err.code = errorCode.badPath;
575
575
  throw err;
576
576
  }
577
- await new Promise((resolve, reject) => {
577
+ let rslt = await new Promise((resolve, reject) => {
578
578
  if (haveConnection(this, 'fastGet', reject)) {
579
579
  this.debugMsg(
580
580
  `fastGet -> remote: ${remotePath} local: ${localPath} `,
@@ -589,10 +589,10 @@ class SftpClient {
589
589
  resolve(`${remotePath} was successfully download to ${localPath}!`);
590
590
  });
591
591
  }
592
- }).finally((rsp) => {
592
+ }).finally(() => {
593
593
  removeTempListeners(this, 'fastGet');
594
- return rsp;
595
594
  });
595
+ return rslt;
596
596
  } catch (err) {
597
597
  this._resetEventFlags();
598
598
  throw fmtError(err, 'fastGet');
@@ -610,7 +610,7 @@ class SftpClient {
610
610
  * @param {String} localPath
611
611
  * @param {String} remotePath
612
612
  * @param {Object} options
613
- * @return {Promise} the result of downloading the file
613
+ * @return {Promise<String>} the result of downloading the file
614
614
  */
615
615
  fastPut(localPath, remotePath, options) {
616
616
  this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
@@ -654,10 +654,9 @@ class SftpClient {
654
654
  resolve(`${localPath} was successfully uploaded to ${remotePath}!`);
655
655
  });
656
656
  }
657
- }).finally((rsp) => {
657
+ }).finally(() => {
658
658
  removeTempListeners(this, 'fastPut');
659
659
  this._resetEventFlags();
660
- return rsp;
661
660
  });
662
661
  }
663
662
 
@@ -671,12 +670,16 @@ class SftpClient {
671
670
  * @param {Object} options - options used for read, write stream and pipe configuration
672
671
  * value supported by node. Allowed properties are readStreamOptions,
673
672
  * writeStreamOptions and pipeOptions.
674
- * @return {Promise}
673
+ * @return {Promise<String>}
675
674
  */
676
675
  put(
677
676
  localSrc,
678
677
  remotePath,
679
- options = { readStreamOptions: {}, writeStreamOptions: {}, pipeOptions: {} }
678
+ options = {
679
+ readStreamOptions: {},
680
+ writeStreamOptions: { autoClose: true },
681
+ pipeOptions: {},
682
+ }
680
683
  ) {
681
684
  let wtr, rdr;
682
685
 
@@ -698,13 +701,15 @@ class SftpClient {
698
701
  addTempListeners(this, 'put', reject);
699
702
  wtr = this.sftp.createWriteStream(
700
703
  remotePath,
701
- options.writeStreamOptions ? options.writeStreamOptions : {}
704
+ options.writeStreamOptions
705
+ ? { ...options.writeStreamOptions, autoClose: true }
706
+ : {}
702
707
  );
703
708
  wtr.once('error', (err) => {
704
709
  this.debugMsg(`put: write stream error ${err.message}`);
705
710
  reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
706
711
  });
707
- wtr.once('finish', () => {
712
+ wtr.once('close', () => {
708
713
  this.debugMsg('put: promise resolved');
709
714
  resolve(`Uploaded data stream to ${remotePath}`);
710
715
  });
@@ -716,7 +721,7 @@ class SftpClient {
716
721
  this.debugMsg(`put source is a file path: ${localSrc}`);
717
722
  rdr = fs.createReadStream(
718
723
  localSrc,
719
- options.readStreamOptions ? options.readStreamOptons : {}
724
+ options.readStreamOptions ? options.readStreamOptions : {}
720
725
  );
721
726
  } else {
722
727
  this.debugMsg('put source is a stream');
@@ -737,7 +742,7 @@ class SftpClient {
737
742
  rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
738
743
  }
739
744
  }
740
- }).finally((resp) => {
745
+ }).finally(() => {
741
746
  removeTempListeners(this, 'put');
742
747
  this._resetEventFlags();
743
748
  if (
@@ -748,14 +753,6 @@ class SftpClient {
748
753
  ) {
749
754
  rdr.destroy();
750
755
  }
751
- if (
752
- wtr &&
753
- options.writeStreamOptions &&
754
- options.writeStreamOptions.autoClose === false
755
- ) {
756
- wtr.destroy();
757
- }
758
- return resp;
759
756
  });
760
757
  }
761
758
 
@@ -765,49 +762,45 @@ class SftpClient {
765
762
  * @param {Buffer|stream} input
766
763
  * @param {String} remotePath
767
764
  * @param {Object} options
768
- * @return {Promise}
765
+ * @return {Promise<String>}
769
766
  */
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'));
767
+ async append(input, remotePath, options = {}) {
768
+ const fileType = await this.exists(remotePath);
769
+ if (fileType && fileType === 'd') {
770
+ throw fmtError(
771
+ `Bad path: ${remotePath}: cannot append to a directory`,
772
+ 'append',
773
+ errorCode.badPath
774
+ );
775
+ }
776
+ return await new Promise((resolve, reject) => {
777
+ if (haveConnection(this, 'append', reject)) {
778
+ if (typeof input === 'string') {
779
+ reject(fmtError('Cannot append one file to another', 'append'));
780
+ } else {
781
+ this.debugMsg(`append -> remote: ${remotePath} `, options);
782
+ addTempListeners(this, 'append', reject);
783
+ options.flags = 'a';
784
+ let stream = this.sftp.createWriteStream(remotePath, options);
785
+ stream.on('error', (err_1) => {
786
+ reject(
787
+ fmtError(`${err_1.message} ${remotePath}`, 'append', err_1.code)
788
+ );
789
+ });
790
+ stream.on('finish', () => {
791
+ resolve(`Appended data to ${remotePath}`);
792
+ });
793
+ if (input instanceof Buffer) {
794
+ stream.write(input);
795
+ stream.end();
785
796
  } 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
- }
797
+ input.pipe(stream);
804
798
  }
805
799
  }
806
- }).finally((rsp) => {
807
- removeTempListeners(this, 'append');
808
- this._resetEventFlags();
809
- return rsp;
810
- });
800
+ }
801
+ }).finally(() => {
802
+ removeTempListeners(this, 'append');
803
+ this._resetEventFlags();
811
804
  });
812
805
  }
813
806
 
@@ -818,7 +811,7 @@ class SftpClient {
818
811
  *
819
812
  * @param {string} remotePath - remote directory path.
820
813
  * @param {boolean} recursive - if true, recursively create directories
821
- * @return {Promise}
814
+ * @return {Promise<String>}
822
815
  */
823
816
  async mkdir(remotePath, recursive = false) {
824
817
  const _mkdir = (p) => {
@@ -847,16 +840,23 @@ class SftpClient {
847
840
  resolve(`${p} directory created`);
848
841
  }
849
842
  });
850
- }).finally((rsp) => {
843
+ }).finally(() => {
851
844
  removeTempListeners(this, '_mkdir');
852
845
  this._resetEventFlags();
853
- return rsp;
854
846
  });
855
847
  };
856
848
 
857
849
  try {
858
850
  haveConnection(this, 'mkdir');
859
851
  let rPath = await normalizeRemotePath(this, remotePath);
852
+ let targetExists = await this.exists(rPath);
853
+ if (targetExists && targetExists !== 'd') {
854
+ let error = new Error(`Bad path: ${rPath} already exists as a file`);
855
+ error.code = errorCode.badPath;
856
+ throw error;
857
+ } else if (targetExists) {
858
+ return `${rPath} already exists`;
859
+ }
860
860
  if (!recursive) {
861
861
  return await _mkdir(rPath);
862
862
  }
@@ -885,7 +885,7 @@ class SftpClient {
885
885
  * @param {string} remotePath - path to directory to be removed
886
886
  * @param {boolean} recursive - if true, remove directories/files in target
887
887
  * directory
888
- * @return {Promise}
888
+ * @return {Promise<String>}
889
889
  */
890
890
  async rmdir(remotePath, recursive = false) {
891
891
  const _rmdir = (p) => {
@@ -899,9 +899,8 @@ class SftpClient {
899
899
  }
900
900
  resolve('Successfully removed directory');
901
901
  });
902
- }).finally((rsp) => {
902
+ }).finally(() => {
903
903
  removeTempListeners(this, 'rmdir');
904
- return rsp;
905
904
  });
906
905
  };
907
906
 
@@ -939,7 +938,7 @@ class SftpClient {
939
938
  * @param {string} remotePath - path to the file to delete
940
939
  * @param {boolean} notFoundOK - if true, ignore errors for missing target.
941
940
  * Default is false.
942
- * @return {Promise} with string 'Successfully deleted file' once resolved
941
+ * @return {Promise<String>} with string 'Successfully deleted file' once resolved
943
942
  *
944
943
  */
945
944
  delete(remotePath, notFoundOK = false) {
@@ -962,10 +961,9 @@ class SftpClient {
962
961
  resolve(`Successfully deleted ${remotePath}`);
963
962
  });
964
963
  }
965
- }).finally((rsp) => {
964
+ }).finally(() => {
966
965
  removeTempListeners(this, 'delete');
967
966
  this._resetEventFlags();
968
- return rsp;
969
967
  });
970
968
  }
971
969
 
@@ -977,7 +975,7 @@ class SftpClient {
977
975
  * @param {string} fromPath - path to the file to be renamed.
978
976
  * @param {string} toPath - path to the new name.
979
977
  *
980
- * @return {Promise}
978
+ * @return {Promise<String>}
981
979
  *
982
980
  */
983
981
  rename(fromPath, toPath) {
@@ -999,10 +997,9 @@ class SftpClient {
999
997
  resolve(`Successfully renamed ${fromPath} to ${toPath}`);
1000
998
  });
1001
999
  }
1002
- }).finally((rsp) => {
1000
+ }).finally(() => {
1003
1001
  removeTempListeners(this, 'rename');
1004
1002
  this._resetEventFlags();
1005
- return rsp;
1006
1003
  });
1007
1004
  }
1008
1005
 
@@ -1015,7 +1012,7 @@ class SftpClient {
1015
1012
  * @param {string} fromPath - path to the file to be renamed.
1016
1013
  * @param {string} toPath - path the new name.
1017
1014
  *
1018
- * @return {Promise}
1015
+ * @return {Promise<String>}
1019
1016
  *
1020
1017
  */
1021
1018
  posixRename(fromPath, toPath) {
@@ -1037,10 +1034,9 @@ class SftpClient {
1037
1034
  resolve(`Successful POSIX rename ${fromPath} to ${toPath}`);
1038
1035
  });
1039
1036
  }
1040
- }).finally((rsp) => {
1037
+ }).finally(() => {
1041
1038
  removeTempListeners(this, 'posixRename');
1042
1039
  this._resetEventFlags();
1043
- return rsp;
1044
1040
  });
1045
1041
  }
1046
1042
 
@@ -1052,7 +1048,7 @@ class SftpClient {
1052
1048
  * @param {string} remotePath - path to the remote target object.
1053
1049
  * @param {number | string} mode - the new octal mode to set
1054
1050
  *
1055
- * @return {Promise}
1051
+ * @return {Promise<String>}
1056
1052
  */
1057
1053
  chmod(remotePath, mode) {
1058
1054
  return new Promise((resolve, reject) => {
@@ -1064,10 +1060,9 @@ class SftpClient {
1064
1060
  }
1065
1061
  resolve('Successfully change file mode');
1066
1062
  });
1067
- }).finally((rsp) => {
1063
+ }).finally(() => {
1068
1064
  removeTempListeners(this, 'chmod');
1069
1065
  this._resetEventFlags();
1070
- return rsp;
1071
1066
  });
1072
1067
  }
1073
1068
 
@@ -1081,8 +1076,7 @@ class SftpClient {
1081
1076
  * @param {String} dstDir - remote destination directory
1082
1077
  * @param {RegExp} filter - (Optional) a regular expression used to select
1083
1078
  * files and directories to upload
1084
- * @returns {String}
1085
- * @throws {Error}
1079
+ * @returns {Promise<String>}
1086
1080
  */
1087
1081
  async uploadDir(srcDir, dstDir, filter = /.*/) {
1088
1082
  try {
@@ -1141,8 +1135,7 @@ class SftpClient {
1141
1135
  * @param {String} dstDir - local destination directory
1142
1136
  * @param {RegExp} filter - (Optional) a regular expression used to select
1143
1137
  * files and directories to upload
1144
- * @returns {Promise}
1145
- * @throws {Error}
1138
+ * @returns {Promise<String>}
1146
1139
  */
1147
1140
  async downloadDir(srcDir, dstDir, filter = /.*/) {
1148
1141
  try {
@@ -1193,6 +1186,7 @@ class SftpClient {
1193
1186
  *
1194
1187
  * End the SFTP connection
1195
1188
  *
1189
+ * @returns {Promise<Boolean>}
1196
1190
  */
1197
1191
  end() {
1198
1192
  let endCloseHandler;
@@ -1209,13 +1203,12 @@ class SftpClient {
1209
1203
  this.debugMsg('end: Have connection - calling end()');
1210
1204
  this.client.end();
1211
1205
  }
1212
- }).finally((resp) => {
1206
+ }).finally(() => {
1213
1207
  this.debugMsg('end: finally clause fired');
1214
1208
  removeTempListeners(this, 'end');
1215
1209
  this.removeListener('close', endCloseHandler);
1216
1210
  this.endCalled = false;
1217
1211
  this._resetEventFlags();
1218
- return resp;
1219
1212
  });
1220
1213
  }
1221
1214
  }
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,