ssh2-sftp-client 10.0.3 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -142
- package/README.org +152 -50
- package/package.json +9 -10
- package/src/index.js +76 -147
- package/src/utils.js +83 -38
package/src/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
3
2
|
const { Client } = require('ssh2');
|
|
4
3
|
const fs = require('node:fs');
|
|
5
4
|
const concat = require('concat-stream');
|
|
6
|
-
const promiseRetry = require('promise-retry');
|
|
7
5
|
const { join, parse } = require('node:path');
|
|
8
6
|
const {
|
|
9
7
|
globalListener,
|
|
@@ -19,11 +17,18 @@ const {
|
|
|
19
17
|
const { errorCode } = require('./constants');
|
|
20
18
|
|
|
21
19
|
class SftpClient {
|
|
22
|
-
constructor(
|
|
23
|
-
|
|
20
|
+
constructor(
|
|
21
|
+
clientName = 'sftp',
|
|
22
|
+
callbacks = {
|
|
23
|
+
error: (err) => console.error(`Global error listener: ${err.message}`),
|
|
24
|
+
end: () => console.log('Global end listener: end event raised'),
|
|
25
|
+
close: () => console.log('Global close listener: close event raised'),
|
|
26
|
+
},
|
|
27
|
+
) {
|
|
28
|
+
this.version = '12.0.0';
|
|
24
29
|
this.client = new Client();
|
|
25
30
|
this.sftp = undefined;
|
|
26
|
-
this.clientName = clientName
|
|
31
|
+
this.clientName = clientName;
|
|
27
32
|
this.endCalled = false;
|
|
28
33
|
this.errorHandled = false;
|
|
29
34
|
this.closeHandled = false;
|
|
@@ -31,9 +36,10 @@ class SftpClient {
|
|
|
31
36
|
this.remotePlatform = 'unix';
|
|
32
37
|
this.debug = undefined;
|
|
33
38
|
this.promiseLimit = 10;
|
|
34
|
-
this.
|
|
35
|
-
this.client.on('
|
|
36
|
-
this.client.on('
|
|
39
|
+
this.eventCallbacks = callbacks;
|
|
40
|
+
this.client.on('close', globalListener(this, 'close', this.eventCallbacks));
|
|
41
|
+
this.client.on('end', globalListener(this, 'end', this.eventCallbacks));
|
|
42
|
+
this.client.on('error', globalListener(this, 'error', this.eventCallbacks));
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
debugMsg(msg, obj) {
|
|
@@ -51,35 +57,32 @@ class SftpClient {
|
|
|
51
57
|
fmtError(err, name = 'sftp', eCode, retryCount) {
|
|
52
58
|
let msg = '';
|
|
53
59
|
let code = '';
|
|
54
|
-
const retry = retryCount
|
|
55
|
-
? ` after ${retryCount} ${retryCount > 1 ? 'attempts' : 'attempt'}`
|
|
56
|
-
: '';
|
|
57
60
|
|
|
58
61
|
if (err === undefined) {
|
|
59
62
|
msg = `${name}: Undefined error - probably a bug!`;
|
|
60
63
|
code = errorCode.generic;
|
|
61
64
|
} else if (typeof err === 'string') {
|
|
62
|
-
msg = `${name}: ${err}
|
|
65
|
+
msg = `${name}: ${err}`;
|
|
63
66
|
code = eCode || errorCode.generic;
|
|
64
67
|
} else if (err.custom) {
|
|
65
|
-
msg = `${name}->${err.message}
|
|
68
|
+
msg = `${name}->${err.message}`;
|
|
66
69
|
code = err.code;
|
|
67
70
|
} else {
|
|
68
71
|
switch (err.code) {
|
|
69
72
|
case 'ENOTFOUND': {
|
|
70
|
-
msg = `${name}: Address lookup failed for host
|
|
73
|
+
msg = `${name}: Address lookup failed for host`;
|
|
71
74
|
break;
|
|
72
75
|
}
|
|
73
76
|
case 'ECONNREFUSED': {
|
|
74
|
-
msg = `${name}: Remote host refused connection
|
|
77
|
+
msg = `${name}: Remote host refused connection`;
|
|
75
78
|
break;
|
|
76
79
|
}
|
|
77
80
|
case 'ECONNRESET': {
|
|
78
|
-
msg = `${name}: Remote host has reset the connection: ${err.message}
|
|
81
|
+
msg = `${name}: Remote host has reset the connection: ${err.message}`;
|
|
79
82
|
break;
|
|
80
83
|
}
|
|
81
84
|
default: {
|
|
82
|
-
msg = `${name}: ${err.message}
|
|
85
|
+
msg = `${name}: ${err.message}`;
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
code = err.code || errorCode.generic;
|
|
@@ -115,27 +118,52 @@ class SftpClient {
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
118
|
-
* @async
|
|
119
121
|
*
|
|
120
|
-
* 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.
|
|
121
125
|
*
|
|
122
126
|
* @param {Object} config - an SFTP configuration object
|
|
123
127
|
*
|
|
124
128
|
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
125
129
|
*/
|
|
126
|
-
|
|
130
|
+
connect(config) {
|
|
127
131
|
let doReady, listeners;
|
|
128
132
|
return new Promise((resolve, reject) => {
|
|
129
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
|
+
|
|
130
141
|
doReady = () => {
|
|
131
|
-
this.
|
|
132
|
-
|
|
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
|
+
});
|
|
133
150
|
};
|
|
134
151
|
this.on('ready', doReady);
|
|
152
|
+
|
|
135
153
|
try {
|
|
136
|
-
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
|
+
}
|
|
137
165
|
} catch (err) {
|
|
138
|
-
this.
|
|
166
|
+
this.end();
|
|
139
167
|
reject(err);
|
|
140
168
|
}
|
|
141
169
|
}).finally(() => {
|
|
@@ -144,90 +172,6 @@ class SftpClient {
|
|
|
144
172
|
});
|
|
145
173
|
}
|
|
146
174
|
|
|
147
|
-
getSftpChannel() {
|
|
148
|
-
return new Promise((resolve, reject) => {
|
|
149
|
-
this.client.sftp((err, sftp) => {
|
|
150
|
-
if (err) {
|
|
151
|
-
reject(this.fmtError(err, 'getSftpChannel', err.code));
|
|
152
|
-
} else {
|
|
153
|
-
this.sftp = sftp;
|
|
154
|
-
resolve(sftp);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @async
|
|
162
|
-
*
|
|
163
|
-
* Create a new SFTP connection to a remote SFTP server.
|
|
164
|
-
* The connection options are the same as those offered
|
|
165
|
-
* by the underlying SSH2 module.
|
|
166
|
-
*
|
|
167
|
-
* @param {Object} config - an SFTP configuration object
|
|
168
|
-
*
|
|
169
|
-
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
170
|
-
*/
|
|
171
|
-
async connect(config) {
|
|
172
|
-
let listeners;
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
listeners = addTempListeners(this, 'connect');
|
|
176
|
-
if (config.debug) {
|
|
177
|
-
this.debug = config.debug;
|
|
178
|
-
this.debugMsg('connect: Debugging turned on');
|
|
179
|
-
this.debugMsg(`ssh2-sftp-client Version: ${this.version} `, process.versions);
|
|
180
|
-
}
|
|
181
|
-
this.promiseLimit = config.promiseLimit ?? 10;
|
|
182
|
-
if (this.sftp) {
|
|
183
|
-
throw this.fmtError(
|
|
184
|
-
'An existing SFTP connection is already defined',
|
|
185
|
-
'connect',
|
|
186
|
-
errorCode.connect,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
const retryOpts = {
|
|
190
|
-
retries: config.retries ?? 1,
|
|
191
|
-
factor: config.retry_factor ?? 2,
|
|
192
|
-
minTimeout: config.retry_minTimeout ?? 25000,
|
|
193
|
-
};
|
|
194
|
-
await promiseRetry(retryOpts, async (retry, attempt) => {
|
|
195
|
-
try {
|
|
196
|
-
this.debugMsg(`connect: Connect attempt ${attempt}`);
|
|
197
|
-
await this.getConnection(config);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
switch (err.code) {
|
|
200
|
-
case 'ENOTFOUND':
|
|
201
|
-
case 'ECONNREFUSED':
|
|
202
|
-
case 'ERR_SOCKET_BAD_PORT': {
|
|
203
|
-
throw err;
|
|
204
|
-
}
|
|
205
|
-
case undefined: {
|
|
206
|
-
if (
|
|
207
|
-
err.message.endsWith('All configured authentication methods failed') ||
|
|
208
|
-
err.message.startsWith('Cannot parse privateKey')
|
|
209
|
-
) {
|
|
210
|
-
throw err;
|
|
211
|
-
}
|
|
212
|
-
retry(err);
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
default: {
|
|
216
|
-
retry(err);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
const sftp = await this.getSftpChannel();
|
|
222
|
-
return sftp;
|
|
223
|
-
} catch (err) {
|
|
224
|
-
this.end();
|
|
225
|
-
throw err.custom ? err : this.fmtError(err, 'connect');
|
|
226
|
-
} finally {
|
|
227
|
-
removeTempListeners(this, listeners, 'connect');
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
175
|
/**
|
|
232
176
|
* @async
|
|
233
177
|
*
|
|
@@ -246,18 +190,14 @@ class SftpClient {
|
|
|
246
190
|
if (addListeners) {
|
|
247
191
|
listeners = addTempListeners(this, 'realPath', reject);
|
|
248
192
|
}
|
|
249
|
-
this.debugMsg(`realPath -> ${remotePath}`);
|
|
250
193
|
this.sftp.realpath(remotePath, (err, absPath) => {
|
|
251
194
|
if (err) {
|
|
252
195
|
if (err.code === 2) {
|
|
253
|
-
this.debugMsg('realPath <- ""');
|
|
254
196
|
resolve('');
|
|
255
197
|
} else {
|
|
256
|
-
this.debugMsg(`${err.message} ${remotePath}`, 'realPath');
|
|
257
198
|
reject(this.fmtError(`${err.message} ${remotePath}`, 'realPath', err.code));
|
|
258
199
|
}
|
|
259
200
|
}
|
|
260
|
-
this.debugMsg(`realPath <- ${absPath}`);
|
|
261
201
|
resolve(absPath);
|
|
262
202
|
});
|
|
263
203
|
}).finally(() => {
|
|
@@ -313,7 +253,6 @@ class SftpClient {
|
|
|
313
253
|
isFIFO: stats.isFIFO(),
|
|
314
254
|
isSocket: stats.isSocket(),
|
|
315
255
|
};
|
|
316
|
-
this.debugMsg('_xstat: result = ', result);
|
|
317
256
|
resolve(result);
|
|
318
257
|
}
|
|
319
258
|
};
|
|
@@ -382,13 +321,11 @@ class SftpClient {
|
|
|
382
321
|
* object if it does
|
|
383
322
|
*/
|
|
384
323
|
async exists(remotePath) {
|
|
385
|
-
this.debugMsg(`exists: remotePath = ${remotePath}`);
|
|
386
324
|
try {
|
|
387
325
|
if (remotePath === '.') {
|
|
388
326
|
return 'd';
|
|
389
327
|
}
|
|
390
328
|
const info = await this.lstat(remotePath);
|
|
391
|
-
this.debugMsg('exists: <- ', info);
|
|
392
329
|
if (info.isDirectory) {
|
|
393
330
|
return 'd';
|
|
394
331
|
} else if (info.isSymbolicLink) {
|
|
@@ -501,20 +438,20 @@ class SftpClient {
|
|
|
501
438
|
};
|
|
502
439
|
rdr = this.sftp.createReadStream(remotePath, options.readStreamOptions);
|
|
503
440
|
rdr.once('error', (err) => {
|
|
504
|
-
if (dst && typeof dst
|
|
505
|
-
|
|
441
|
+
if (dst && typeof dst === 'string' && wtr && !wtr.destroyed) {
|
|
442
|
+
wtr.destroy();
|
|
506
443
|
}
|
|
507
444
|
reject(this.fmtError(`${err.message} ${remotePath}`, 'get', err.code));
|
|
508
445
|
});
|
|
509
446
|
if (dst === undefined) {
|
|
510
447
|
// no dst specified, return buffer of data
|
|
511
|
-
this.debugMsg('get resolving buffer of data');
|
|
448
|
+
this.debugMsg('get resolving with buffer of data');
|
|
512
449
|
wtr = concat((buff) => {
|
|
513
450
|
resolve(buff);
|
|
514
451
|
});
|
|
515
452
|
} else if (typeof dst === 'string') {
|
|
516
453
|
// dst local file path
|
|
517
|
-
this.debugMsg(
|
|
454
|
+
this.debugMsg(`get called with file path destination ${dst}`);
|
|
518
455
|
const localCheck = haveLocalCreate(dst);
|
|
519
456
|
if (localCheck.status) {
|
|
520
457
|
wtr = fs.createWriteStream(dst, options.writeStreamOptions);
|
|
@@ -528,7 +465,7 @@ class SftpClient {
|
|
|
528
465
|
);
|
|
529
466
|
}
|
|
530
467
|
} else {
|
|
531
|
-
this.debugMsg('get
|
|
468
|
+
this.debugMsg('get called with stream destination');
|
|
532
469
|
wtr = dst;
|
|
533
470
|
}
|
|
534
471
|
wtr.once('error', (err) => {
|
|
@@ -542,16 +479,17 @@ class SftpClient {
|
|
|
542
479
|
});
|
|
543
480
|
rdr.once('end', () => {
|
|
544
481
|
if (typeof dst === 'string') {
|
|
545
|
-
this.debugMsg('get: resolving with dst filename');
|
|
546
482
|
resolve(dst);
|
|
547
483
|
} else if (dst !== undefined) {
|
|
548
|
-
this.debugMsg('get: resolving with writer stream object');
|
|
549
484
|
resolve(wtr);
|
|
550
485
|
}
|
|
551
486
|
});
|
|
552
487
|
rdr.pipe(wtr, options.pipeOptions);
|
|
553
488
|
}
|
|
554
489
|
}).finally(() => {
|
|
490
|
+
if (rdr && !rdr.destroyed) {
|
|
491
|
+
rdr.destroy();
|
|
492
|
+
}
|
|
555
493
|
if (addListeners) {
|
|
556
494
|
removeTempListeners(this, listeners, 'get');
|
|
557
495
|
}
|
|
@@ -661,7 +599,6 @@ class SftpClient {
|
|
|
661
599
|
|
|
662
600
|
async fastPut(localPath, remotePath, options) {
|
|
663
601
|
try {
|
|
664
|
-
this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
|
|
665
602
|
const localCheck = haveLocalAccess(localPath);
|
|
666
603
|
if (!localCheck.status) {
|
|
667
604
|
throw this.fmtError(
|
|
@@ -713,6 +650,9 @@ class SftpClient {
|
|
|
713
650
|
if (haveConnection(this, '_put', reject)) {
|
|
714
651
|
wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
|
|
715
652
|
wtr.once('error', (err) => {
|
|
653
|
+
if (typeof lPath === 'string' && rdr && !rdr.destroyed) {
|
|
654
|
+
rdr.destroy();
|
|
655
|
+
}
|
|
716
656
|
reject(
|
|
717
657
|
this.fmtError(
|
|
718
658
|
`Write stream error: ${err.message} ${rPath}`,
|
|
@@ -728,10 +668,13 @@ class SftpClient {
|
|
|
728
668
|
this.debugMsg('put source is a buffer');
|
|
729
669
|
wtr.end(lPath);
|
|
730
670
|
} else {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
671
|
+
if (typeof lPath === 'string') {
|
|
672
|
+
this.debugMsg('put source is string path');
|
|
673
|
+
rdr = fs.createReadStream(lPath, opts.readStreamOptions);
|
|
674
|
+
} else {
|
|
675
|
+
this.debugMsg('put source is a stream');
|
|
676
|
+
rdr = lPath;
|
|
677
|
+
}
|
|
735
678
|
rdr.once('error', (err) => {
|
|
736
679
|
reject(
|
|
737
680
|
this.fmtError(
|
|
@@ -747,6 +690,9 @@ class SftpClient {
|
|
|
747
690
|
}
|
|
748
691
|
}
|
|
749
692
|
}).finally(() => {
|
|
693
|
+
if (wtr && !wtr.destroyed) {
|
|
694
|
+
wtr.destroy();
|
|
695
|
+
}
|
|
750
696
|
if (addListeners) {
|
|
751
697
|
removeTempListeners(this, listeners, '_put');
|
|
752
698
|
}
|
|
@@ -786,7 +732,6 @@ class SftpClient {
|
|
|
786
732
|
listeners = addTempListeners(this, '_append', reject);
|
|
787
733
|
}
|
|
788
734
|
if (haveConnection(this, '_append', reject)) {
|
|
789
|
-
this.debugMsg(`append -> remote: ${rPath} `, opts);
|
|
790
735
|
opts.flags = 'a';
|
|
791
736
|
const stream = this.sftp.createWriteStream(rPath, opts);
|
|
792
737
|
stream.on('error', (err) => {
|
|
@@ -826,7 +771,7 @@ class SftpClient {
|
|
|
826
771
|
errorCode.badPath,
|
|
827
772
|
);
|
|
828
773
|
}
|
|
829
|
-
await this._append(input, remotePath, options);
|
|
774
|
+
return await this._append(input, remotePath, options);
|
|
830
775
|
} catch (e) {
|
|
831
776
|
throw e.custom ? e : this.fmtError(e.message, 'append', e.code);
|
|
832
777
|
}
|
|
@@ -941,7 +886,6 @@ class SftpClient {
|
|
|
941
886
|
let listeners;
|
|
942
887
|
return new Promise((resolve, reject) => {
|
|
943
888
|
listeners = addTempListeners(this, '_rmdir', reject);
|
|
944
|
-
this.debugMsg(`_rmdir: dir = ${dir}`);
|
|
945
889
|
this.sftp.rmdir(dir, (err) => {
|
|
946
890
|
if (err) {
|
|
947
891
|
reject(this.fmtError(`${err.message} ${dir}`, 'rmdir', err.code));
|
|
@@ -957,7 +901,6 @@ class SftpClient {
|
|
|
957
901
|
let listeners;
|
|
958
902
|
return new Promise((resolve, reject) => {
|
|
959
903
|
listeners = addTempListeners(this, '_delFiles', reject);
|
|
960
|
-
this.debugMsg(`_delFiles: path = ${path} fileList = ${fileList}`);
|
|
961
904
|
const pList = [];
|
|
962
905
|
for (const f of fileList) {
|
|
963
906
|
pList.push(this.delete(`${path}/${f.name}`, true, false));
|
|
@@ -973,10 +916,8 @@ class SftpClient {
|
|
|
973
916
|
};
|
|
974
917
|
|
|
975
918
|
try {
|
|
976
|
-
this.debugMsg(`rmdir: dir = ${remoteDir} recursive = ${recursive}`);
|
|
977
919
|
const absPath = await normalizeRemotePath(this, remoteDir);
|
|
978
920
|
const existStatus = await this.exists(absPath);
|
|
979
|
-
this.debugMsg(`rmdir: ${absPath} existStatus = ${existStatus}`);
|
|
980
921
|
if (!existStatus) {
|
|
981
922
|
throw this.fmtError(
|
|
982
923
|
`Bad Path: ${remoteDir}: No such directory`,
|
|
@@ -992,19 +933,14 @@ class SftpClient {
|
|
|
992
933
|
);
|
|
993
934
|
}
|
|
994
935
|
if (!recursive) {
|
|
995
|
-
this.debugMsg('rmdir: non-recursive - just try to remove it');
|
|
996
936
|
return await _rmdir(absPath);
|
|
997
937
|
}
|
|
998
938
|
const listing = await this.list(absPath);
|
|
999
|
-
this.debugMsg(`rmdir: listing count = ${listing.length}`);
|
|
1000
939
|
if (!listing.length) {
|
|
1001
|
-
this.debugMsg('rmdir: No sub dir or files, just rmdir');
|
|
1002
940
|
return await _rmdir(absPath);
|
|
1003
941
|
}
|
|
1004
942
|
const fileList = listing.filter((i) => i.type !== 'd');
|
|
1005
|
-
this.debugMsg(`rmdir: dir content files to remove = ${fileList.length}`);
|
|
1006
943
|
const dirList = listing.filter((i) => i.type === 'd');
|
|
1007
|
-
this.debugMsg(`rmdir: sub-directories to remove = ${dirList.length}`);
|
|
1008
944
|
await _delFiles(absPath, fileList);
|
|
1009
945
|
for (const d of dirList) {
|
|
1010
946
|
await this.rmdir(`${absPath}/${d.name}`, true);
|
|
@@ -1244,11 +1180,7 @@ class SftpClient {
|
|
|
1244
1180
|
|
|
1245
1181
|
try {
|
|
1246
1182
|
haveConnection(this, 'uploadDir');
|
|
1247
|
-
this.debugMsg(
|
|
1248
|
-
`uploadDir: srcDir = ${srcDir} dstDir = ${dstDir} options = ${options}`,
|
|
1249
|
-
);
|
|
1250
1183
|
const { remoteDir, remoteStatus } = await getRemoteStatus(dstDir);
|
|
1251
|
-
this.debugMsg(`uploadDir: remoteDir = ${remoteDir} remoteStatus = ${remoteStatus}`);
|
|
1252
1184
|
checkLocalStatus(srcDir);
|
|
1253
1185
|
if (!remoteStatus) {
|
|
1254
1186
|
await this._mkdir(remoteDir, true);
|
|
@@ -1257,7 +1189,6 @@ class SftpClient {
|
|
|
1257
1189
|
encoding: 'utf8',
|
|
1258
1190
|
withFileTypes: true,
|
|
1259
1191
|
});
|
|
1260
|
-
this.debugMsg(`uploadDir: dirEntries = ${dirEntries}`);
|
|
1261
1192
|
if (options?.filter) {
|
|
1262
1193
|
dirEntries = dirEntries.filter((item) =>
|
|
1263
1194
|
options.filter(join(srcDir, item.name), item.isDirectory()),
|
|
@@ -1265,8 +1196,6 @@ class SftpClient {
|
|
|
1265
1196
|
}
|
|
1266
1197
|
const dirUploads = dirEntries.filter((item) => item.isDirectory());
|
|
1267
1198
|
const fileUploads = dirEntries.filter((item) => !item.isDirectory());
|
|
1268
|
-
this.debugMsg(`uploadDir: dirUploads = ${dirUploads}`);
|
|
1269
|
-
this.debugMsg(`uploadDir: fileUploads = ${fileUploads}`);
|
|
1270
1199
|
await uploadFiles(srcDir, remoteDir, fileUploads, options?.useFastput);
|
|
1271
1200
|
for (const d of dirUploads) {
|
|
1272
1201
|
const src = join(srcDir, d.name);
|
|
@@ -1417,7 +1346,7 @@ class SftpClient {
|
|
|
1417
1346
|
} catch (err) {
|
|
1418
1347
|
throw err.custom ? err : this.fmtError(err.message, 'createReadStream', err.code);
|
|
1419
1348
|
} finally {
|
|
1420
|
-
removeTempListeners(this, listeners, '
|
|
1349
|
+
removeTempListeners(this, listeners, 'createReadStream');
|
|
1421
1350
|
}
|
|
1422
1351
|
}
|
|
1423
1352
|
|
|
@@ -1527,6 +1456,7 @@ class SftpClient {
|
|
|
1527
1456
|
};
|
|
1528
1457
|
this.on('close', endCloseHandler);
|
|
1529
1458
|
if (this.sftp) {
|
|
1459
|
+
this.debugMsg('end: Ending SFTP connection');
|
|
1530
1460
|
this.client.end();
|
|
1531
1461
|
} else {
|
|
1532
1462
|
// no actual connection exists - just resolve
|
|
@@ -1536,7 +1466,6 @@ class SftpClient {
|
|
|
1536
1466
|
}).finally(() => {
|
|
1537
1467
|
removeTempListeners(this, listeners, 'end');
|
|
1538
1468
|
this.removeListener('close', endCloseHandler);
|
|
1539
|
-
this.endCalled = false;
|
|
1540
1469
|
});
|
|
1541
1470
|
}
|
|
1542
1471
|
}
|
package/src/utils.js
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const { errorCode } = require('./constants');
|
|
1
|
+
const { statSync, constants, accessSync } = require('node:fs');
|
|
2
|
+
const { dirname } = require('node:path');
|
|
3
|
+
const { errorCode } = require('./constants.js');
|
|
4
|
+
|
|
5
|
+
function eventHandled(client) {
|
|
6
|
+
if (client.errorHandled || client.endHandled || client.closeHandled) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function globalListener(client, evt, eventCallbacks) {
|
|
13
|
+
if (evt === 'error') {
|
|
14
|
+
return (err) => {
|
|
15
|
+
if (client.errorHandled) {
|
|
16
|
+
client.debugMsg(`Global error event: Ignoring handled error ${err.message}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
client.debugMsg(`Global error event: ${err.message}`);
|
|
20
|
+
client.errorHandled = true;
|
|
21
|
+
if (eventCallbacks?.error) {
|
|
22
|
+
eventCallbacks.error(err);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (evt === 'end') {
|
|
27
|
+
return () => {
|
|
28
|
+
if (client.endCalled || client.endHandled) {
|
|
29
|
+
client.debugMsg('Global end event: Ignoring handled end event');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
client.debugMsg('Global end event: Handling end event');
|
|
33
|
+
client.endHandled = true;
|
|
34
|
+
if (eventCallbacks?.end) {
|
|
35
|
+
eventCallbacks.end();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
if (client.endCalled || client.closeHandled) {
|
|
41
|
+
client.debugMsg('Global close event: Ignoring handled close event');
|
|
42
|
+
} else {
|
|
43
|
+
client.debugMsg('Global close event: Handling close event');
|
|
44
|
+
client.closeHandled = true;
|
|
45
|
+
client.sftp = undefined;
|
|
46
|
+
if (eventCallbacks?.close) {
|
|
47
|
+
eventCallbacks.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
4
52
|
|
|
5
53
|
/**
|
|
6
54
|
* Simple default error listener. Will reformat the error message and
|
|
@@ -10,12 +58,13 @@ const { errorCode } = require('./constants');
|
|
|
10
58
|
* @throws {Error} Throws new error
|
|
11
59
|
*/
|
|
12
60
|
function errorListener(client, name, reject) {
|
|
13
|
-
const fn = (err)
|
|
14
|
-
if (client
|
|
61
|
+
const fn = function (err) {
|
|
62
|
+
if (eventHandled(client)) {
|
|
15
63
|
// error already handled or expected - ignore
|
|
16
|
-
client.debugMsg(`${name} errorListener - ignoring handled error`);
|
|
64
|
+
client.debugMsg(`${name} errorListener - ignoring handled error ${err.message}`);
|
|
17
65
|
return;
|
|
18
66
|
}
|
|
67
|
+
client.debugMsg(`${name} errorListener - handling error ${err.message}`);
|
|
19
68
|
client.errorHandled = true;
|
|
20
69
|
const newError = new Error(`${name}: ${err.message}`);
|
|
21
70
|
newError.code = err.code;
|
|
@@ -28,55 +77,52 @@ function errorListener(client, name, reject) {
|
|
|
28
77
|
return fn;
|
|
29
78
|
}
|
|
30
79
|
|
|
31
|
-
function
|
|
32
|
-
return () => {
|
|
33
|
-
if (client.endCalled || client.errorHandled || client.closeHandled) {
|
|
34
|
-
// we are processing an expected event handled elsewhere
|
|
35
|
-
client.debugMsg(`Global ${evt} event: Ignoring expected and handled event`);
|
|
36
|
-
} else {
|
|
37
|
-
client.debugMsg(`Global ${evt} event: Handling unexpected event`);
|
|
38
|
-
client.sftp = undefined;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function endListener(client, name) {
|
|
80
|
+
function endListener(client, name, reject) {
|
|
44
81
|
const fn = function () {
|
|
45
82
|
client.sftp = undefined;
|
|
46
|
-
if (client.endCalled || client
|
|
83
|
+
if (client.endCalled || eventHandled(client)) {
|
|
47
84
|
// end event already handled - ignore
|
|
48
|
-
client.debugMsg(`${name} endListener - handled end event`);
|
|
85
|
+
client.debugMsg(`${name} endListener - ignoring handled end event`);
|
|
49
86
|
return;
|
|
50
87
|
}
|
|
51
88
|
client.endHandled = true;
|
|
52
|
-
client.debugMsg(`${name}
|
|
89
|
+
client.debugMsg(`${name} endListener - handling unexpected end event`);
|
|
90
|
+
const newError = new Error(`${name}: Unexpected end event`);
|
|
91
|
+
newError.code = errorCode.ERR_GENERIC_CLIENT;
|
|
92
|
+
if (reject) {
|
|
93
|
+
reject(newError);
|
|
94
|
+
} else {
|
|
95
|
+
throw newError;
|
|
96
|
+
}
|
|
53
97
|
};
|
|
54
98
|
return fn;
|
|
55
99
|
}
|
|
56
100
|
|
|
57
|
-
function closeListener(client, name) {
|
|
101
|
+
function closeListener(client, name, reject) {
|
|
58
102
|
const fn = function () {
|
|
59
103
|
client.sftp = undefined;
|
|
60
|
-
if (
|
|
61
|
-
client.endCalled ||
|
|
62
|
-
client.closeHandled ||
|
|
63
|
-
client.errorHandled ||
|
|
64
|
-
client.endHandled
|
|
65
|
-
) {
|
|
104
|
+
if (client.endCalled || eventHandled(client)) {
|
|
66
105
|
// handled or expected close event - ignore
|
|
67
|
-
client.debugMsg(`${name} closeListener - handled close event`);
|
|
106
|
+
client.debugMsg(`${name} closeListener - ignoring handled close event`);
|
|
68
107
|
return;
|
|
69
108
|
}
|
|
70
109
|
client.closeHandled = true;
|
|
71
|
-
client.debugMsg(`${name}
|
|
110
|
+
client.debugMsg(`${name} closeListener - handling unexpected close event`);
|
|
111
|
+
const newError = new Error(`${name}: Unexpected close event`);
|
|
112
|
+
newError.code = errorCode.ERR_GENERIC_CLIENT;
|
|
113
|
+
if (reject) {
|
|
114
|
+
reject(newError);
|
|
115
|
+
} else {
|
|
116
|
+
throw newError;
|
|
117
|
+
}
|
|
72
118
|
};
|
|
73
119
|
return fn;
|
|
74
120
|
}
|
|
75
121
|
|
|
76
122
|
function addTempListeners(client, name, reject) {
|
|
77
123
|
const listeners = {
|
|
78
|
-
end: endListener(client, name),
|
|
79
|
-
close: closeListener(client, name),
|
|
124
|
+
end: endListener(client, name, reject),
|
|
125
|
+
close: closeListener(client, name, reject),
|
|
80
126
|
error: errorListener(client, name, reject),
|
|
81
127
|
};
|
|
82
128
|
client.on('end', listeners.end);
|
|
@@ -109,7 +155,7 @@ function removeTempListeners(client, listeners, name) {
|
|
|
109
155
|
* @returns {string | boolean} returns a string for object type if it exists, false otherwise
|
|
110
156
|
*/
|
|
111
157
|
function localExists(filePath) {
|
|
112
|
-
const stats =
|
|
158
|
+
const stats = statSync(filePath, { throwIfNoEntry: false });
|
|
113
159
|
if (!stats) {
|
|
114
160
|
return false;
|
|
115
161
|
} else if (stats.isDirectory()) {
|
|
@@ -140,11 +186,10 @@ function localExists(filePath) {
|
|
|
140
186
|
* @returns {Object} with properties status, type, details and code
|
|
141
187
|
*/
|
|
142
188
|
function haveLocalAccess(filePath, mode = 'r') {
|
|
143
|
-
const accessMode =
|
|
144
|
-
fs.constants.F_OK | (mode === 'w') ? fs.constants.W_OK : fs.constants.R_OK;
|
|
189
|
+
const accessMode = constants.F_OK | (mode === 'w') ? constants.W_OK : constants.R_OK;
|
|
145
190
|
|
|
146
191
|
try {
|
|
147
|
-
|
|
192
|
+
accessSync(filePath, accessMode);
|
|
148
193
|
const type = localExists(filePath);
|
|
149
194
|
return {
|
|
150
195
|
status: true,
|
|
@@ -210,7 +255,7 @@ function haveLocalCreate(filePath) {
|
|
|
210
255
|
};
|
|
211
256
|
}
|
|
212
257
|
// to create it, parent must be directory and writeable
|
|
213
|
-
const dirPath =
|
|
258
|
+
const dirPath = dirname(filePath);
|
|
214
259
|
const localCheck = haveLocalAccess(dirPath, 'w');
|
|
215
260
|
if (!localCheck.status) {
|
|
216
261
|
// no access to parent directory
|