ssh2-sftp-client 11.0.0 → 12.0.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 +160 -153
- package/README.org +78 -70
- package/package.json +3 -4
- package/src/index.js +65 -112
- package/src/utils.js +2 -2
package/src/index.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
const { Client } = require('ssh2');
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const concat = require('concat-stream');
|
|
5
|
-
const promiseRetry = require('promise-retry');
|
|
6
5
|
const { join, parse } = require('node:path');
|
|
7
6
|
const {
|
|
8
7
|
globalListener,
|
|
@@ -26,7 +25,7 @@ class SftpClient {
|
|
|
26
25
|
close: () => console.log('Global close listener: close event raised'),
|
|
27
26
|
},
|
|
28
27
|
) {
|
|
29
|
-
this.version = '
|
|
28
|
+
this.version = '12.0.1';
|
|
30
29
|
this.client = new Client();
|
|
31
30
|
this.sftp = undefined;
|
|
32
31
|
this.clientName = clientName;
|
|
@@ -58,35 +57,32 @@ class SftpClient {
|
|
|
58
57
|
fmtError(err, name = 'sftp', eCode, retryCount) {
|
|
59
58
|
let msg = '';
|
|
60
59
|
let code = '';
|
|
61
|
-
const retry = retryCount
|
|
62
|
-
? ` after ${retryCount} ${retryCount > 1 ? 'attempts' : 'attempt'}`
|
|
63
|
-
: '';
|
|
64
60
|
|
|
65
61
|
if (err === undefined) {
|
|
66
62
|
msg = `${name}: Undefined error - probably a bug!`;
|
|
67
63
|
code = errorCode.generic;
|
|
68
64
|
} else if (typeof err === 'string') {
|
|
69
|
-
msg = `${name}: ${err}
|
|
65
|
+
msg = `${name}: ${err}`;
|
|
70
66
|
code = eCode || errorCode.generic;
|
|
71
67
|
} else if (err.custom) {
|
|
72
|
-
msg = `${name}->${err.message}
|
|
68
|
+
msg = `${name}->${err.message}`;
|
|
73
69
|
code = err.code;
|
|
74
70
|
} else {
|
|
75
71
|
switch (err.code) {
|
|
76
72
|
case 'ENOTFOUND': {
|
|
77
|
-
msg = `${name}: Address lookup failed for host
|
|
73
|
+
msg = `${name}: Address lookup failed for host`;
|
|
78
74
|
break;
|
|
79
75
|
}
|
|
80
76
|
case 'ECONNREFUSED': {
|
|
81
|
-
msg = `${name}: Remote host refused connection
|
|
77
|
+
msg = `${name}: Remote host refused connection`;
|
|
82
78
|
break;
|
|
83
79
|
}
|
|
84
80
|
case 'ECONNRESET': {
|
|
85
|
-
msg = `${name}: Remote host has reset the connection: ${err.message}
|
|
81
|
+
msg = `${name}: Remote host has reset the connection: ${err.message}`;
|
|
86
82
|
break;
|
|
87
83
|
}
|
|
88
84
|
default: {
|
|
89
|
-
msg = `${name}: ${err.message}
|
|
85
|
+
msg = `${name}: ${err.message}`;
|
|
90
86
|
}
|
|
91
87
|
}
|
|
92
88
|
code = err.code || errorCode.generic;
|
|
@@ -122,118 +118,61 @@ class SftpClient {
|
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
/**
|
|
125
|
-
* @async
|
|
126
121
|
*
|
|
127
|
-
* Create a new SFTP connection to a remote SFTP server
|
|
122
|
+
* Create a new SFTP connection to a remote SFTP server.
|
|
123
|
+
* The connection options are the same as those offered
|
|
124
|
+
* by the underlying SSH2 module.
|
|
128
125
|
*
|
|
129
126
|
* @param {Object} config - an SFTP configuration object
|
|
130
127
|
*
|
|
131
128
|
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
132
129
|
*/
|
|
133
|
-
|
|
130
|
+
connect(config) {
|
|
134
131
|
let doReady, listeners;
|
|
135
132
|
return new Promise((resolve, reject) => {
|
|
136
133
|
listeners = addTempListeners(this, 'getConnection', reject);
|
|
134
|
+
if (config.debug) {
|
|
135
|
+
this.debug = config.debug;
|
|
136
|
+
this.debugMsg('connect: Debugging turned on');
|
|
137
|
+
this.debugMsg(`ssh2-sftp-client Version: ${this.version} `, process.versions);
|
|
138
|
+
}
|
|
139
|
+
this.promiseLimit = config.promiseLimit ?? 10;
|
|
140
|
+
|
|
137
141
|
doReady = () => {
|
|
138
|
-
|
|
142
|
+
this.client.sftp((err, sftp) => {
|
|
143
|
+
if (err) {
|
|
144
|
+
reject(this.fmtError(err));
|
|
145
|
+
} else {
|
|
146
|
+
this.sftp = sftp;
|
|
147
|
+
resolve(sftp);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
139
150
|
};
|
|
140
151
|
this.on('ready', doReady);
|
|
152
|
+
|
|
141
153
|
try {
|
|
142
|
-
this.
|
|
154
|
+
if (this.sftp) {
|
|
155
|
+
reject(
|
|
156
|
+
this.fmtError(
|
|
157
|
+
'An existing SFTP connection is already defined',
|
|
158
|
+
'connect',
|
|
159
|
+
errorCode.connect,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
this.client.connect(config);
|
|
164
|
+
}
|
|
143
165
|
} catch (err) {
|
|
166
|
+
this.end();
|
|
144
167
|
reject(err);
|
|
145
168
|
}
|
|
146
169
|
}).finally(() => {
|
|
147
170
|
this.removeListener('ready', doReady);
|
|
148
171
|
removeTempListeners(this, listeners, 'getConnection');
|
|
172
|
+
this._resetEventFlags();
|
|
149
173
|
});
|
|
150
174
|
}
|
|
151
175
|
|
|
152
|
-
getSftpChannel() {
|
|
153
|
-
return new Promise((resolve, reject) => {
|
|
154
|
-
this.client.sftp((err, sftp) => {
|
|
155
|
-
if (err) {
|
|
156
|
-
reject(this.fmtError(err, 'getSftpChannel', err.code));
|
|
157
|
-
} else {
|
|
158
|
-
this.sftp = sftp;
|
|
159
|
-
resolve(sftp);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @async
|
|
167
|
-
*
|
|
168
|
-
* Create a new SFTP connection to a remote SFTP server.
|
|
169
|
-
* The connection options are the same as those offered
|
|
170
|
-
* by the underlying SSH2 module.
|
|
171
|
-
*
|
|
172
|
-
* @param {Object} config - an SFTP configuration object
|
|
173
|
-
*
|
|
174
|
-
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
175
|
-
*/
|
|
176
|
-
async connect(config) {
|
|
177
|
-
let listeners;
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
listeners = addTempListeners(this, 'connect');
|
|
181
|
-
if (config.debug) {
|
|
182
|
-
this.debug = config.debug;
|
|
183
|
-
this.debugMsg('connect: Debugging turned on');
|
|
184
|
-
this.debugMsg(`ssh2-sftp-client Version: ${this.version} `, process.versions);
|
|
185
|
-
}
|
|
186
|
-
this.promiseLimit = config.promiseLimit ?? 10;
|
|
187
|
-
if (this.sftp) {
|
|
188
|
-
throw this.fmtError(
|
|
189
|
-
'An existing SFTP connection is already defined',
|
|
190
|
-
'connect',
|
|
191
|
-
errorCode.connect,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
const retryOpts = {
|
|
195
|
-
retries: config.retries ?? 1,
|
|
196
|
-
factor: config.retry_factor ?? 2,
|
|
197
|
-
minTimeout: config.retry_minTimeout ?? 25000,
|
|
198
|
-
};
|
|
199
|
-
await promiseRetry(retryOpts, async (retry, attempt) => {
|
|
200
|
-
try {
|
|
201
|
-
this.debugMsg(`connect: Connect attempt ${attempt}`);
|
|
202
|
-
await this.getConnection(config);
|
|
203
|
-
} catch (err) {
|
|
204
|
-
switch (err.code) {
|
|
205
|
-
case 'ENOTFOUND':
|
|
206
|
-
case 'ECONNREFUSED':
|
|
207
|
-
case 'ERR_SOCKET_BAD_PORT': {
|
|
208
|
-
throw err;
|
|
209
|
-
}
|
|
210
|
-
case undefined: {
|
|
211
|
-
if (
|
|
212
|
-
err.message.endsWith('All configured authentication methods failed') ||
|
|
213
|
-
err.message.startsWith('Cannot parse privateKey')
|
|
214
|
-
) {
|
|
215
|
-
throw err;
|
|
216
|
-
}
|
|
217
|
-
retry(err);
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
default: {
|
|
221
|
-
retry(err);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
const sftp = await this.getSftpChannel();
|
|
227
|
-
this.endCalled = false;
|
|
228
|
-
return sftp;
|
|
229
|
-
} catch (err) {
|
|
230
|
-
this.end();
|
|
231
|
-
throw err.custom ? err : this.fmtError(err, 'connect');
|
|
232
|
-
} finally {
|
|
233
|
-
removeTempListeners(this, listeners, 'connect');
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
176
|
/**
|
|
238
177
|
* @async
|
|
239
178
|
*
|
|
@@ -265,6 +204,7 @@ class SftpClient {
|
|
|
265
204
|
}).finally(() => {
|
|
266
205
|
if (addListeners) {
|
|
267
206
|
removeTempListeners(this, listeners, 'realPath');
|
|
207
|
+
this._resetEventFlags();
|
|
268
208
|
}
|
|
269
209
|
});
|
|
270
210
|
}
|
|
@@ -329,6 +269,7 @@ class SftpClient {
|
|
|
329
269
|
}).finally(() => {
|
|
330
270
|
if (addListeners) {
|
|
331
271
|
removeTempListeners(this, listeners, '_xstat');
|
|
272
|
+
this._resetEventFlags();
|
|
332
273
|
}
|
|
333
274
|
});
|
|
334
275
|
}
|
|
@@ -459,6 +400,7 @@ class SftpClient {
|
|
|
459
400
|
}).finally(() => {
|
|
460
401
|
if (addListeners) {
|
|
461
402
|
removeTempListeners(this, listeners, 'list');
|
|
403
|
+
this._resetEventFlags();
|
|
462
404
|
}
|
|
463
405
|
});
|
|
464
406
|
}
|
|
@@ -499,7 +441,7 @@ class SftpClient {
|
|
|
499
441
|
pipeOptions: { ...options?.pipeOptions, end: true },
|
|
500
442
|
};
|
|
501
443
|
rdr = this.sftp.createReadStream(remotePath, options.readStreamOptions);
|
|
502
|
-
rdr.
|
|
444
|
+
rdr.on('error', (err) => {
|
|
503
445
|
if (dst && typeof dst === 'string' && wtr && !wtr.destroyed) {
|
|
504
446
|
wtr.destroy();
|
|
505
447
|
}
|
|
@@ -507,13 +449,11 @@ class SftpClient {
|
|
|
507
449
|
});
|
|
508
450
|
if (dst === undefined) {
|
|
509
451
|
// no dst specified, return buffer of data
|
|
510
|
-
this.debugMsg('get resolving with buffer of data');
|
|
511
452
|
wtr = concat((buff) => {
|
|
512
453
|
resolve(buff);
|
|
513
454
|
});
|
|
514
455
|
} else if (typeof dst === 'string') {
|
|
515
456
|
// dst local file path
|
|
516
|
-
this.debugMsg(`get called with file path destination ${dst}`);
|
|
517
457
|
const localCheck = haveLocalCreate(dst);
|
|
518
458
|
if (localCheck.status) {
|
|
519
459
|
wtr = fs.createWriteStream(dst, options.writeStreamOptions);
|
|
@@ -527,10 +467,9 @@ class SftpClient {
|
|
|
527
467
|
);
|
|
528
468
|
}
|
|
529
469
|
} else {
|
|
530
|
-
this.debugMsg('get called with stream destination');
|
|
531
470
|
wtr = dst;
|
|
532
471
|
}
|
|
533
|
-
wtr.
|
|
472
|
+
wtr.on('error', (err) => {
|
|
534
473
|
reject(
|
|
535
474
|
this.fmtError(
|
|
536
475
|
`${err.message} ${typeof dst === 'string' ? dst : '<stream>'}`,
|
|
@@ -554,6 +493,7 @@ class SftpClient {
|
|
|
554
493
|
}
|
|
555
494
|
if (addListeners) {
|
|
556
495
|
removeTempListeners(this, listeners, 'get');
|
|
496
|
+
this._resetEventFlags();
|
|
557
497
|
}
|
|
558
498
|
});
|
|
559
499
|
}
|
|
@@ -589,6 +529,7 @@ class SftpClient {
|
|
|
589
529
|
}).finally(() => {
|
|
590
530
|
if (addListeners) {
|
|
591
531
|
removeTempListeners(this, listeners, '_fastGet');
|
|
532
|
+
this._resetEventFlags();
|
|
592
533
|
}
|
|
593
534
|
});
|
|
594
535
|
}
|
|
@@ -655,6 +596,7 @@ class SftpClient {
|
|
|
655
596
|
}).finally(() => {
|
|
656
597
|
if (addListeners) {
|
|
657
598
|
removeTempListeners(this, listeners, '_fastPut');
|
|
599
|
+
this._resetEventFlags();
|
|
658
600
|
}
|
|
659
601
|
});
|
|
660
602
|
}
|
|
@@ -711,7 +653,7 @@ class SftpClient {
|
|
|
711
653
|
};
|
|
712
654
|
if (haveConnection(this, '_put', reject)) {
|
|
713
655
|
wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
|
|
714
|
-
wtr.
|
|
656
|
+
wtr.on('error', (err) => {
|
|
715
657
|
if (typeof lPath === 'string' && rdr && !rdr.destroyed) {
|
|
716
658
|
rdr.destroy();
|
|
717
659
|
}
|
|
@@ -727,17 +669,14 @@ class SftpClient {
|
|
|
727
669
|
resolve(`Uploaded data stream to ${rPath}`);
|
|
728
670
|
});
|
|
729
671
|
if (lPath instanceof Buffer) {
|
|
730
|
-
this.debugMsg('put source is a buffer');
|
|
731
672
|
wtr.end(lPath);
|
|
732
673
|
} else {
|
|
733
674
|
if (typeof lPath === 'string') {
|
|
734
|
-
this.debugMsg('put source is string path');
|
|
735
675
|
rdr = fs.createReadStream(lPath, opts.readStreamOptions);
|
|
736
676
|
} else {
|
|
737
|
-
this.debugMsg('put source is a stream');
|
|
738
677
|
rdr = lPath;
|
|
739
678
|
}
|
|
740
|
-
rdr.
|
|
679
|
+
rdr.on('error', (err) => {
|
|
741
680
|
reject(
|
|
742
681
|
this.fmtError(
|
|
743
682
|
`Read stream error: ${err.message} ${
|
|
@@ -757,6 +696,7 @@ class SftpClient {
|
|
|
757
696
|
}
|
|
758
697
|
if (addListeners) {
|
|
759
698
|
removeTempListeners(this, listeners, '_put');
|
|
699
|
+
this._resetEventFlags();
|
|
760
700
|
}
|
|
761
701
|
});
|
|
762
702
|
}
|
|
@@ -812,6 +752,7 @@ class SftpClient {
|
|
|
812
752
|
}).finally(() => {
|
|
813
753
|
if (addListeners) {
|
|
814
754
|
removeTempListeners(this, listeners, '_append');
|
|
755
|
+
this._resetEventFlags();
|
|
815
756
|
}
|
|
816
757
|
});
|
|
817
758
|
}
|
|
@@ -883,6 +824,7 @@ class SftpClient {
|
|
|
883
824
|
}).finally(() => {
|
|
884
825
|
if (addListeners) {
|
|
885
826
|
removeTempListeners(this, listeners, '_doMkdir');
|
|
827
|
+
this._resetEventFlags();
|
|
886
828
|
}
|
|
887
829
|
});
|
|
888
830
|
}
|
|
@@ -956,6 +898,7 @@ class SftpClient {
|
|
|
956
898
|
});
|
|
957
899
|
}).finally(() => {
|
|
958
900
|
removeTempListeners(this, listeners, '_rmdir');
|
|
901
|
+
this._resetEventFlags();
|
|
959
902
|
});
|
|
960
903
|
};
|
|
961
904
|
|
|
@@ -1045,6 +988,7 @@ class SftpClient {
|
|
|
1045
988
|
}).finally(() => {
|
|
1046
989
|
if (addListeners) {
|
|
1047
990
|
removeTempListeners(this, listeners, 'delete');
|
|
991
|
+
this._resetEventFlags();
|
|
1048
992
|
}
|
|
1049
993
|
});
|
|
1050
994
|
}
|
|
@@ -1083,6 +1027,7 @@ class SftpClient {
|
|
|
1083
1027
|
}).finally(() => {
|
|
1084
1028
|
if (addListeners) {
|
|
1085
1029
|
removeTempListeners(this, listeners, 'rename');
|
|
1030
|
+
this._resetEventFlags();
|
|
1086
1031
|
}
|
|
1087
1032
|
});
|
|
1088
1033
|
}
|
|
@@ -1121,6 +1066,7 @@ class SftpClient {
|
|
|
1121
1066
|
}
|
|
1122
1067
|
}).finally(() => {
|
|
1123
1068
|
removeTempListeners(this, listeners, 'posixRename');
|
|
1069
|
+
this._resetEventFlags();
|
|
1124
1070
|
});
|
|
1125
1071
|
}
|
|
1126
1072
|
|
|
@@ -1152,6 +1098,7 @@ class SftpClient {
|
|
|
1152
1098
|
}).finally(() => {
|
|
1153
1099
|
if (addListeners) {
|
|
1154
1100
|
removeTempListeners(this, listeners, 'chmod');
|
|
1101
|
+
this._resetEventFlags();
|
|
1155
1102
|
}
|
|
1156
1103
|
});
|
|
1157
1104
|
}
|
|
@@ -1237,6 +1184,7 @@ class SftpClient {
|
|
|
1237
1184
|
throw this.fmtError(`${e.message} ${srcDir} to ${dstDir}`, 'uploadFiles', e.code);
|
|
1238
1185
|
} finally {
|
|
1239
1186
|
removeTempListeners(this, listeners, uploadFiles);
|
|
1187
|
+
this._resetEventFlags();
|
|
1240
1188
|
}
|
|
1241
1189
|
};
|
|
1242
1190
|
|
|
@@ -1362,6 +1310,7 @@ class SftpClient {
|
|
|
1362
1310
|
);
|
|
1363
1311
|
} finally {
|
|
1364
1312
|
removeTempListeners(this, listeners, 'downloadFiles');
|
|
1313
|
+
this._resetEventFlags();
|
|
1365
1314
|
}
|
|
1366
1315
|
};
|
|
1367
1316
|
|
|
@@ -1408,7 +1357,8 @@ class SftpClient {
|
|
|
1408
1357
|
} catch (err) {
|
|
1409
1358
|
throw err.custom ? err : this.fmtError(err.message, 'createReadStream', err.code);
|
|
1410
1359
|
} finally {
|
|
1411
|
-
removeTempListeners(this, listeners, '
|
|
1360
|
+
removeTempListeners(this, listeners, 'createReadStream');
|
|
1361
|
+
this._resetEventFlags();
|
|
1412
1362
|
}
|
|
1413
1363
|
}
|
|
1414
1364
|
|
|
@@ -1434,6 +1384,7 @@ class SftpClient {
|
|
|
1434
1384
|
throw err.custom ? err : this.fmtError(err.message, 'createWriteStream', err.code);
|
|
1435
1385
|
} finally {
|
|
1436
1386
|
removeTempListeners(this, listeners, 'createWriteStream');
|
|
1387
|
+
this._resetEventFlags();
|
|
1437
1388
|
}
|
|
1438
1389
|
}
|
|
1439
1390
|
|
|
@@ -1497,6 +1448,7 @@ class SftpClient {
|
|
|
1497
1448
|
throw err.custom ? err : this.fmtError(err, 'rcopy');
|
|
1498
1449
|
} finally {
|
|
1499
1450
|
removeTempListeners(this, listeners, 'rcopy');
|
|
1451
|
+
this._resetEventFlags();
|
|
1500
1452
|
}
|
|
1501
1453
|
}
|
|
1502
1454
|
/**
|
|
@@ -1528,6 +1480,7 @@ class SftpClient {
|
|
|
1528
1480
|
}).finally(() => {
|
|
1529
1481
|
removeTempListeners(this, listeners, 'end');
|
|
1530
1482
|
this.removeListener('close', endCloseHandler);
|
|
1483
|
+
this._resetEventFlags();
|
|
1531
1484
|
});
|
|
1532
1485
|
}
|
|
1533
1486
|
}
|
package/src/utils.js
CHANGED
|
@@ -88,7 +88,7 @@ function endListener(client, name, reject) {
|
|
|
88
88
|
client.endHandled = true;
|
|
89
89
|
client.debugMsg(`${name} endListener - handling unexpected end event`);
|
|
90
90
|
const newError = new Error(`${name}: Unexpected end event`);
|
|
91
|
-
newError.code = errorCode.
|
|
91
|
+
newError.code = errorCode.generic;
|
|
92
92
|
if (reject) {
|
|
93
93
|
reject(newError);
|
|
94
94
|
} else {
|
|
@@ -109,7 +109,7 @@ function closeListener(client, name, reject) {
|
|
|
109
109
|
client.closeHandled = true;
|
|
110
110
|
client.debugMsg(`${name} closeListener - handling unexpected close event`);
|
|
111
111
|
const newError = new Error(`${name}: Unexpected close event`);
|
|
112
|
-
newError.code = errorCode.
|
|
112
|
+
newError.code = errorCode.generic;
|
|
113
113
|
if (reject) {
|
|
114
114
|
reject(newError);
|
|
115
115
|
} else {
|