ssh2-sftp-client 9.1.0 → 10.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 +149 -140
- package/README.org +69 -20
- package/package.json +10 -10
- package/src/index.js +167 -139
- package/src/utils.js +58 -53
package/src/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { Client } = require('ssh2');
|
|
4
|
-
const fs = require('fs');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
5
|
const concat = require('concat-stream');
|
|
6
6
|
const promiseRetry = require('promise-retry');
|
|
7
|
-
const { join, parse } = require('path');
|
|
7
|
+
const { join, parse } = require('node:path');
|
|
8
8
|
const {
|
|
9
9
|
globalListener,
|
|
10
10
|
addTempListeners,
|
|
@@ -14,22 +14,23 @@ const {
|
|
|
14
14
|
localExists,
|
|
15
15
|
haveLocalAccess,
|
|
16
16
|
haveLocalCreate,
|
|
17
|
+
partition,
|
|
17
18
|
} = require('./utils');
|
|
18
19
|
const { errorCode } = require('./constants');
|
|
19
20
|
|
|
20
21
|
class SftpClient {
|
|
21
22
|
constructor(clientName) {
|
|
22
|
-
this.version = '
|
|
23
|
+
this.version = '10.0.0';
|
|
23
24
|
this.client = new Client();
|
|
24
25
|
this.sftp = undefined;
|
|
25
|
-
this.clientName = clientName
|
|
26
|
+
this.clientName = clientName || 'sftp';
|
|
26
27
|
this.endCalled = false;
|
|
27
28
|
this.errorHandled = false;
|
|
28
29
|
this.closeHandled = false;
|
|
29
30
|
this.endHandled = false;
|
|
30
31
|
this.remotePlatform = 'unix';
|
|
31
32
|
this.debug = undefined;
|
|
32
|
-
|
|
33
|
+
this.promiseLimit = 10;
|
|
33
34
|
this.client.on('close', globalListener(this, 'close'));
|
|
34
35
|
this.client.on('end', globalListener(this, 'end'));
|
|
35
36
|
this.client.on('error', globalListener(this, 'error'));
|
|
@@ -39,7 +40,7 @@ class SftpClient {
|
|
|
39
40
|
if (this.debug) {
|
|
40
41
|
if (obj) {
|
|
41
42
|
this.debug(
|
|
42
|
-
`CLIENT[${this.clientName}]: ${msg} ${JSON.stringify(obj, null, ' ')}
|
|
43
|
+
`CLIENT[${this.clientName}]: ${msg} ${JSON.stringify(obj, null, ' ')}`,
|
|
43
44
|
);
|
|
44
45
|
} else {
|
|
45
46
|
this.debug(`CLIENT[${this.clientName}]: ${msg}`);
|
|
@@ -59,25 +60,29 @@ class SftpClient {
|
|
|
59
60
|
code = errorCode.generic;
|
|
60
61
|
} else if (typeof err === 'string') {
|
|
61
62
|
msg = `${name}: ${err}${retry}`;
|
|
62
|
-
code = eCode
|
|
63
|
+
code = eCode || errorCode.generic;
|
|
63
64
|
} else if (err.custom) {
|
|
64
65
|
msg = `${name}->${err.message}${retry}`;
|
|
65
66
|
code = err.code;
|
|
66
67
|
} else {
|
|
67
68
|
switch (err.code) {
|
|
68
|
-
case 'ENOTFOUND':
|
|
69
|
+
case 'ENOTFOUND': {
|
|
69
70
|
msg = `${name}: Address lookup failed for host${retry}`;
|
|
70
71
|
break;
|
|
71
|
-
|
|
72
|
+
}
|
|
73
|
+
case 'ECONNREFUSED': {
|
|
72
74
|
msg = `${name}: Remote host refused connection${retry}`;
|
|
73
75
|
break;
|
|
74
|
-
|
|
76
|
+
}
|
|
77
|
+
case 'ECONNRESET': {
|
|
75
78
|
msg = `${name}: Remote host has reset the connection: ${err.message}${retry}`;
|
|
76
79
|
break;
|
|
77
|
-
|
|
80
|
+
}
|
|
81
|
+
default: {
|
|
78
82
|
msg = `${name}: ${err.message}${retry}`;
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
|
-
code = err.code
|
|
85
|
+
code = err.code || errorCode.generic;
|
|
81
86
|
}
|
|
82
87
|
const newError = new Error(msg);
|
|
83
88
|
newError.code = code;
|
|
@@ -117,7 +122,6 @@ class SftpClient {
|
|
|
117
122
|
* @param {Object} config - an SFTP configuration object
|
|
118
123
|
*
|
|
119
124
|
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
120
|
-
*
|
|
121
125
|
*/
|
|
122
126
|
getConnection(config) {
|
|
123
127
|
let doReady, listeners;
|
|
@@ -163,7 +167,6 @@ class SftpClient {
|
|
|
163
167
|
* @param {Object} config - an SFTP configuration object
|
|
164
168
|
*
|
|
165
169
|
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
166
|
-
*
|
|
167
170
|
*/
|
|
168
171
|
async connect(config) {
|
|
169
172
|
let listeners;
|
|
@@ -175,16 +178,17 @@ class SftpClient {
|
|
|
175
178
|
this.debugMsg('connect: Debugging turned on');
|
|
176
179
|
this.debugMsg(`ssh2-sftp-client Version: ${this.version} `, process.versions);
|
|
177
180
|
}
|
|
181
|
+
this.promiseLimit = config.promiseLimit ?? 10;
|
|
178
182
|
if (this.sftp) {
|
|
179
183
|
throw this.fmtError(
|
|
180
184
|
'An existing SFTP connection is already defined',
|
|
181
185
|
'connect',
|
|
182
|
-
errorCode.connect
|
|
186
|
+
errorCode.connect,
|
|
183
187
|
);
|
|
184
188
|
}
|
|
185
189
|
const retryOpts = {
|
|
186
190
|
retries: config.retries ?? 1,
|
|
187
|
-
factor: config.
|
|
191
|
+
factor: config.retry_factor ?? 2,
|
|
188
192
|
minTimeout: config.retry_minTimeout ?? 25000,
|
|
189
193
|
};
|
|
190
194
|
await promiseRetry(retryOpts, async (retry, attempt) => {
|
|
@@ -195,8 +199,9 @@ class SftpClient {
|
|
|
195
199
|
switch (err.code) {
|
|
196
200
|
case 'ENOTFOUND':
|
|
197
201
|
case 'ECONNREFUSED':
|
|
198
|
-
case 'ERR_SOCKET_BAD_PORT':
|
|
202
|
+
case 'ERR_SOCKET_BAD_PORT': {
|
|
199
203
|
throw err;
|
|
204
|
+
}
|
|
200
205
|
case undefined: {
|
|
201
206
|
if (
|
|
202
207
|
err.message.endsWith('All configured authentication methods failed') ||
|
|
@@ -207,8 +212,9 @@ class SftpClient {
|
|
|
207
212
|
retry(err);
|
|
208
213
|
break;
|
|
209
214
|
}
|
|
210
|
-
default:
|
|
215
|
+
default: {
|
|
211
216
|
retry(err);
|
|
217
|
+
}
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
});
|
|
@@ -284,7 +290,7 @@ class SftpClient {
|
|
|
284
290
|
_xstat(cmd, aPath, addListeners = true) {
|
|
285
291
|
let listeners;
|
|
286
292
|
return new Promise((resolve, reject) => {
|
|
287
|
-
|
|
293
|
+
const cb = (err, stats) => {
|
|
288
294
|
if (err) {
|
|
289
295
|
if (err.code === 2 || err.code === 4) {
|
|
290
296
|
reject(this.fmtError(`No such file: ${aPath}`, '_xstat', errorCode.notexist));
|
|
@@ -335,7 +341,6 @@ class SftpClient {
|
|
|
335
341
|
*
|
|
336
342
|
* @param {String} remotePath - path to an object on the remote server
|
|
337
343
|
* @return {Promise<Object>} stats - attributes info
|
|
338
|
-
*
|
|
339
344
|
*/
|
|
340
345
|
async stat(remotePath) {
|
|
341
346
|
try {
|
|
@@ -355,7 +360,6 @@ class SftpClient {
|
|
|
355
360
|
*
|
|
356
361
|
* @param {String} remotePath - path to an object on the remote server
|
|
357
362
|
* @return {Promise<Object>} stats - attributes info
|
|
358
|
-
*
|
|
359
363
|
*/
|
|
360
364
|
async lstat(remotePath) {
|
|
361
365
|
try {
|
|
@@ -436,9 +440,9 @@ class SftpClient {
|
|
|
436
440
|
modifyTime: item.attrs.mtime * 1000,
|
|
437
441
|
accessTime: item.attrs.atime * 1000,
|
|
438
442
|
rights: {
|
|
439
|
-
user: item.longname.slice(1, 4).
|
|
440
|
-
group: item.longname.slice(4, 7).
|
|
441
|
-
other: item.longname.slice(7, 10).
|
|
443
|
+
user: item.longname.slice(1, 4).replaceAll(reg, ''),
|
|
444
|
+
group: item.longname.slice(4, 7).replaceAll(reg, ''),
|
|
445
|
+
other: item.longname.slice(7, 10).replaceAll(reg, ''),
|
|
442
446
|
},
|
|
443
447
|
owner: item.attrs.uid,
|
|
444
448
|
group: item.attrs.gid,
|
|
@@ -489,7 +493,10 @@ class SftpClient {
|
|
|
489
493
|
if (haveConnection(this, 'get', reject)) {
|
|
490
494
|
options = {
|
|
491
495
|
readStreamOptions: { ...options?.readStreamOptions, autoClose: true },
|
|
492
|
-
writeStreamOptions: {
|
|
496
|
+
writeStreamOptions: {
|
|
497
|
+
...options?.writeStreamOptions,
|
|
498
|
+
autoClose: true,
|
|
499
|
+
},
|
|
493
500
|
pipeOptions: { ...options?.pipeOptions, end: true },
|
|
494
501
|
};
|
|
495
502
|
rdr = this.sftp.createReadStream(remotePath, options.readStreamOptions);
|
|
@@ -509,17 +516,16 @@ class SftpClient {
|
|
|
509
516
|
// dst local file path
|
|
510
517
|
this.debugMsg('get returning local file');
|
|
511
518
|
const localCheck = haveLocalCreate(dst);
|
|
512
|
-
if (
|
|
519
|
+
if (localCheck.status) {
|
|
520
|
+
wtr = fs.createWriteStream(dst, options.writeStreamOptions);
|
|
521
|
+
} else {
|
|
513
522
|
reject(
|
|
514
523
|
this.fmtError(
|
|
515
524
|
`Bad path: ${dst}: ${localCheck.details}`,
|
|
516
525
|
'get',
|
|
517
|
-
localCheck.code
|
|
518
|
-
)
|
|
526
|
+
localCheck.code,
|
|
527
|
+
),
|
|
519
528
|
);
|
|
520
|
-
return;
|
|
521
|
-
} else {
|
|
522
|
-
wtr = fs.createWriteStream(dst, options.writeStreamOptions);
|
|
523
529
|
}
|
|
524
530
|
} else {
|
|
525
531
|
this.debugMsg('get: returning data into supplied stream');
|
|
@@ -530,8 +536,8 @@ class SftpClient {
|
|
|
530
536
|
this.fmtError(
|
|
531
537
|
`${err.message} ${typeof dst === 'string' ? dst : '<stream>'}`,
|
|
532
538
|
'get',
|
|
533
|
-
err.code
|
|
534
|
-
)
|
|
539
|
+
err.code,
|
|
540
|
+
),
|
|
535
541
|
);
|
|
536
542
|
});
|
|
537
543
|
rdr.once('end', () => {
|
|
@@ -557,6 +563,10 @@ class SftpClient {
|
|
|
557
563
|
* Downloads a file at remotePath to localPath using parallel reads
|
|
558
564
|
* for faster throughput.
|
|
559
565
|
*
|
|
566
|
+
* WARNING: The functionality of fastGet is heavily dependent on the capabilities
|
|
567
|
+
* of the remote SFTP server. Not all sftp server support or fully support this
|
|
568
|
+
* functionality. See the Platform Quirks & Warnings section of the README.
|
|
569
|
+
*
|
|
560
570
|
* @param {String} remotePath
|
|
561
571
|
* @param {String} localPath
|
|
562
572
|
* @param {Object} options
|
|
@@ -587,7 +597,7 @@ class SftpClient {
|
|
|
587
597
|
try {
|
|
588
598
|
const ftype = await this.exists(remotePath);
|
|
589
599
|
if (ftype !== '-') {
|
|
590
|
-
const msg = `${
|
|
600
|
+
const msg = `${ftype ? 'Not a regular file' : 'No such file '} ${remotePath}`;
|
|
591
601
|
throw this.fmtError(msg, 'fastGet', errorCode.badPath);
|
|
592
602
|
}
|
|
593
603
|
const localCheck = haveLocalCreate(localPath);
|
|
@@ -595,7 +605,7 @@ class SftpClient {
|
|
|
595
605
|
throw this.fmtError(
|
|
596
606
|
`Bad path: ${localPath}: ${localCheck.details}`,
|
|
597
607
|
'fastGet',
|
|
598
|
-
errorCode.badPath
|
|
608
|
+
errorCode.badPath,
|
|
599
609
|
);
|
|
600
610
|
}
|
|
601
611
|
return await this._fastGet(remotePath, localPath, options);
|
|
@@ -612,6 +622,10 @@ class SftpClient {
|
|
|
612
622
|
* See 'fastPut' at
|
|
613
623
|
* https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md
|
|
614
624
|
*
|
|
625
|
+
* WARNING: The fastPut functionality is heavily dependent on the capabilities of
|
|
626
|
+
* the remote sftp server. Many sftp servers do not support or do not fully support this
|
|
627
|
+
* functionality. See the Platform Quirks & Warnings section of the README for more details.
|
|
628
|
+
*
|
|
615
629
|
* @param {String} localPath - path to local file to put
|
|
616
630
|
* @param {String} remotePath - destination path for put file
|
|
617
631
|
* @param {Object} options - additonal fastPut options
|
|
@@ -631,8 +645,8 @@ class SftpClient {
|
|
|
631
645
|
this.fmtError(
|
|
632
646
|
`${err.message} Local: ${lPath} Remote: ${rPath}`,
|
|
633
647
|
'fastPut',
|
|
634
|
-
err.code
|
|
635
|
-
)
|
|
648
|
+
err.code,
|
|
649
|
+
),
|
|
636
650
|
);
|
|
637
651
|
}
|
|
638
652
|
resolve(`${lPath} was successfully uploaded to ${rPath}!`);
|
|
@@ -653,13 +667,13 @@ class SftpClient {
|
|
|
653
667
|
throw this.fmtError(
|
|
654
668
|
`Bad path: ${localPath}: ${localCheck.details}`,
|
|
655
669
|
'fastPut',
|
|
656
|
-
localCheck.code
|
|
670
|
+
localCheck.code,
|
|
657
671
|
);
|
|
658
672
|
} else if (localCheck.status && localExists(localPath) === 'd') {
|
|
659
673
|
throw this.fmtError(
|
|
660
674
|
`Bad path: ${localPath} not a regular file`,
|
|
661
675
|
'fastgPut',
|
|
662
|
-
errorCode.badPath
|
|
676
|
+
errorCode.badPath,
|
|
663
677
|
);
|
|
664
678
|
}
|
|
665
679
|
return await this._fastPut(localPath, remotePath, options);
|
|
@@ -700,7 +714,11 @@ class SftpClient {
|
|
|
700
714
|
wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
|
|
701
715
|
wtr.once('error', (err) => {
|
|
702
716
|
reject(
|
|
703
|
-
this.fmtError(
|
|
717
|
+
this.fmtError(
|
|
718
|
+
`Write stream error: ${err.message} ${rPath}`,
|
|
719
|
+
'_put',
|
|
720
|
+
err.code,
|
|
721
|
+
),
|
|
704
722
|
);
|
|
705
723
|
});
|
|
706
724
|
wtr.once('close', () => {
|
|
@@ -721,8 +739,8 @@ class SftpClient {
|
|
|
721
739
|
typeof lPath === 'string' ? lPath : '<stream>'
|
|
722
740
|
}`,
|
|
723
741
|
'_put',
|
|
724
|
-
err.code
|
|
725
|
-
)
|
|
742
|
+
err.code,
|
|
743
|
+
),
|
|
726
744
|
);
|
|
727
745
|
});
|
|
728
746
|
rdr.pipe(wtr, opts.pipeOptions);
|
|
@@ -743,7 +761,7 @@ class SftpClient {
|
|
|
743
761
|
throw this.fmtError(
|
|
744
762
|
`Bad path: ${localSrc} ${localCheck.details}`,
|
|
745
763
|
'put',
|
|
746
|
-
localCheck.code
|
|
764
|
+
localCheck.code,
|
|
747
765
|
);
|
|
748
766
|
}
|
|
749
767
|
}
|
|
@@ -797,7 +815,7 @@ class SftpClient {
|
|
|
797
815
|
throw this.fmtError(
|
|
798
816
|
'Cannot append one file to another',
|
|
799
817
|
'append',
|
|
800
|
-
errorCode.badPath
|
|
818
|
+
errorCode.badPath,
|
|
801
819
|
);
|
|
802
820
|
}
|
|
803
821
|
const fileType = await this.exists(remotePath);
|
|
@@ -805,7 +823,7 @@ class SftpClient {
|
|
|
805
823
|
throw this.fmtError(
|
|
806
824
|
`Bad path: ${remotePath}: cannot append to a directory`,
|
|
807
825
|
'append',
|
|
808
|
-
errorCode.badPath
|
|
826
|
+
errorCode.badPath,
|
|
809
827
|
);
|
|
810
828
|
}
|
|
811
829
|
await this._append(input, remotePath, options);
|
|
@@ -837,16 +855,16 @@ class SftpClient {
|
|
|
837
855
|
this.fmtError(
|
|
838
856
|
`Bad path: ${p} permission denied`,
|
|
839
857
|
'_doMkdir',
|
|
840
|
-
errorCode.badPath
|
|
841
|
-
)
|
|
858
|
+
errorCode.badPath,
|
|
859
|
+
),
|
|
842
860
|
);
|
|
843
861
|
} else if (err.code === 2) {
|
|
844
862
|
reject(
|
|
845
863
|
this.fmtError(
|
|
846
864
|
`Bad path: ${p} parent not a directory or not exist`,
|
|
847
865
|
'_doMkdir',
|
|
848
|
-
errorCode.badPath
|
|
849
|
-
)
|
|
866
|
+
errorCode.badPath,
|
|
867
|
+
),
|
|
850
868
|
);
|
|
851
869
|
} else {
|
|
852
870
|
reject(this.fmtError(`${err.message} ${p}`, '_doMkdir', err.code));
|
|
@@ -870,7 +888,7 @@ class SftpClient {
|
|
|
870
888
|
throw this.fmtError(
|
|
871
889
|
`Bad path: ${rPath} already exists as a file`,
|
|
872
890
|
'_mkdir',
|
|
873
|
-
errorCode.badPath
|
|
891
|
+
errorCode.badPath,
|
|
874
892
|
);
|
|
875
893
|
} else if (targetExists) {
|
|
876
894
|
return `${rPath} already exists`;
|
|
@@ -887,7 +905,7 @@ class SftpClient {
|
|
|
887
905
|
throw this.fmtError(
|
|
888
906
|
`Bad path: ${dir} not a directory`,
|
|
889
907
|
'_mkdir',
|
|
890
|
-
errorCode.badPath
|
|
908
|
+
errorCode.badPath,
|
|
891
909
|
);
|
|
892
910
|
}
|
|
893
911
|
}
|
|
@@ -940,7 +958,7 @@ class SftpClient {
|
|
|
940
958
|
return new Promise((resolve, reject) => {
|
|
941
959
|
listeners = addTempListeners(this, '_delFiles', reject);
|
|
942
960
|
this.debugMsg(`_delFiles: path = ${path} fileList = ${fileList}`);
|
|
943
|
-
|
|
961
|
+
const pList = [];
|
|
944
962
|
for (const f of fileList) {
|
|
945
963
|
pList.push(this.delete(`${path}/${f.name}`, true, false));
|
|
946
964
|
}
|
|
@@ -956,36 +974,36 @@ class SftpClient {
|
|
|
956
974
|
|
|
957
975
|
try {
|
|
958
976
|
this.debugMsg(`rmdir: dir = ${remoteDir} recursive = ${recursive}`);
|
|
959
|
-
|
|
960
|
-
|
|
977
|
+
const absPath = await normalizeRemotePath(this, remoteDir);
|
|
978
|
+
const existStatus = await this.exists(absPath);
|
|
961
979
|
this.debugMsg(`rmdir: ${absPath} existStatus = ${existStatus}`);
|
|
962
980
|
if (!existStatus) {
|
|
963
981
|
throw this.fmtError(
|
|
964
982
|
`Bad Path: ${remoteDir}: No such directory`,
|
|
965
983
|
'rmdir',
|
|
966
|
-
errorCode.badPath
|
|
984
|
+
errorCode.badPath,
|
|
967
985
|
);
|
|
968
986
|
}
|
|
969
987
|
if (existStatus !== 'd') {
|
|
970
988
|
throw this.fmtError(
|
|
971
989
|
`Bad Path: ${remoteDir}: Not a directory`,
|
|
972
990
|
'rmdir',
|
|
973
|
-
errorCode.badPath
|
|
991
|
+
errorCode.badPath,
|
|
974
992
|
);
|
|
975
993
|
}
|
|
976
994
|
if (!recursive) {
|
|
977
995
|
this.debugMsg('rmdir: non-recursive - just try to remove it');
|
|
978
996
|
return await _rmdir(absPath);
|
|
979
997
|
}
|
|
980
|
-
|
|
998
|
+
const listing = await this.list(absPath);
|
|
981
999
|
this.debugMsg(`rmdir: listing count = ${listing.length}`);
|
|
982
1000
|
if (!listing.length) {
|
|
983
1001
|
this.debugMsg('rmdir: No sub dir or files, just rmdir');
|
|
984
1002
|
return await _rmdir(absPath);
|
|
985
1003
|
}
|
|
986
|
-
|
|
1004
|
+
const fileList = listing.filter((i) => i.type !== 'd');
|
|
987
1005
|
this.debugMsg(`rmdir: dir content files to remove = ${fileList.length}`);
|
|
988
|
-
|
|
1006
|
+
const dirList = listing.filter((i) => i.type === 'd');
|
|
989
1007
|
this.debugMsg(`rmdir: sub-directories to remove = ${dirList.length}`);
|
|
990
1008
|
await _delFiles(absPath, fileList);
|
|
991
1009
|
for (const d of dirList) {
|
|
@@ -1009,7 +1027,6 @@ class SftpClient {
|
|
|
1009
1027
|
* @param {boolean} notFoundOK - if true, ignore errors for missing target.
|
|
1010
1028
|
* Default is false.
|
|
1011
1029
|
* @return {Promise<String>} with string 'Successfully deleted file' once resolved
|
|
1012
|
-
*
|
|
1013
1030
|
*/
|
|
1014
1031
|
delete(remotePath, notFoundOK = false, addListeners = true) {
|
|
1015
1032
|
let listeners;
|
|
@@ -1044,7 +1061,6 @@ class SftpClient {
|
|
|
1044
1061
|
* @param {Boolean} addListeners - (Optional) if true, add listeners. Default true
|
|
1045
1062
|
*
|
|
1046
1063
|
* @return {Promise<String>}
|
|
1047
|
-
*
|
|
1048
1064
|
*/
|
|
1049
1065
|
rename(fPath, tPath, addListeners = true) {
|
|
1050
1066
|
let listeners;
|
|
@@ -1059,8 +1075,8 @@ class SftpClient {
|
|
|
1059
1075
|
this.fmtError(
|
|
1060
1076
|
`${err.message} From: ${fPath} To: ${tPath}`,
|
|
1061
1077
|
'_rename',
|
|
1062
|
-
err.code
|
|
1063
|
-
)
|
|
1078
|
+
err.code,
|
|
1079
|
+
),
|
|
1064
1080
|
);
|
|
1065
1081
|
}
|
|
1066
1082
|
resolve(`Successfully renamed ${fPath} to ${tPath}`);
|
|
@@ -1084,7 +1100,6 @@ class SftpClient {
|
|
|
1084
1100
|
* @param {Boolean} addListeners - (Optional) if true, add listeners. Default true
|
|
1085
1101
|
*
|
|
1086
1102
|
* @return {Promise<String>}
|
|
1087
|
-
*
|
|
1088
1103
|
*/
|
|
1089
1104
|
posixRename(fPath, tPath, addListeners = true) {
|
|
1090
1105
|
let listeners;
|
|
@@ -1099,8 +1114,8 @@ class SftpClient {
|
|
|
1099
1114
|
this.fmtError(
|
|
1100
1115
|
`${err.message} From: ${fPath} To: ${tPath}`,
|
|
1101
1116
|
'_posixRename',
|
|
1102
|
-
err.code
|
|
1103
|
-
)
|
|
1117
|
+
err.code,
|
|
1118
|
+
),
|
|
1104
1119
|
);
|
|
1105
1120
|
}
|
|
1106
1121
|
resolve(`Successful POSIX rename ${fPath} to ${tPath}`);
|
|
@@ -1164,13 +1179,13 @@ class SftpClient {
|
|
|
1164
1179
|
*/
|
|
1165
1180
|
async uploadDir(srcDir, dstDir, options) {
|
|
1166
1181
|
const getRemoteStatus = async (dstDir) => {
|
|
1167
|
-
|
|
1168
|
-
|
|
1182
|
+
const absDstDir = await normalizeRemotePath(this, dstDir);
|
|
1183
|
+
const status = await this.exists(absDstDir);
|
|
1169
1184
|
if (status && status !== 'd') {
|
|
1170
1185
|
throw this.fmtError(
|
|
1171
1186
|
`Bad path ${absDstDir} Not a directory`,
|
|
1172
1187
|
'getRemoteStatus',
|
|
1173
|
-
errorCode.badPath
|
|
1188
|
+
errorCode.badPath,
|
|
1174
1189
|
);
|
|
1175
1190
|
}
|
|
1176
1191
|
return { remoteDir: absDstDir, remoteStatus: status };
|
|
@@ -1182,54 +1197,57 @@ class SftpClient {
|
|
|
1182
1197
|
throw this.fmtError(
|
|
1183
1198
|
`Bad path: ${srcDir} not exist`,
|
|
1184
1199
|
'getLocalStatus',
|
|
1185
|
-
errorCode.badPath
|
|
1200
|
+
errorCode.badPath,
|
|
1186
1201
|
);
|
|
1187
1202
|
}
|
|
1188
1203
|
if (srcType !== 'd') {
|
|
1189
1204
|
throw this.fmtError(
|
|
1190
1205
|
`Bad path: ${srcDir}: not a directory`,
|
|
1191
1206
|
'getLocalStatus',
|
|
1192
|
-
errorCode.badPath
|
|
1207
|
+
errorCode.badPath,
|
|
1193
1208
|
);
|
|
1194
1209
|
}
|
|
1195
1210
|
return srcType;
|
|
1196
1211
|
};
|
|
1197
1212
|
|
|
1198
|
-
const uploadFiles = (srcDir, dstDir, fileList, useFastput) => {
|
|
1199
|
-
let listeners;
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1213
|
+
const uploadFiles = async (srcDir, dstDir, fileList, useFastput) => {
|
|
1214
|
+
let listeners = addTempListeners(this, 'uploadFiles');
|
|
1215
|
+
|
|
1216
|
+
try {
|
|
1217
|
+
const uploadList = [];
|
|
1203
1218
|
for (const f of fileList) {
|
|
1204
|
-
const
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1219
|
+
const src = join(srcDir, f.name);
|
|
1220
|
+
const dst = `${dstDir}/${f.name}`;
|
|
1221
|
+
uploadList.push([src, dst]);
|
|
1222
|
+
}
|
|
1223
|
+
const uploadGroups = partition(uploadList, this.promiseLimit);
|
|
1224
|
+
const func = useFastput ? this._fastPut.bind(this) : this._put.bind(this);
|
|
1225
|
+
const uploadResults = [];
|
|
1226
|
+
for (const group of uploadGroups) {
|
|
1227
|
+
const pList = [];
|
|
1228
|
+
for (const [src, dst] of group) {
|
|
1229
|
+
pList.push(func(src, dst, null, false));
|
|
1230
|
+
this.client.emit('upload', { source: src, destination: dst });
|
|
1231
|
+
}
|
|
1232
|
+
const groupResults = await Promise.all(pList);
|
|
1233
|
+
for (const r of groupResults) {
|
|
1234
|
+
uploadResults.push(r);
|
|
1215
1235
|
}
|
|
1216
1236
|
}
|
|
1217
|
-
|
|
1218
|
-
})
|
|
1219
|
-
.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
removeTempListeners(this, listeners, uploadFiles);
|
|
1224
|
-
});
|
|
1237
|
+
return uploadResults;
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
throw this.fmtError(`${e.message} ${srcDir} to ${dstDir}`, 'uploadFiles', e.code);
|
|
1240
|
+
} finally {
|
|
1241
|
+
removeTempListeners(this, listeners, uploadFiles);
|
|
1242
|
+
}
|
|
1225
1243
|
};
|
|
1226
1244
|
|
|
1227
1245
|
try {
|
|
1228
1246
|
haveConnection(this, 'uploadDir');
|
|
1229
1247
|
this.debugMsg(
|
|
1230
|
-
`uploadDir: srcDir = ${srcDir} dstDir = ${dstDir} options = ${options}
|
|
1248
|
+
`uploadDir: srcDir = ${srcDir} dstDir = ${dstDir} options = ${options}`,
|
|
1231
1249
|
);
|
|
1232
|
-
|
|
1250
|
+
const { remoteDir, remoteStatus } = await getRemoteStatus(dstDir);
|
|
1233
1251
|
this.debugMsg(`uploadDir: remoteDir = ${remoteDir} remoteStatus = ${remoteStatus}`);
|
|
1234
1252
|
checkLocalStatus(srcDir);
|
|
1235
1253
|
if (!remoteStatus) {
|
|
@@ -1242,17 +1260,17 @@ class SftpClient {
|
|
|
1242
1260
|
this.debugMsg(`uploadDir: dirEntries = ${dirEntries}`);
|
|
1243
1261
|
if (options?.filter) {
|
|
1244
1262
|
dirEntries = dirEntries.filter((item) =>
|
|
1245
|
-
options.filter(join(srcDir, item.name), item.isDirectory())
|
|
1263
|
+
options.filter(join(srcDir, item.name), item.isDirectory()),
|
|
1246
1264
|
);
|
|
1247
1265
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1266
|
+
const dirUploads = dirEntries.filter((item) => item.isDirectory());
|
|
1267
|
+
const fileUploads = dirEntries.filter((item) => !item.isDirectory());
|
|
1250
1268
|
this.debugMsg(`uploadDir: dirUploads = ${dirUploads}`);
|
|
1251
1269
|
this.debugMsg(`uploadDir: fileUploads = ${fileUploads}`);
|
|
1252
1270
|
await uploadFiles(srcDir, remoteDir, fileUploads, options?.useFastput);
|
|
1253
1271
|
for (const d of dirUploads) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1272
|
+
const src = join(srcDir, d.name);
|
|
1273
|
+
const dst = `${remoteDir}/${d.name}`;
|
|
1256
1274
|
await this.uploadDir(src, dst, options);
|
|
1257
1275
|
}
|
|
1258
1276
|
return `${srcDir} uploaded to ${dstDir}`;
|
|
@@ -1281,12 +1299,12 @@ class SftpClient {
|
|
|
1281
1299
|
* @returns {Promise<Array>}
|
|
1282
1300
|
*/
|
|
1283
1301
|
async downloadDir(srcDir, dstDir, options = { filter: null, useFastget: false }) {
|
|
1284
|
-
const
|
|
1302
|
+
const getDownloadList = async (srcDir, filter) => {
|
|
1285
1303
|
try {
|
|
1286
|
-
|
|
1304
|
+
const listing = await this.list(srcDir);
|
|
1287
1305
|
if (filter) {
|
|
1288
1306
|
return listing.filter((item) =>
|
|
1289
|
-
filter(`${srcDir}/${item.name}`, item.type === 'd')
|
|
1307
|
+
filter(`${srcDir}/${item.name}`, item.type === 'd'),
|
|
1290
1308
|
);
|
|
1291
1309
|
}
|
|
1292
1310
|
return listing;
|
|
@@ -1295,14 +1313,14 @@ class SftpClient {
|
|
|
1295
1313
|
}
|
|
1296
1314
|
};
|
|
1297
1315
|
|
|
1298
|
-
const
|
|
1316
|
+
const prepareDestination = (dst) => {
|
|
1299
1317
|
try {
|
|
1300
1318
|
const localCheck = haveLocalCreate(dst);
|
|
1301
1319
|
if (!localCheck.status && localCheck.details === 'permission denied') {
|
|
1302
1320
|
throw this.fmtError(
|
|
1303
1321
|
`Bad path: ${dst}: ${localCheck.details}`,
|
|
1304
1322
|
'prepareDestination',
|
|
1305
|
-
localCheck.code
|
|
1323
|
+
localCheck.code,
|
|
1306
1324
|
);
|
|
1307
1325
|
} else if (localCheck.status && !localCheck.type) {
|
|
1308
1326
|
fs.mkdirSync(dst, { recursive: true });
|
|
@@ -1310,7 +1328,7 @@ class SftpClient {
|
|
|
1310
1328
|
throw this.fmtError(
|
|
1311
1329
|
`Bad path: ${dstDir}: not a directory`,
|
|
1312
1330
|
'_prepareDestination',
|
|
1313
|
-
errorCode.badPath
|
|
1331
|
+
errorCode.badPath,
|
|
1314
1332
|
);
|
|
1315
1333
|
}
|
|
1316
1334
|
} catch (err) {
|
|
@@ -1320,40 +1338,55 @@ class SftpClient {
|
|
|
1320
1338
|
}
|
|
1321
1339
|
};
|
|
1322
1340
|
|
|
1323
|
-
const
|
|
1324
|
-
let listeners;
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1341
|
+
const downloadFiles = async (remotePath, localPath, fileList, useFastget) => {
|
|
1342
|
+
let listeners = addTempListeners(this, 'downloadFIles');
|
|
1343
|
+
|
|
1344
|
+
try {
|
|
1345
|
+
const downloadList = [];
|
|
1328
1346
|
for (const f of fileList) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1347
|
+
const src = `${remotePath}/${f.name}`;
|
|
1348
|
+
const dst = join(localPath, f.name);
|
|
1349
|
+
downloadList.push([src, dst]);
|
|
1350
|
+
}
|
|
1351
|
+
const downloadGroups = partition(downloadList, this.promiseLimit);
|
|
1352
|
+
const func = useFastget ? this._fastGet.bind(this) : this.get.bind(this);
|
|
1353
|
+
const downloadResults = [];
|
|
1354
|
+
for (const group of downloadGroups) {
|
|
1355
|
+
const pList = [];
|
|
1356
|
+
for (const [src, dst] of group) {
|
|
1357
|
+
pList.push(func(src, dst, null, false));
|
|
1358
|
+
this.client.emit('download', { source: src, destination: dst });
|
|
1359
|
+
}
|
|
1360
|
+
const groupResults = await Promise.all(pList);
|
|
1361
|
+
for (const r of groupResults) {
|
|
1362
|
+
downloadResults.push(r);
|
|
1335
1363
|
}
|
|
1336
|
-
this.client.emit('download', { source: src, destination: dst });
|
|
1337
1364
|
}
|
|
1338
|
-
return
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1365
|
+
return downloadResults;
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
throw this.fmtError(
|
|
1368
|
+
`${e.message} ${srcDir} to ${dstDir}`,
|
|
1369
|
+
'downloadFiles',
|
|
1370
|
+
e.code,
|
|
1371
|
+
);
|
|
1372
|
+
} finally {
|
|
1373
|
+
removeTempListeners(this, listeners, 'downloadFiles');
|
|
1374
|
+
}
|
|
1342
1375
|
};
|
|
1343
1376
|
|
|
1344
1377
|
try {
|
|
1345
1378
|
haveConnection(this, 'downloadDir');
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1379
|
+
const downloadList = await getDownloadList(srcDir, options.filter);
|
|
1380
|
+
prepareDestination(dstDir);
|
|
1381
|
+
const fileDownloads = downloadList.filter((i) => i.type !== 'd');
|
|
1349
1382
|
if (fileDownloads.length) {
|
|
1350
|
-
await
|
|
1383
|
+
await downloadFiles(srcDir, dstDir, fileDownloads, options.useFastget);
|
|
1351
1384
|
}
|
|
1352
|
-
|
|
1385
|
+
const dirDownloads = downloadList.filter((i) => i.type === 'd');
|
|
1353
1386
|
for (const d of dirDownloads) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
await this.downloadDir(src, dst);
|
|
1387
|
+
const src = `${srcDir}/${d.name}`;
|
|
1388
|
+
const dst = join(dstDir, d.name);
|
|
1389
|
+
await this.downloadDir(src, dst, options);
|
|
1357
1390
|
}
|
|
1358
1391
|
return `${srcDir} downloaded to ${dstDir}`;
|
|
1359
1392
|
} catch (err) {
|
|
@@ -1364,7 +1397,6 @@ class SftpClient {
|
|
|
1364
1397
|
}
|
|
1365
1398
|
|
|
1366
1399
|
/**
|
|
1367
|
-
*
|
|
1368
1400
|
* Returns a read stream object. This is a low level method which will return a read stream
|
|
1369
1401
|
* connected to the remote file object specified as an argument. Client code is fully responsible
|
|
1370
1402
|
* for managing this stream object i.e. adding any necessary listeners and disposing of the object etc.
|
|
@@ -1374,7 +1406,6 @@ class SftpClient {
|
|
|
1374
1406
|
* @param {Object} options - options to pass to the create stream process
|
|
1375
1407
|
*
|
|
1376
1408
|
* @returns {Object} a read stream object
|
|
1377
|
-
*
|
|
1378
1409
|
*/
|
|
1379
1410
|
createReadStream(remotePath, options) {
|
|
1380
1411
|
let listeners;
|
|
@@ -1391,7 +1422,6 @@ class SftpClient {
|
|
|
1391
1422
|
}
|
|
1392
1423
|
|
|
1393
1424
|
/**
|
|
1394
|
-
*
|
|
1395
1425
|
* Create a write stream object connected to a file on the remote sftp server.
|
|
1396
1426
|
* This is a low level method which will return a write stream for the remote file specified
|
|
1397
1427
|
* in the 'remotePath' argument. Client code to responsible for managing this object once created.
|
|
@@ -1401,7 +1431,6 @@ class SftpClient {
|
|
|
1401
1431
|
* @param (Object} options - options to pass to the create write stream process)
|
|
1402
1432
|
*
|
|
1403
1433
|
* @returns {Object} a stream object
|
|
1404
|
-
*
|
|
1405
1434
|
*/
|
|
1406
1435
|
createWriteStream(remotePath, options) {
|
|
1407
1436
|
let listeners;
|
|
@@ -1428,7 +1457,6 @@ class SftpClient {
|
|
|
1428
1457
|
* @param {String} dstPath - destination path for the copy.
|
|
1429
1458
|
*
|
|
1430
1459
|
* @returns {String}.
|
|
1431
|
-
*
|
|
1432
1460
|
*/
|
|
1433
1461
|
_rcopy(srcPath, dstPath) {
|
|
1434
1462
|
return new Promise((resolve, reject) => {
|
|
@@ -1458,7 +1486,7 @@ class SftpClient {
|
|
|
1458
1486
|
throw this.fmtError(
|
|
1459
1487
|
`Source does not exist ${srcPath}`,
|
|
1460
1488
|
'rcopy',
|
|
1461
|
-
errorCode.badPath
|
|
1489
|
+
errorCode.badPath,
|
|
1462
1490
|
);
|
|
1463
1491
|
}
|
|
1464
1492
|
if (srcExists !== '-') {
|
|
@@ -1470,7 +1498,7 @@ class SftpClient {
|
|
|
1470
1498
|
throw this.fmtError(
|
|
1471
1499
|
`Destination already exists ${dstPath}`,
|
|
1472
1500
|
'rcopy',
|
|
1473
|
-
errorCode.badPath
|
|
1501
|
+
errorCode.badPath,
|
|
1474
1502
|
);
|
|
1475
1503
|
}
|
|
1476
1504
|
return this._rcopy(srcPath, dstPath);
|