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 +9 -3
- package/README.org +23 -3
- package/package.json +15 -5
- package/src/constants.js +3 -5
- package/src/index.js +105 -112
- package/src/utils.js +46 -33
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
|
|
64
|
+
Current stable release is **v7.2.0**.
|
|
65
65
|
|
|
66
|
-
Code has been tested against Node versions
|
|
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
|
|
12
|
+
Current stable release is *v7.2.0*.
|
|
13
13
|
|
|
14
|
-
Code has been tested against Node versions
|
|
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
|
|
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\"
|
|
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.
|
|
31
|
+
"ssh2": "^1.5.0"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
|
-
"chai": "^4.
|
|
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
|
-
"
|
|
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 (
|
|
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((
|
|
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
|
-
|
|
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
|
|
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((
|
|
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((
|
|
306
|
-
removeTempListeners(this, '
|
|
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.
|
|
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.
|
|
414
|
-
group: item.longname.
|
|
415
|
-
other: item.longname.
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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 = {
|
|
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
|
|
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('
|
|
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.
|
|
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((
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
obj.
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
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.
|
|
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,
|