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.
Files changed (5) hide show
  1. package/README.md +149 -140
  2. package/README.org +69 -20
  3. package/package.json +10 -10
  4. package/src/index.js +167 -139
  5. 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 = '9.0.4';
23
+ this.version = '10.0.0';
23
24
  this.client = new Client();
24
25
  this.sftp = undefined;
25
- this.clientName = clientName ? clientName : 'sftp';
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 ? eCode : errorCode.generic;
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
- case 'ECONNREFUSED':
72
+ }
73
+ case 'ECONNREFUSED': {
72
74
  msg = `${name}: Remote host refused connection${retry}`;
73
75
  break;
74
- case 'ECONNRESET':
76
+ }
77
+ case 'ECONNRESET': {
75
78
  msg = `${name}: Remote host has reset the connection: ${err.message}${retry}`;
76
79
  break;
77
- default:
80
+ }
81
+ default: {
78
82
  msg = `${name}: ${err.message}${retry}`;
83
+ }
79
84
  }
80
- code = err.code ? err.code : errorCode.generic;
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.factor ?? 2,
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
- let cb = (err, stats) => {
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).replace(reg, ''),
440
- group: item.longname.slice(4, 7).replace(reg, ''),
441
- other: item.longname.slice(7, 10).replace(reg, ''),
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: { ...options?.writeStreamOptions, autoClose: true },
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 (!localCheck.status) {
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 = `${!ftype ? 'No such file ' : 'Not a regular file'} ${remotePath}`;
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(`Write stream error: ${err.message} ${rPath}`, '_put', err.code)
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
- let pList = [];
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
- let absPath = await normalizeRemotePath(this, remoteDir);
960
- let existStatus = await this.exists(absPath);
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
- let listing = await this.list(absPath);
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
- let fileList = listing.filter((i) => i.type !== 'd');
1004
+ const fileList = listing.filter((i) => i.type !== 'd');
987
1005
  this.debugMsg(`rmdir: dir content files to remove = ${fileList.length}`);
988
- let dirList = listing.filter((i) => i.type === 'd');
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
- let absDstDir = await normalizeRemotePath(this, dstDir);
1168
- let status = await this.exists(absDstDir);
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
- return new Promise((resolve, reject) => {
1201
- listeners = addTempListeners(this, 'uploadFiles', reject);
1202
- let uploads = [];
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 newSrc = join(srcDir, f.name);
1205
- const newDst = `${dstDir}/${f.name}`;
1206
- if (f.isFile()) {
1207
- if (useFastput) {
1208
- uploads.push(this._fastPut(newSrc, newDst, null, false));
1209
- } else {
1210
- uploads.push(this._put(newSrc, newDst, null, false));
1211
- }
1212
- this.client.emit('upload', { source: newSrc, destination: newDst });
1213
- } else {
1214
- this.debugMsg(`uploadFiles: File ignored: ${f.name} not a regular file`);
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
- resolve(Promise.all(uploads));
1218
- })
1219
- .then((pList) => {
1220
- return Promise.all(pList);
1221
- })
1222
- .finally(() => {
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
- let { remoteDir, remoteStatus } = await getRemoteStatus(dstDir);
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
- let dirUploads = dirEntries.filter((item) => item.isDirectory());
1249
- let fileUploads = dirEntries.filter((item) => !item.isDirectory());
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
- let src = join(srcDir, d.name);
1255
- let dst = `${remoteDir}/${d.name}`;
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 _getDownloadList = async (srcDir, filter) => {
1302
+ const getDownloadList = async (srcDir, filter) => {
1285
1303
  try {
1286
- let listing = await this.list(srcDir);
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 _prepareDestination = (dst) => {
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 _downloadFiles = (remotePath, localPath, fileList, useFastget) => {
1324
- let listeners;
1325
- return new Promise((resolve, reject) => {
1326
- listeners = addTempListeners(this, '_downloadFIles', reject);
1327
- let pList = [];
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
- let src = `${remotePath}/${f.name}`;
1330
- let dst = join(localPath, f.name);
1331
- if (useFastget) {
1332
- pList.push(this.fastGet(src, dst, false));
1333
- } else {
1334
- pList.push(this.get(src, dst, false));
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 resolve(Promise.all(pList));
1339
- }).finally(() => {
1340
- removeTempListeners(this, listeners, '_downloadFiles');
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
- let downloadList = await _getDownloadList(srcDir, options.filter);
1347
- _prepareDestination(dstDir);
1348
- let fileDownloads = downloadList.filter((i) => i.type !== 'd');
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 _downloadFiles(srcDir, dstDir, fileDownloads, options.useFastget);
1383
+ await downloadFiles(srcDir, dstDir, fileDownloads, options.useFastget);
1351
1384
  }
1352
- let dirDownloads = downloadList.filter((i) => i.type === 'd');
1385
+ const dirDownloads = downloadList.filter((i) => i.type === 'd');
1353
1386
  for (const d of dirDownloads) {
1354
- let src = `${srcDir}/${d.name}`;
1355
- let dst = join(dstDir, d.name);
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);