ssh2-sftp-client 9.0.4 → 9.1.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/src/index.js CHANGED
@@ -6,6 +6,7 @@ const concat = require('concat-stream');
6
6
  const promiseRetry = require('promise-retry');
7
7
  const { join, parse } = require('path');
8
8
  const {
9
+ globalListener,
9
10
  addTempListeners,
10
11
  removeTempListeners,
11
12
  haveConnection,
@@ -26,42 +27,12 @@ class SftpClient {
26
27
  this.errorHandled = false;
27
28
  this.closeHandled = false;
28
29
  this.endHandled = false;
29
- this.remotePathSep = '/';
30
30
  this.remotePlatform = 'unix';
31
31
  this.debug = undefined;
32
32
 
33
- this.client.on('close', () => {
34
- if (this.endCalled || this.errorHandled || this.closeHandled) {
35
- // we are processing an expected end event or close event handled elsewhere
36
- this.debugMsg('Global: Ignoring handled close event');
37
- } else {
38
- this.debugMsg('Global: Handling unexpected close event');
39
- this.sftp = undefined;
40
- }
41
- });
42
-
43
- this.client.on('end', () => {
44
- if (this.endCalled || this.errorHandled || this.endHandled) {
45
- // end event expected or handled elsewhere
46
- this.debugMsg('Global: Ignoring hanlded end event');
47
- } else {
48
- this.debugMsg('Global: Handling unexpected end event');
49
- this.sftp = undefined;
50
- }
51
- });
52
-
53
- this.client.on('error', (err) => {
54
- if (this.endCalled || this.errorHandled) {
55
- // error event expected or handled elsewhere
56
- this.debugMsg(`Global: Ignoring handled error: ${err.message}`);
57
- } else {
58
- this.debugMsg(`Global; Handling unexpected error; ${err.message}`);
59
- this.sftp = undefined;
60
- console.log(
61
- `ssh2-sftp-client: Unexpected error: ${err.message}. Error code: ${err.code}`
62
- );
63
- }
64
- });
33
+ this.client.on('close', globalListener(this, 'close'));
34
+ this.client.on('end', globalListener(this, 'end'));
35
+ this.client.on('error', globalListener(this, 'error'));
65
36
  }
66
37
 
67
38
  debugMsg(msg, obj) {
@@ -175,7 +146,6 @@ class SftpClient {
175
146
  if (err) {
176
147
  reject(this.fmtError(err, 'getSftpChannel', err.code));
177
148
  } else {
178
- this.debugMsg('getSftpChannel: SFTP channel established');
179
149
  this.sftp = sftp;
180
150
  resolve(sftp);
181
151
  }
@@ -261,41 +231,36 @@ class SftpClient {
261
231
  * Returns undefined if the path does not exists.
262
232
  *
263
233
  * @param {String} remotePath - remote path, may be relative
234
+ * @param {Boolean} addListeners - (Optional) add event listeners. Default = true
264
235
  * @returns {Promise<String>} - remote absolute path or ''
265
236
  */
266
- _realPath(rPath) {
237
+ realPath(remotePath, addListeners = true) {
238
+ let listeners;
267
239
  return new Promise((resolve, reject) => {
268
- this.debugMsg(`_realPath -> ${rPath}`);
269
- this.sftp.realpath(rPath, (err, absPath) => {
240
+ if (addListeners) {
241
+ listeners = addTempListeners(this, 'realPath', reject);
242
+ }
243
+ this.debugMsg(`realPath -> ${remotePath}`);
244
+ this.sftp.realpath(remotePath, (err, absPath) => {
270
245
  if (err) {
271
246
  if (err.code === 2) {
272
- this.debugMsg('_realPath <- ""');
247
+ this.debugMsg('realPath <- ""');
273
248
  resolve('');
274
249
  } else {
275
- reject(this.fmtError(`${err.message} ${rPath}`, 'realPath', err.code));
250
+ this.debugMsg(`${err.message} ${remotePath}`, 'realPath');
251
+ reject(this.fmtError(`${err.message} ${remotePath}`, 'realPath', err.code));
276
252
  }
277
253
  }
278
- this.debugMsg(`_realPath <- ${absPath}`);
254
+ this.debugMsg(`realPath <- ${absPath}`);
279
255
  resolve(absPath);
280
256
  });
257
+ }).finally(() => {
258
+ if (addListeners) {
259
+ removeTempListeners(this, listeners, 'realPath');
260
+ }
281
261
  });
282
262
  }
283
263
 
284
- async realPath(remotePath) {
285
- let listeners;
286
- try {
287
- listeners = addTempListeners(this, 'realPath');
288
- haveConnection(this, 'realPath');
289
- return await this._realPath(remotePath);
290
- } catch (e) {
291
- throw e.custom
292
- ? e
293
- : this.fmtError(`${e.message} ${remotePath}`, 'realPath', e.code);
294
- } finally {
295
- removeTempListeners(this, listeners, 'realPath');
296
- }
297
- }
298
-
299
264
  /**
300
265
  * @async
301
266
  *
@@ -308,20 +273,23 @@ class SftpClient {
308
273
  }
309
274
 
310
275
  /**
311
- * Retrieves attributes for path
276
+ * Retrieves attributes for path using cmd, which is either
277
+ * this.sftp.stat or this.sftp.lstat
312
278
  *
279
+ * @param {Function} cmd - either this.sftp.stat or this.sftp.lstat
313
280
  * @param {String} remotePath - a string containing the path to a file
281
+ * @param {Boolean} addListeners - (Optional) if true add event listeners. Default true.
314
282
  * @return {Promise<Object>} stats - attributes info
315
283
  */
316
- _stat(aPath) {
284
+ _xstat(cmd, aPath, addListeners = true) {
285
+ let listeners;
317
286
  return new Promise((resolve, reject) => {
318
- this.debugMsg(`_stat: ${aPath}`);
319
- this.sftp.stat(aPath, (err, stats) => {
287
+ let cb = (err, stats) => {
320
288
  if (err) {
321
289
  if (err.code === 2 || err.code === 4) {
322
- reject(this.fmtError(`No such file: ${aPath}`, '_stat', errorCode.notexist));
290
+ reject(this.fmtError(`No such file: ${aPath}`, '_xstat', errorCode.notexist));
323
291
  } else {
324
- reject(this.fmtError(`${err.message} ${aPath}`, '_stat', err.code));
292
+ reject(this.fmtError(`${err.message} ${aPath}`, '_xstat', err.code));
325
293
  }
326
294
  } else {
327
295
  const result = {
@@ -339,24 +307,62 @@ class SftpClient {
339
307
  isFIFO: stats.isFIFO(),
340
308
  isSocket: stats.isSocket(),
341
309
  };
342
- this.debugMsg('_stat: stats <- ', result);
310
+ this.debugMsg('_xstat: result = ', result);
343
311
  resolve(result);
344
312
  }
345
- });
313
+ };
314
+ if (addListeners) {
315
+ listeners = addTempListeners(this, '_xstat', reject);
316
+ }
317
+ if (cmd === 'stat') {
318
+ this.sftp.stat(aPath, cb);
319
+ } else {
320
+ this.sftp.lstat(aPath, cb);
321
+ }
322
+ }).finally(() => {
323
+ if (addListeners) {
324
+ removeTempListeners(this, listeners, '_xstat');
325
+ }
346
326
  });
347
327
  }
348
328
 
329
+ /*
330
+ * Use the stat command to obtain attributes associated with a remote path.
331
+ * THe difference between stat and lstat is that stat, in the case of symbolic
332
+ * links, will return the attributes associated with the target of the link. With
333
+ * lstat, attributes associated with the symbolic link rather than the target are
334
+ * returned.
335
+ *
336
+ * @param {String} remotePath - path to an object on the remote server
337
+ * @return {Promise<Object>} stats - attributes info
338
+ *
339
+ */
349
340
  async stat(remotePath) {
350
- let listeners;
351
341
  try {
352
- listeners = addTempListeners(this, 'stat');
353
342
  haveConnection(this, 'stat');
354
- const absPath = await normalizeRemotePath(this, remotePath);
355
- return await this._stat(absPath);
343
+ return await this._xstat('stat', remotePath);
356
344
  } catch (err) {
357
345
  throw err.custom ? err : this.fmtError(err, 'stat', err.code);
358
- } finally {
359
- removeTempListeners(this, listeners, 'stat');
346
+ }
347
+ }
348
+
349
+ /*
350
+ * Use the lstat command to obtain attributes associated with a remote path.
351
+ * THe difference between stat and lstat is that stat, in the case of symbolic
352
+ * links, will return the attributes associated with the target of the link. With
353
+ * lstat, attributes associated with the symbolic link rather than the target are
354
+ * returned.
355
+ *
356
+ * @param {String} remotePath - path to an object on the remote server
357
+ * @return {Promise<Object>} stats - attributes info
358
+ *
359
+ */
360
+ async lstat(remotePath) {
361
+ try {
362
+ haveConnection(this, 'lstat');
363
+ return await this._xstat('lstat', remotePath);
364
+ } catch (err) {
365
+ throw err.custom ? err : this.fmtError(err, 'lstat', err.code);
360
366
  }
361
367
  }
362
368
 
@@ -371,51 +377,31 @@ class SftpClient {
371
377
  * @return {Promise<Boolean|String>} returns false if object does not exist. Returns type of
372
378
  * object if it does
373
379
  */
374
- async _exists(rPath) {
380
+ async exists(remotePath) {
381
+ this.debugMsg(`exists: remotePath = ${remotePath}`);
375
382
  try {
376
- const absPath = await normalizeRemotePath(this, rPath);
377
- this.debugMsg(`exists: ${rPath} -> ${absPath}`);
378
- const info = await this._stat(absPath);
383
+ if (remotePath === '.') {
384
+ return 'd';
385
+ }
386
+ const info = await this.lstat(remotePath);
379
387
  this.debugMsg('exists: <- ', info);
380
388
  if (info.isDirectory) {
381
- this.debugMsg(`exists: ${rPath} = d`);
382
389
  return 'd';
383
- }
384
- if (info.isSymbolicLink) {
385
- this.debugMsg(`exists: ${rPath} = l`);
390
+ } else if (info.isSymbolicLink) {
386
391
  return 'l';
387
- }
388
- if (info.isFile) {
389
- this.debugMsg(`exists: ${rPath} = -`);
392
+ } else if (info.isFile) {
390
393
  return '-';
394
+ } else {
395
+ return false;
391
396
  }
392
- this.debugMsg(`exists: ${rPath} = false`);
393
- return false;
394
397
  } catch (err) {
395
398
  if (err.code === errorCode.notexist) {
396
- this.debugMsg(`exists: ${rPath} = false errorCode = ${err.code}`);
397
399
  return false;
398
400
  }
399
401
  throw err.custom ? err : this.fmtError(err.message, 'exists', err.code);
400
402
  }
401
403
  }
402
404
 
403
- async exists(remotePath) {
404
- let listeners;
405
- try {
406
- listeners = addTempListeners(this, 'exists');
407
- haveConnection(this, 'exists');
408
- if (remotePath === '.') {
409
- return 'd';
410
- }
411
- return await this._exists(remotePath);
412
- } catch (err) {
413
- throw err.custom ? err : this.fmtError(err, 'exists', err.code);
414
- } finally {
415
- removeTempListeners(this, listeners, 'exists');
416
- }
417
- }
418
-
419
405
  /**
420
406
  * @async
421
407
  *
@@ -427,55 +413,53 @@ class SftpClient {
427
413
  *
428
414
  * @param {String} remotePath - path to remote directory
429
415
  * @param {function} filter - a filter function used to select return entries
416
+ * @param {Boolean} addListeners - (Optional) if true, add listeners. Default true
430
417
  * @returns {Promise<Array>} array of file description objects
431
418
  */
432
- _list(remotePath, filter) {
419
+ list(remotePath, filter, addListeners = true) {
420
+ let listeners;
433
421
  return new Promise((resolve, reject) => {
434
- this.sftp.readdir(remotePath, (err, fileList) => {
435
- if (err) {
436
- reject(this.fmtError(`${err.message} ${remotePath}`, 'list', err.code));
437
- } else {
438
- const reg = /-/gi;
439
- const newList = fileList.map((item) => {
440
- return {
441
- type: item.longname.slice(0, 1),
442
- name: item.filename,
443
- size: item.attrs.size,
444
- modifyTime: item.attrs.mtime * 1000,
445
- accessTime: item.attrs.atime * 1000,
446
- rights: {
447
- user: item.longname.slice(1, 4).replace(reg, ''),
448
- group: item.longname.slice(4, 7).replace(reg, ''),
449
- other: item.longname.slice(7, 10).replace(reg, ''),
450
- },
451
- owner: item.attrs.uid,
452
- group: item.attrs.gid,
453
- longname: item.longname,
454
- };
455
- });
456
- if (filter) {
457
- resolve(newList.filter((item) => filter(item)));
422
+ if (addListeners) {
423
+ listeners = addTempListeners(this, 'list', reject);
424
+ }
425
+ if (haveConnection(this, 'list', reject)) {
426
+ this.sftp.readdir(remotePath, (err, fileList) => {
427
+ if (err) {
428
+ reject(this.fmtError(`${err.message} ${remotePath}`, 'list', err.code));
458
429
  } else {
459
- resolve(newList);
430
+ const reg = /-/gi;
431
+ const newList = fileList.map((item) => {
432
+ return {
433
+ type: item.longname.slice(0, 1),
434
+ name: item.filename,
435
+ size: item.attrs.size,
436
+ modifyTime: item.attrs.mtime * 1000,
437
+ accessTime: item.attrs.atime * 1000,
438
+ 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, ''),
442
+ },
443
+ owner: item.attrs.uid,
444
+ group: item.attrs.gid,
445
+ longname: item.longname,
446
+ };
447
+ });
448
+ if (filter) {
449
+ resolve(newList.filter((item) => filter(item)));
450
+ } else {
451
+ resolve(newList);
452
+ }
460
453
  }
461
- }
462
- });
454
+ });
455
+ }
456
+ }).finally(() => {
457
+ if (addListeners) {
458
+ removeTempListeners(this, listeners, 'list');
459
+ }
463
460
  });
464
461
  }
465
462
 
466
- async list(remotePath, filter) {
467
- let listeners;
468
- try {
469
- listeners = addTempListeners(this, 'list');
470
- haveConnection(this, 'list');
471
- return await this._list(remotePath, filter);
472
- } catch (e) {
473
- throw e.custom ? e : this.fmtError(`${e.message} ${remotePath}`, 'list', e.code);
474
- } finally {
475
- removeTempListeners(this, listeners, 'list');
476
- }
477
- }
478
-
479
463
  /**
480
464
  * get file
481
465
  *
@@ -488,6 +472,7 @@ class SftpClient {
488
472
  * @param {string|stream|undefined} dst - data destination
489
473
  * @param {Object} options - options object with supported properties of readStreamOptions,
490
474
  * writeStreamOptions and pipeOptions.
475
+ * @param {Boolean} addListeners - (Optional) if true, add listeners. Default true
491
476
  *
492
477
  * *Important Note*: The ability to set ''autoClose' on read/write streams and 'end' on pipe() calls
493
478
  * is no longer supported. New methods 'createReadStream()' and 'createWriteStream()' have been
@@ -495,83 +480,78 @@ class SftpClient {
495
480
  *
496
481
  * @return {Promise<String|Stream|Buffer>}
497
482
  */
498
- _get(rPath, dst, opts) {
499
- let rdr, wtr;
483
+ get(remotePath, dst, options, addListeners = true) {
484
+ let listeners, rdr, wtr;
500
485
  return new Promise((resolve, reject) => {
501
- opts = {
502
- ...opts,
503
- readStreamOptions: { autoClose: true },
504
- writeStreamOptions: { autoClose: true },
505
- pipeOptions: { end: true },
506
- };
507
- rdr = this.sftp.createReadStream(rPath, opts.readStreamOptions);
508
- rdr.once('error', (err) => {
509
- if (dst && typeof dst !== 'string' && !dst.destroyed) {
510
- dst.destroy();
511
- }
512
- reject(this.fmtError(`${err.message} ${rPath}`, '_get', err.code));
513
- });
514
- if (dst === undefined) {
515
- // no dst specified, return buffer of data
516
- this.debugMsg('get resolving buffer of data');
517
- wtr = concat((buff) => {
518
- resolve(buff);
486
+ if (addListeners) {
487
+ listeners = addTempListeners(this, 'get', reject);
488
+ }
489
+ if (haveConnection(this, 'get', reject)) {
490
+ options = {
491
+ readStreamOptions: { ...options?.readStreamOptions, autoClose: true },
492
+ writeStreamOptions: { ...options?.writeStreamOptions, autoClose: true },
493
+ pipeOptions: { ...options?.pipeOptions, end: true },
494
+ };
495
+ rdr = this.sftp.createReadStream(remotePath, options.readStreamOptions);
496
+ rdr.once('error', (err) => {
497
+ if (dst && typeof dst !== 'string' && !dst.destroyed) {
498
+ dst.destroy();
499
+ }
500
+ reject(this.fmtError(`${err.message} ${remotePath}`, 'get', err.code));
519
501
  });
520
- } else if (typeof dst === 'string') {
521
- // dst local file path
522
- this.debugMsg('get returning local file');
523
- const localCheck = haveLocalCreate(dst);
524
- if (!localCheck.status) {
502
+ if (dst === undefined) {
503
+ // no dst specified, return buffer of data
504
+ this.debugMsg('get resolving buffer of data');
505
+ wtr = concat((buff) => {
506
+ resolve(buff);
507
+ });
508
+ } else if (typeof dst === 'string') {
509
+ // dst local file path
510
+ this.debugMsg('get returning local file');
511
+ const localCheck = haveLocalCreate(dst);
512
+ if (!localCheck.status) {
513
+ reject(
514
+ this.fmtError(
515
+ `Bad path: ${dst}: ${localCheck.details}`,
516
+ 'get',
517
+ localCheck.code
518
+ )
519
+ );
520
+ return;
521
+ } else {
522
+ wtr = fs.createWriteStream(dst, options.writeStreamOptions);
523
+ }
524
+ } else {
525
+ this.debugMsg('get: returning data into supplied stream');
526
+ wtr = dst;
527
+ }
528
+ wtr.once('error', (err) => {
525
529
  reject(
526
530
  this.fmtError(
527
- `Bad path: ${dst}: ${localCheck.details}`,
531
+ `${err.message} ${typeof dst === 'string' ? dst : '<stream>'}`,
528
532
  'get',
529
- localCheck.code
533
+ err.code
530
534
  )
531
535
  );
532
- return;
533
- } else {
534
- wtr = fs.createWriteStream(dst, opts.writeStreamOptions);
535
- }
536
- } else {
537
- this.debugMsg('get: returning data into supplied stream');
538
- wtr = dst;
539
- }
540
- wtr.once('error', (err) => {
541
- reject(
542
- this.fmtError(
543
- `${err.message} ${typeof dst === 'string' ? dst : '<stream>'}`,
544
- 'get',
545
- err.code
546
- )
547
- );
548
- });
549
- rdr.once('end', () => {
550
- if (typeof dst === 'string') {
551
- this.debugMsg('get: resolving with dst filename');
552
- resolve(dst);
553
- } else if (dst !== undefined) {
554
- this.debugMsg('get: resolving with writer stream object');
555
- resolve(wtr);
556
- }
557
- });
558
- rdr.pipe(wtr, opts.pipeOptions);
536
+ });
537
+ rdr.once('end', () => {
538
+ if (typeof dst === 'string') {
539
+ this.debugMsg('get: resolving with dst filename');
540
+ resolve(dst);
541
+ } else if (dst !== undefined) {
542
+ this.debugMsg('get: resolving with writer stream object');
543
+ resolve(wtr);
544
+ }
545
+ });
546
+ rdr.pipe(wtr, options.pipeOptions);
547
+ }
548
+ }).finally(() => {
549
+ if (addListeners) {
550
+ removeTempListeners(this, listeners, 'get');
551
+ }
559
552
  });
560
553
  }
561
554
 
562
- async get(remotePath, dst, options) {
563
- let listeners;
564
- try {
565
- listeners = addTempListeners(this, 'get');
566
- haveConnection(this, 'get');
567
- return await this._get(remotePath, dst, options);
568
- } catch (e) {
569
- throw e.custom ? e : this.fmtError(`${e.message} ${remotePath}`, 'get', e.code);
570
- } finally {
571
- removeTempListeners(this, listeners, 'get');
572
- }
573
- }
574
-
575
555
  /**
576
556
  * Use SSH2 fastGet for downloading the file.
577
557
  * Downloads a file at remotePath to localPath using parallel reads
@@ -582,22 +562,29 @@ class SftpClient {
582
562
  * @param {Object} options
583
563
  * @return {Promise<String>} the result of downloading the file
584
564
  */
585
- _fastGet(rPath, lPath, opts) {
565
+ _fastGet(rPath, lPath, opts, addListeners = true) {
566
+ let listeners;
586
567
  return new Promise((resolve, reject) => {
587
- this.sftp.fastGet(rPath, lPath, opts, (err) => {
588
- if (err) {
589
- reject(this.fmtError(`${err.message} Remote: ${rPath} Local: ${lPath}`));
590
- }
591
- resolve(`${rPath} was successfully download to ${lPath}!`);
592
- });
568
+ if (addListeners) {
569
+ listeners = addTempListeners(this, '_fastGet', reject);
570
+ }
571
+ if (haveConnection(this, '_fastGet', reject)) {
572
+ this.sftp.fastGet(rPath, lPath, opts, (err) => {
573
+ if (err) {
574
+ reject(this.fmtError(`${err.message} Remote: ${rPath} Local: ${lPath}`));
575
+ }
576
+ resolve(`${rPath} was successfully download to ${lPath}!`);
577
+ });
578
+ }
579
+ }).finally(() => {
580
+ if (addListeners) {
581
+ removeTempListeners(this, listeners, '_fastGet');
582
+ }
593
583
  });
594
584
  }
595
585
 
596
586
  async fastGet(remotePath, localPath, options) {
597
- let listeners;
598
587
  try {
599
- listeners = addTempListeners(this, 'fastGet');
600
- haveConnection(this, 'fastGet');
601
588
  const ftype = await this.exists(remotePath);
602
589
  if (ftype !== '-') {
603
590
  const msg = `${!ftype ? 'No such file ' : 'Not a regular file'} ${remotePath}`;
@@ -614,8 +601,6 @@ class SftpClient {
614
601
  return await this._fastGet(remotePath, localPath, options);
615
602
  } catch (err) {
616
603
  throw this.fmtError(err, 'fastGet');
617
- } finally {
618
- removeTempListeners(this, listeners, 'fastGet');
619
604
  }
620
605
  }
621
606
 
@@ -627,34 +612,42 @@ class SftpClient {
627
612
  * See 'fastPut' at
628
613
  * https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md
629
614
  *
630
- * @param {String} localPath
631
- * @param {String} remotePath
632
- * @param {Object} options
615
+ * @param {String} localPath - path to local file to put
616
+ * @param {String} remotePath - destination path for put file
617
+ * @param {Object} options - additonal fastPut options
618
+ * @param {Boolean} addListeners - (Optional) if true, add listeners. Default true.
633
619
  * @return {Promise<String>} the result of downloading the file
634
620
  */
635
- _fastPut(lPath, rPath, opts) {
621
+ _fastPut(lPath, rPath, opts, addListeners = true) {
622
+ let listeners;
636
623
  return new Promise((resolve, reject) => {
637
- this.sftp.fastPut(lPath, rPath, opts, (err) => {
638
- if (err) {
639
- reject(
640
- this.fmtError(
641
- `${err.message} Local: ${lPath} Remote: ${rPath}`,
642
- 'fastPut',
643
- err.code
644
- )
645
- );
646
- }
647
- resolve(`${lPath} was successfully uploaded to ${rPath}!`);
648
- });
624
+ if (addListeners) {
625
+ listeners = addTempListeners(this, '_fastPut', reject);
626
+ }
627
+ if (haveConnection(this, '_fastPut', reject)) {
628
+ this.sftp.fastPut(lPath, rPath, opts, (err) => {
629
+ if (err) {
630
+ reject(
631
+ this.fmtError(
632
+ `${err.message} Local: ${lPath} Remote: ${rPath}`,
633
+ 'fastPut',
634
+ err.code
635
+ )
636
+ );
637
+ }
638
+ resolve(`${lPath} was successfully uploaded to ${rPath}!`);
639
+ });
640
+ }
641
+ }).finally(() => {
642
+ if (addListeners) {
643
+ removeTempListeners(this, listeners, '_fastPut');
644
+ }
649
645
  });
650
646
  }
651
647
 
652
648
  async fastPut(localPath, remotePath, options) {
653
- let listeners;
654
649
  try {
655
- listeners = addTempListeners(this, 'fastPut');
656
650
  this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
657
- haveConnection(this, 'fastPut');
658
651
  const localCheck = haveLocalAccess(localPath);
659
652
  if (!localCheck.status) {
660
653
  throw this.fmtError(
@@ -672,8 +665,6 @@ class SftpClient {
672
665
  return await this._fastPut(localPath, remotePath, options);
673
666
  } catch (e) {
674
667
  throw e.custom ? e : this.fmtError(e.message, 'fastPut', e.code);
675
- } finally {
676
- removeTempListeners(this, listeners, 'fastPut');
677
668
  }
678
669
  }
679
670
 
@@ -694,49 +685,58 @@ class SftpClient {
694
685
  *
695
686
  * @return {Promise<String>}
696
687
  */
697
- _put(lPath, rPath, opts) {
698
- let wtr, rdr;
688
+ _put(lPath, rPath, opts, addListeners = true) {
689
+ let listeners, wtr, rdr;
699
690
  return new Promise((resolve, reject) => {
691
+ if (addListeners) {
692
+ listeners = addTempListeners(this, '_put', reject);
693
+ }
700
694
  opts = {
701
- ...opts,
702
- readStreamOptions: { autoClose: true },
703
- writeStreamOptions: { autoClose: true },
704
- pipeOptions: { end: true },
695
+ readStreamOptions: { ...opts?.readStreamOptions, autoClose: true },
696
+ writeStreamOptions: { ...opts?.writeStreamOptions, autoClose: true },
697
+ pipeOptions: { ...opts?.pipeOptions, end: true },
705
698
  };
706
- wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
707
- wtr.once('error', (err) => {
708
- reject(this.fmtError(`${err.message} ${rPath}`, 'put', err.code));
709
- });
710
- wtr.once('close', () => {
711
- resolve(`Uploaded data stream to ${rPath}`);
712
- });
713
- if (lPath instanceof Buffer) {
714
- this.debugMsg('put source is a buffer');
715
- wtr.end(lPath);
716
- } else {
717
- rdr =
718
- typeof lPath === 'string'
719
- ? fs.createReadStream(lPath, opts.readStreamOptions)
720
- : lPath;
721
- rdr.once('error', (err) => {
699
+ if (haveConnection(this, '_put', reject)) {
700
+ wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
701
+ wtr.once('error', (err) => {
722
702
  reject(
723
- this.fmtError(
724
- `${err.message} ${typeof lPath === 'string' ? lPath : '<stream>'}`,
725
- '_put',
726
- err.code
727
- )
703
+ this.fmtError(`Write stream error: ${err.message} ${rPath}`, '_put', err.code)
728
704
  );
729
705
  });
730
- rdr.pipe(wtr, opts.pipeOptions);
706
+ wtr.once('close', () => {
707
+ resolve(`Uploaded data stream to ${rPath}`);
708
+ });
709
+ if (lPath instanceof Buffer) {
710
+ this.debugMsg('put source is a buffer');
711
+ wtr.end(lPath);
712
+ } else {
713
+ rdr =
714
+ typeof lPath === 'string'
715
+ ? fs.createReadStream(lPath, opts.readStreamOptions)
716
+ : lPath;
717
+ rdr.once('error', (err) => {
718
+ reject(
719
+ this.fmtError(
720
+ `Read stream error: ${err.message} ${
721
+ typeof lPath === 'string' ? lPath : '<stream>'
722
+ }`,
723
+ '_put',
724
+ err.code
725
+ )
726
+ );
727
+ });
728
+ rdr.pipe(wtr, opts.pipeOptions);
729
+ }
730
+ }
731
+ }).finally(() => {
732
+ if (addListeners) {
733
+ removeTempListeners(this, listeners, '_put');
731
734
  }
732
735
  });
733
736
  }
734
737
 
735
738
  async put(localSrc, remotePath, options) {
736
- let listeners;
737
739
  try {
738
- listeners = addTempListeners(this, 'put');
739
- haveConnection(this, 'put');
740
740
  if (typeof localSrc === 'string') {
741
741
  const localCheck = haveLocalAccess(localSrc);
742
742
  if (!localCheck.status) {
@@ -749,9 +749,7 @@ class SftpClient {
749
749
  }
750
750
  return await this._put(localSrc, remotePath, options);
751
751
  } catch (e) {
752
- throw e.custom ? e : this.fmtError(e.message, 'put', e.code);
753
- } finally {
754
- removeTempListeners(this, listeners, 'put');
752
+ throw e.custom ? e : this.fmtError(`Re-thrown: ${e.message}`, 'put', e.code);
755
753
  }
756
754
  }
757
755
 
@@ -763,30 +761,38 @@ class SftpClient {
763
761
  * @param {Object} options
764
762
  * @return {Promise<String>}
765
763
  */
766
- _append(input, rPath, opts) {
764
+ _append(input, rPath, opts, addListeners = true) {
765
+ let listeners;
767
766
  return new Promise((resolve, reject) => {
768
- this.debugMsg(`append -> remote: ${rPath} `, opts);
769
- opts.flags = 'a';
770
- const stream = this.sftp.createWriteStream(rPath, opts);
771
- stream.on('error', (err) => {
772
- reject(this.fmtError(`${err.message} ${rPath}`, 'append', err.code));
773
- });
774
- stream.on('close', () => {
775
- resolve(`Appended data to ${rPath}`);
776
- });
777
- if (input instanceof Buffer) {
778
- stream.write(input);
779
- stream.end();
780
- } else {
781
- input.pipe(stream);
767
+ if (addListeners) {
768
+ listeners = addTempListeners(this, '_append', reject);
769
+ }
770
+ if (haveConnection(this, '_append', reject)) {
771
+ this.debugMsg(`append -> remote: ${rPath} `, opts);
772
+ opts.flags = 'a';
773
+ const stream = this.sftp.createWriteStream(rPath, opts);
774
+ stream.on('error', (err) => {
775
+ reject(this.fmtError(`${err.message} ${rPath}`, 'append', err.code));
776
+ });
777
+ stream.on('close', () => {
778
+ resolve(`Appended data to ${rPath}`);
779
+ });
780
+ if (input instanceof Buffer) {
781
+ stream.write(input);
782
+ stream.end();
783
+ } else {
784
+ input.pipe(stream);
785
+ }
786
+ }
787
+ }).finally(() => {
788
+ if (addListeners) {
789
+ removeTempListeners(this, listeners, '_append');
782
790
  }
783
791
  });
784
792
  }
785
793
 
786
794
  async append(input, remotePath, options = {}) {
787
- let listeners;
788
795
  try {
789
- listeners = addTempListeners(this, 'append');
790
796
  if (typeof input === 'string') {
791
797
  throw this.fmtError(
792
798
  'Cannot append one file to another',
@@ -794,7 +800,6 @@ class SftpClient {
794
800
  errorCode.badPath
795
801
  );
796
802
  }
797
- haveConnection(this, 'append');
798
803
  const fileType = await this.exists(remotePath);
799
804
  if (fileType && fileType === 'd') {
800
805
  throw this.fmtError(
@@ -806,8 +811,6 @@ class SftpClient {
806
811
  await this._append(input, remotePath, options);
807
812
  } catch (e) {
808
813
  throw e.custom ? e : this.fmtError(e.message, 'append', e.code);
809
- } finally {
810
- removeTempListeners(this, listeners, 'append');
811
814
  }
812
815
  }
813
816
 
@@ -820,8 +823,12 @@ class SftpClient {
820
823
  * @param {boolean} recursive - if true, recursively create directories
821
824
  * @return {Promise<String>}
822
825
  */
823
- _doMkdir(p) {
826
+ _doMkdir(p, addListeners = true) {
827
+ let listeners;
824
828
  return new Promise((resolve, reject) => {
829
+ if (addListeners) {
830
+ listeners = addTempListeners(this, '_doMkdir', reject);
831
+ }
825
832
  this.sftp.mkdir(p, (err) => {
826
833
  if (err) {
827
834
  if (err.code === 4) {
@@ -848,6 +855,10 @@ class SftpClient {
848
855
  resolve(`${p} directory created`);
849
856
  }
850
857
  });
858
+ }).finally(() => {
859
+ if (addListeners) {
860
+ removeTempListeners(this, listeners, '_doMkdir');
861
+ }
851
862
  });
852
863
  }
853
864
 
@@ -889,15 +900,11 @@ class SftpClient {
889
900
  }
890
901
 
891
902
  async mkdir(remotePath, recursive = false) {
892
- let listeners;
893
903
  try {
894
- listeners = addTempListeners(this, '_mkdir');
895
904
  haveConnection(this, 'mkdir');
896
905
  return await this._mkdir(remotePath, recursive);
897
906
  } catch (err) {
898
907
  throw this.fmtError(`${err.message}`, 'mkdir', err.code);
899
- } finally {
900
- removeTempListeners(this, listeners, 'append');
901
908
  }
902
909
  }
903
910
 
@@ -911,69 +918,85 @@ class SftpClient {
911
918
  * directory
912
919
  * @return {Promise<String>}
913
920
  */
914
- async rmdir(remotePath, recursive = false) {
915
- const _rmdir = (p) => {
921
+ async rmdir(remoteDir, recursive = false) {
922
+ const _rmdir = (dir) => {
923
+ let listeners;
916
924
  return new Promise((resolve, reject) => {
917
- this.debugMsg(`rmdir -> ${p}`);
918
- this.sftp.rmdir(p, (err) => {
925
+ listeners = addTempListeners(this, '_rmdir', reject);
926
+ this.debugMsg(`_rmdir: dir = ${dir}`);
927
+ this.sftp.rmdir(dir, (err) => {
919
928
  if (err) {
920
- reject(this.fmtError(`${err.message} ${p}`, 'rmdir', err.code));
929
+ reject(this.fmtError(`${err.message} ${dir}`, 'rmdir', err.code));
921
930
  }
922
931
  resolve('Successfully removed directory');
923
932
  });
933
+ }).finally(() => {
934
+ removeTempListeners(this, listeners, '_rmdir');
924
935
  });
925
936
  };
926
937
 
927
- const _dormdir = async (p, recur) => {
928
- try {
929
- if (recur) {
930
- const list = await this.list(p);
931
- if (list.length) {
932
- const files = list.filter((item) => item.type !== 'd');
933
- const dirs = list.filter((item) => item.type === 'd');
934
- this.debugMsg('rmdir contents (files): ', files);
935
- this.debugMsg('rmdir contents (dirs): ', dirs);
936
- for (const d of dirs) {
937
- await _dormdir(`${p}${this.remotePathSep}${d.name}`, true);
938
- }
939
- const promiseList = [];
940
- for (const f of files) {
941
- promiseList.push(this._delete(`${p}${this.remotePathSep}${f.name}`));
942
- }
943
- await Promise.all(promiseList);
944
- }
938
+ const _delFiles = (path, fileList) => {
939
+ let listeners;
940
+ return new Promise((resolve, reject) => {
941
+ listeners = addTempListeners(this, '_delFiles', reject);
942
+ this.debugMsg(`_delFiles: path = ${path} fileList = ${fileList}`);
943
+ let pList = [];
944
+ for (const f of fileList) {
945
+ pList.push(this.delete(`${path}/${f.name}`, true, false));
945
946
  }
946
- return await _rmdir(p);
947
- } catch (err) {
948
- throw err.custom ? err : this.fmtError(err, '_dormdir', err.code);
949
- }
947
+ resolve(pList);
948
+ })
949
+ .then((p) => {
950
+ return Promise.all(p);
951
+ })
952
+ .finally(() => {
953
+ removeTempListeners(this, listeners, '_delFiles');
954
+ });
950
955
  };
951
956
 
952
- let listeners;
953
957
  try {
954
- listeners = addTempListeners(this, 'rmdir');
955
- haveConnection(this, 'rmdir');
956
- const absPath = await normalizeRemotePath(this, remotePath);
957
- const dirStatus = await this.exists(absPath);
958
- if (dirStatus && dirStatus !== 'd') {
958
+ this.debugMsg(`rmdir: dir = ${remoteDir} recursive = ${recursive}`);
959
+ let absPath = await normalizeRemotePath(this, remoteDir);
960
+ let existStatus = await this.exists(absPath);
961
+ this.debugMsg(`rmdir: ${absPath} existStatus = ${existStatus}`);
962
+ if (!existStatus) {
959
963
  throw this.fmtError(
960
- `Bad path: ${absPath} not a directory`,
964
+ `Bad Path: ${remoteDir}: No such directory`,
961
965
  'rmdir',
962
966
  errorCode.badPath
963
967
  );
964
- } else if (!dirStatus) {
968
+ }
969
+ if (existStatus !== 'd') {
965
970
  throw this.fmtError(
966
- `Bad path: ${absPath} No such file`,
971
+ `Bad Path: ${remoteDir}: Not a directory`,
967
972
  'rmdir',
968
973
  errorCode.badPath
969
974
  );
970
- } else {
971
- return await _dormdir(absPath, recursive);
972
975
  }
976
+ if (!recursive) {
977
+ this.debugMsg('rmdir: non-recursive - just try to remove it');
978
+ return await _rmdir(absPath);
979
+ }
980
+ let listing = await this.list(absPath);
981
+ this.debugMsg(`rmdir: listing count = ${listing.length}`);
982
+ if (!listing.length) {
983
+ this.debugMsg('rmdir: No sub dir or files, just rmdir');
984
+ return await _rmdir(absPath);
985
+ }
986
+ let fileList = listing.filter((i) => i.type !== 'd');
987
+ this.debugMsg(`rmdir: dir content files to remove = ${fileList.length}`);
988
+ let dirList = listing.filter((i) => i.type === 'd');
989
+ this.debugMsg(`rmdir: sub-directories to remove = ${dirList.length}`);
990
+ await _delFiles(absPath, fileList);
991
+ for (const d of dirList) {
992
+ await this.rmdir(`${absPath}/${d.name}`, true);
993
+ }
994
+ await _rmdir(absPath);
995
+ return 'Successfully removed directory';
973
996
  } catch (err) {
974
- throw err.custom ? err : this.fmtError(err.message, 'rmdir', err.code);
975
- } finally {
976
- removeTempListeners(this, listeners, 'rmdir');
997
+ throw err.custom
998
+ ? err
999
+ : this.fmtError(`${err.message} ${remoteDir}`, 'rmdir', err.code);
977
1000
  }
978
1001
  }
979
1002
 
@@ -988,34 +1011,29 @@ class SftpClient {
988
1011
  * @return {Promise<String>} with string 'Successfully deleted file' once resolved
989
1012
  *
990
1013
  */
991
- _delete(rPath, notFoundOK) {
1014
+ delete(remotePath, notFoundOK = false, addListeners = true) {
1015
+ let listeners;
992
1016
  return new Promise((resolve, reject) => {
993
- this.sftp.unlink(rPath, (err) => {
1017
+ if (addListeners) {
1018
+ listeners = addTempListeners(this, 'delete', reject);
1019
+ }
1020
+ this.sftp.unlink(remotePath, (err) => {
994
1021
  if (err) {
995
1022
  if (notFoundOK && err.code === 2) {
996
- resolve(`Successfully deleted ${rPath}`);
1023
+ resolve(`Successfully deleted ${remotePath}`);
997
1024
  } else {
998
- reject(this.fmtError(`${err.message} ${rPath}`, 'delete', err.code));
1025
+ reject(this.fmtError(`${err.message} ${remotePath}`, 'delete', err.code));
999
1026
  }
1000
1027
  }
1001
- resolve(`Successfully deleted ${rPath}`);
1028
+ resolve(`Successfully deleted ${remotePath}`);
1002
1029
  });
1030
+ }).finally(() => {
1031
+ if (addListeners) {
1032
+ removeTempListeners(this, listeners, 'delete');
1033
+ }
1003
1034
  });
1004
1035
  }
1005
1036
 
1006
- async delete(remotePath, notFoundOK = false) {
1007
- let listeners;
1008
- try {
1009
- listeners = addTempListeners(this, 'delete');
1010
- haveConnection(this, 'delete');
1011
- return await this._delete(remotePath, notFoundOK);
1012
- } catch (err) {
1013
- throw err.custom ? err : this.fmtError(err.message, 'delete', err.code);
1014
- } finally {
1015
- removeTempListeners(this, listeners, 'delete');
1016
- }
1017
- }
1018
-
1019
1037
  /**
1020
1038
  * @async
1021
1039
  *
@@ -1023,42 +1041,38 @@ class SftpClient {
1023
1041
  *
1024
1042
  * @param {string} fromPath - path to the file to be renamed.
1025
1043
  * @param {string} toPath - path to the new name.
1044
+ * @param {Boolean} addListeners - (Optional) if true, add listeners. Default true
1026
1045
  *
1027
1046
  * @return {Promise<String>}
1028
1047
  *
1029
1048
  */
1030
- _rename(fPath, tPath) {
1049
+ rename(fPath, tPath, addListeners = true) {
1050
+ let listeners;
1031
1051
  return new Promise((resolve, reject) => {
1032
- this.sftp.rename(fPath, tPath, (err) => {
1033
- if (err) {
1034
- reject(
1035
- this.fmtError(
1036
- `${err.message} From: ${fPath} To: ${tPath}`,
1037
- '_rename',
1038
- err.code
1039
- )
1040
- );
1041
- }
1042
- resolve(`Successfully renamed ${fPath} to ${tPath}`);
1043
- });
1052
+ if (addListeners) {
1053
+ listeners = addTempListeners(this, 'rename', reject);
1054
+ }
1055
+ if (haveConnection(this, 'rename', reject)) {
1056
+ this.sftp.rename(fPath, tPath, (err) => {
1057
+ if (err) {
1058
+ reject(
1059
+ this.fmtError(
1060
+ `${err.message} From: ${fPath} To: ${tPath}`,
1061
+ '_rename',
1062
+ err.code
1063
+ )
1064
+ );
1065
+ }
1066
+ resolve(`Successfully renamed ${fPath} to ${tPath}`);
1067
+ });
1068
+ }
1069
+ }).finally(() => {
1070
+ if (addListeners) {
1071
+ removeTempListeners(this, listeners, 'rename');
1072
+ }
1044
1073
  });
1045
1074
  }
1046
1075
 
1047
- async rename(fromPath, toPath) {
1048
- let listeners;
1049
- try {
1050
- listeners = addTempListeners(this, 'rename');
1051
- haveConnection(this, 'rename');
1052
- return await this._rename(fromPath, toPath);
1053
- } catch (err) {
1054
- throw err.custom
1055
- ? err
1056
- : this.fmtError(`${err.message} ${fromPath} ${toPath}`, 'rename', err.code);
1057
- } finally {
1058
- removeTempListeners(this, listeners, 'rename');
1059
- }
1060
- }
1061
-
1062
1076
  /**
1063
1077
  * @async
1064
1078
  *
@@ -1067,40 +1081,34 @@ class SftpClient {
1067
1081
  *
1068
1082
  * @param {string} fromPath - path to the file to be renamed.
1069
1083
  * @param {string} toPath - path the new name.
1084
+ * @param {Boolean} addListeners - (Optional) if true, add listeners. Default true
1070
1085
  *
1071
1086
  * @return {Promise<String>}
1072
1087
  *
1073
1088
  */
1074
- _posixRename(fPath, tPath) {
1075
- return new Promise((resolve, reject) => {
1076
- this.sftp.ext_openssh_rename(fPath, tPath, (err) => {
1077
- if (err) {
1078
- reject(
1079
- this.fmtError(
1080
- `${err.message} From: ${fPath} To: ${tPath}`,
1081
- '_posixRename',
1082
- err.code
1083
- )
1084
- );
1085
- }
1086
- resolve(`Successful POSIX rename ${fPath} to ${tPath}`);
1087
- });
1088
- });
1089
- }
1090
-
1091
- async posixRename(fromPath, toPath) {
1089
+ posixRename(fPath, tPath, addListeners = true) {
1092
1090
  let listeners;
1093
- try {
1094
- listeners = addTempListeners(this, 'posixRename');
1095
- haveConnection(this, 'posixRename');
1096
- return await this._posixRename(fromPath, toPath);
1097
- } catch (err) {
1098
- throw err.custom
1099
- ? err
1100
- : this.fmtError(`${err.message} ${fromPath} ${toPath}`, 'posixRename', err.code);
1101
- } finally {
1091
+ return new Promise((resolve, reject) => {
1092
+ if (addListeners) {
1093
+ listeners = addTempListeners(this, 'posixRename', reject);
1094
+ }
1095
+ if (haveConnection(this, 'posixRename', reject)) {
1096
+ this.sftp.ext_openssh_rename(fPath, tPath, (err) => {
1097
+ if (err) {
1098
+ reject(
1099
+ this.fmtError(
1100
+ `${err.message} From: ${fPath} To: ${tPath}`,
1101
+ '_posixRename',
1102
+ err.code
1103
+ )
1104
+ );
1105
+ }
1106
+ resolve(`Successful POSIX rename ${fPath} to ${tPath}`);
1107
+ });
1108
+ }
1109
+ }).finally(() => {
1102
1110
  removeTempListeners(this, listeners, 'posixRename');
1103
- }
1111
+ });
1104
1112
  }
1105
1113
 
1106
1114
  /**
@@ -1110,35 +1118,31 @@ class SftpClient {
1110
1118
  *
1111
1119
  * @param {string} remotePath - path to the remote target object.
1112
1120
  * @param {number | string} mode - the new octal mode to set
1121
+ * @param {boolean} addListeners - (Optional) if true, add listeners. Default true.
1113
1122
  *
1114
1123
  * @return {Promise<String>}
1115
1124
  */
1116
- _chmod(rPath, mode) {
1125
+ chmod(rPath, mode, addListeners = true) {
1126
+ let listeners;
1117
1127
  return new Promise((resolve, reject) => {
1118
- this.sftp.chmod(rPath, mode, (err) => {
1119
- if (err) {
1120
- reject(this.fmtError(`${err.message} ${rPath}`, '_chmod', err.code));
1121
- }
1122
- resolve('Successfully change file mode');
1123
- });
1128
+ if (addListeners) {
1129
+ listeners = addTempListeners(this, 'chmod', reject);
1130
+ }
1131
+ if (haveConnection(this, 'chmod', reject)) {
1132
+ this.sftp.chmod(rPath, mode, (err) => {
1133
+ if (err) {
1134
+ reject(this.fmtError(`${err.message} ${rPath}`, '_chmod', err.code));
1135
+ }
1136
+ resolve('Successfully change file mode');
1137
+ });
1138
+ }
1139
+ }).finally(() => {
1140
+ if (addListeners) {
1141
+ removeTempListeners(this, listeners, 'chmod');
1142
+ }
1124
1143
  });
1125
1144
  }
1126
1145
 
1127
- async chmod(remotePath, mode) {
1128
- let listeners;
1129
- try {
1130
- listeners = addTempListeners(this, 'chmod');
1131
- haveConnection(this, 'chmod');
1132
- return await this._chmod(remotePath, mode);
1133
- } catch (err) {
1134
- throw err.custom
1135
- ? err
1136
- : this.fmtError(`${err.message} ${remotePath}`, 'chmod', err.code);
1137
- } finally {
1138
- removeTempListeners(this, listeners, 'chmod');
1139
- }
1140
- }
1141
-
1142
1146
  /**
1143
1147
  * @async
1144
1148
  *
@@ -1148,91 +1152,114 @@ class SftpClient {
1148
1152
  * @param {String} srcDir - local source directory
1149
1153
  * @param {String} dstDir - remote destination directory
1150
1154
  * @param {Object} options - (Optional) An object with 2 supported properties,
1151
- * 'filter' and 'useFastput'. The first argument is the full path of the item
1155
+ * 'filter' and 'useFastput'. Filter is a function of two arguments.
1156
+ * The first argument is the full path of a directory entry from the directory
1152
1157
  * to be uploaded and the second argument is a boolean, which will be true if
1153
- * the target path is for a directory. If the function returns true, the item
1158
+ * the target path is for a directory. If the function returns true, this item
1154
1159
  * will be uploaded and excluded when it returns false. The 'useFastput' property is a
1155
1160
  * boolean value. When true, the 'fastPut()' method will be used to upload files. Default
1156
1161
  * is to use the slower, but more supported 'put()' method.
1157
1162
  *
1158
- * @returns {Promise<String>}
1163
+ * @returns {Promise<Array>}
1159
1164
  */
1160
- async _uploadDir(srcDir, dstDir, options) {
1161
- try {
1162
- const absDstDir = await normalizeRemotePath(this, dstDir);
1163
- this.debugMsg(`uploadDir <- SRC = ${srcDir} DST = ${absDstDir}`);
1165
+ async uploadDir(srcDir, dstDir, options) {
1166
+ const getRemoteStatus = async (dstDir) => {
1167
+ let absDstDir = await normalizeRemotePath(this, dstDir);
1168
+ let status = await this.exists(absDstDir);
1169
+ if (status && status !== 'd') {
1170
+ throw this.fmtError(
1171
+ `Bad path ${absDstDir} Not a directory`,
1172
+ 'getRemoteStatus',
1173
+ errorCode.badPath
1174
+ );
1175
+ }
1176
+ return { remoteDir: absDstDir, remoteStatus: status };
1177
+ };
1178
+
1179
+ const checkLocalStatus = (srcDir) => {
1164
1180
  const srcType = localExists(srcDir);
1165
1181
  if (!srcType) {
1166
1182
  throw this.fmtError(
1167
1183
  `Bad path: ${srcDir} not exist`,
1168
- '_uploadDir',
1184
+ 'getLocalStatus',
1169
1185
  errorCode.badPath
1170
1186
  );
1171
1187
  }
1172
1188
  if (srcType !== 'd') {
1173
1189
  throw this.fmtError(
1174
1190
  `Bad path: ${srcDir}: not a directory`,
1175
- '_uploadDir',
1191
+ 'getLocalStatus',
1176
1192
  errorCode.badPath
1177
1193
  );
1178
1194
  }
1179
- const dstStatus = await this.exists(absDstDir);
1180
- if (dstStatus && dstStatus !== 'd') {
1181
- throw this.fmtError(
1182
- `Bad path ${absDstDir} Not a directory`,
1183
- '_uploadDir',
1184
- errorCode.badPath
1185
- );
1186
- }
1187
- if (!dstStatus) {
1188
- await this._mkdir(absDstDir, true);
1195
+ return srcType;
1196
+ };
1197
+
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 = [];
1203
+ 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`);
1215
+ }
1216
+ }
1217
+ resolve(Promise.all(uploads));
1218
+ })
1219
+ .then((pList) => {
1220
+ return Promise.all(pList);
1221
+ })
1222
+ .finally(() => {
1223
+ removeTempListeners(this, listeners, uploadFiles);
1224
+ });
1225
+ };
1226
+
1227
+ try {
1228
+ haveConnection(this, 'uploadDir');
1229
+ this.debugMsg(
1230
+ `uploadDir: srcDir = ${srcDir} dstDir = ${dstDir} options = ${options}`
1231
+ );
1232
+ let { remoteDir, remoteStatus } = await getRemoteStatus(dstDir);
1233
+ this.debugMsg(`uploadDir: remoteDir = ${remoteDir} remoteStatus = ${remoteStatus}`);
1234
+ checkLocalStatus(srcDir);
1235
+ if (!remoteStatus) {
1236
+ await this._mkdir(remoteDir, true);
1189
1237
  }
1190
1238
  let dirEntries = fs.readdirSync(srcDir, {
1191
1239
  encoding: 'utf8',
1192
1240
  withFileTypes: true,
1193
1241
  });
1242
+ this.debugMsg(`uploadDir: dirEntries = ${dirEntries}`);
1194
1243
  if (options?.filter) {
1195
1244
  dirEntries = dirEntries.filter((item) =>
1196
1245
  options.filter(join(srcDir, item.name), item.isDirectory())
1197
1246
  );
1198
1247
  }
1199
- let fileUploads = [];
1200
- for (const e of dirEntries) {
1201
- const newSrc = join(srcDir, e.name);
1202
- const newDst = `${absDstDir}${this.remotePathSep}${e.name}`;
1203
- if (e.isDirectory()) {
1204
- await this.uploadDir(newSrc, newDst, options);
1205
- } else if (e.isFile()) {
1206
- if (options?.useFastput) {
1207
- fileUploads.push(this._fastPut(newSrc, newDst));
1208
- } else {
1209
- fileUploads.push(this._put(newSrc, newDst));
1210
- }
1211
- this.client.emit('upload', { source: newSrc, destination: newDst });
1212
- } else {
1213
- this.debugMsg(`uploadDir: File ignored: ${e.name} not a regular file`);
1214
- }
1215
- await Promise.all(fileUploads);
1248
+ let dirUploads = dirEntries.filter((item) => item.isDirectory());
1249
+ let fileUploads = dirEntries.filter((item) => !item.isDirectory());
1250
+ this.debugMsg(`uploadDir: dirUploads = ${dirUploads}`);
1251
+ this.debugMsg(`uploadDir: fileUploads = ${fileUploads}`);
1252
+ await uploadFiles(srcDir, remoteDir, fileUploads, options?.useFastput);
1253
+ for (const d of dirUploads) {
1254
+ let src = join(srcDir, d.name);
1255
+ let dst = `${remoteDir}/${d.name}`;
1256
+ await this.uploadDir(src, dst, options);
1216
1257
  }
1217
- return `${srcDir} uploaded to ${absDstDir}`;
1258
+ return `${srcDir} uploaded to ${dstDir}`;
1218
1259
  } catch (err) {
1219
1260
  throw err.custom
1220
1261
  ? err
1221
- : this.fmtError(`${err.message} ${srcDir}`, '_uploadDir', err.code);
1222
- }
1223
- }
1224
-
1225
- async uploadDir(srcDir, dstDir, options) {
1226
- let listeners;
1227
- try {
1228
- listeners = addTempListeners(this, 'uploadDir');
1229
- this.debugMsg(`uploadDir -> SRC = ${srcDir} DST = ${dstDir}`);
1230
- haveConnection(this, 'uploadDir');
1231
- return await this._uploadDir(srcDir, dstDir, options);
1232
- } catch (err) {
1233
- throw err.custom ? err : this.fmtError(err, 'uploadDir');
1234
- } finally {
1235
- removeTempListeners(this, listeners, 'chmod');
1262
+ : this.fmtError(`${err.message} ${srcDir}`, 'uploadDir', err.code);
1236
1263
  }
1237
1264
  }
1238
1265
 
@@ -1251,71 +1278,88 @@ class SftpClient {
1251
1278
  * is for a directory. If the function returns true, the item will be
1252
1279
  * downloaded and excluded if teh function returns false.
1253
1280
  *
1254
- * @returns {Promise<String>}
1281
+ * @returns {Promise<Array>}
1255
1282
  */
1256
- async _downloadDir(srcDir, dstDir, options) {
1257
- try {
1258
- let fileList = await this._list(srcDir);
1259
- if (options?.filter) {
1260
- fileList = fileList.filter((item) =>
1261
- options.filter(
1262
- `${srcDir}${this.remotePathSep}${item.name}`,
1263
- item.type === 'd' ? true : false
1264
- )
1265
- );
1283
+ async downloadDir(srcDir, dstDir, options = { filter: null, useFastget: false }) {
1284
+ const _getDownloadList = async (srcDir, filter) => {
1285
+ try {
1286
+ let listing = await this.list(srcDir);
1287
+ if (filter) {
1288
+ return listing.filter((item) =>
1289
+ filter(`${srcDir}/${item.name}`, item.type === 'd')
1290
+ );
1291
+ }
1292
+ return listing;
1293
+ } catch (err) {
1294
+ throw err.custom ? err : this.fmtError(err.message, '_getDownloadList', err.code);
1266
1295
  }
1267
- const localCheck = haveLocalCreate(dstDir);
1268
- if (!localCheck.status && localCheck.details === 'permission denied') {
1269
- throw this.fmtError(
1270
- `Bad path: ${dstDir}: ${localCheck.details}`,
1271
- 'downloadDir',
1272
- localCheck.code
1273
- );
1274
- } else if (localCheck.status && !localCheck.type) {
1275
- fs.mkdirSync(dstDir, { recursive: true });
1276
- } else if (localCheck.status && localCheck.type !== 'd') {
1277
- throw this.fmtError(
1278
- `Bad path: ${dstDir}: not a directory`,
1279
- 'downloadDir',
1280
- errorCode.badPath
1281
- );
1296
+ };
1297
+
1298
+ const _prepareDestination = (dst) => {
1299
+ try {
1300
+ const localCheck = haveLocalCreate(dst);
1301
+ if (!localCheck.status && localCheck.details === 'permission denied') {
1302
+ throw this.fmtError(
1303
+ `Bad path: ${dst}: ${localCheck.details}`,
1304
+ 'prepareDestination',
1305
+ localCheck.code
1306
+ );
1307
+ } else if (localCheck.status && !localCheck.type) {
1308
+ fs.mkdirSync(dst, { recursive: true });
1309
+ } else if (localCheck.status && localCheck.type !== 'd') {
1310
+ throw this.fmtError(
1311
+ `Bad path: ${dstDir}: not a directory`,
1312
+ '_prepareDestination',
1313
+ errorCode.badPath
1314
+ );
1315
+ }
1316
+ } catch (err) {
1317
+ throw err.custom
1318
+ ? err
1319
+ : this.fmtError(err.message, '_prepareDestination', err.code);
1282
1320
  }
1283
- let downloadFiles = [];
1284
- for (const f of fileList) {
1285
- const newSrc = `${srcDir}${this.remotePathSep}${f.name}`;
1286
- const newDst = join(dstDir, f.name);
1287
- if (f.type === 'd') {
1288
- await this._downloadDir(newSrc, newDst, options);
1289
- } else if (f.type === '-') {
1290
- if (options?.useFasget) {
1291
- downloadFiles.push(this._fastGet(newSrc, newDst));
1321
+ };
1322
+
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 = [];
1328
+ 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));
1292
1333
  } else {
1293
- downloadFiles.push(this._get(newSrc, newDst));
1334
+ pList.push(this.get(src, dst, false));
1294
1335
  }
1295
- this.client.emit('download', { source: newSrc, destination: newDst });
1296
- } else {
1297
- this.debugMsg(`downloadDir: File ignored: ${f.name} not regular file`);
1336
+ this.client.emit('download', { source: src, destination: dst });
1298
1337
  }
1338
+ return resolve(Promise.all(pList));
1339
+ }).finally(() => {
1340
+ removeTempListeners(this, listeners, '_downloadFiles');
1341
+ });
1342
+ };
1343
+
1344
+ try {
1345
+ haveConnection(this, 'downloadDir');
1346
+ let downloadList = await _getDownloadList(srcDir, options.filter);
1347
+ _prepareDestination(dstDir);
1348
+ let fileDownloads = downloadList.filter((i) => i.type !== 'd');
1349
+ if (fileDownloads.length) {
1350
+ await _downloadFiles(srcDir, dstDir, fileDownloads, options.useFastget);
1351
+ }
1352
+ let dirDownloads = downloadList.filter((i) => i.type === 'd');
1353
+ for (const d of dirDownloads) {
1354
+ let src = `${srcDir}/${d.name}`;
1355
+ let dst = join(dstDir, d.name);
1356
+ await this.downloadDir(src, dst);
1299
1357
  }
1300
- await Promise.all(downloadFiles);
1301
1358
  return `${srcDir} downloaded to ${dstDir}`;
1302
1359
  } catch (err) {
1303
1360
  throw err.custom
1304
1361
  ? err
1305
- : this.fmtError(`${err.message} ${srcDir}`, '_downloadDir', err.code);
1306
- }
1307
- }
1308
-
1309
- async downloadDir(srcDir, dstDir, options) {
1310
- let listeners;
1311
- try {
1312
- listeners = addTempListeners(this, 'downloadDir');
1313
- haveConnection(this, 'downloadDir');
1314
- return await this._downloadDir(srcDir, dstDir, options);
1315
- } catch (err) {
1316
- throw err.custom ? err : this.fmtError(err, 'downloadDir', err.code);
1317
- } finally {
1318
- removeTempListeners(this, listeners, 'downloadDir');
1362
+ : this.fmtError(`${err.message}: ${srcDir}`, 'downloadDir', err.code);
1319
1363
  }
1320
1364
  }
1321
1365
 
@@ -1454,7 +1498,7 @@ class SftpClient {
1454
1498
  resolve(true);
1455
1499
  };
1456
1500
  this.on('close', endCloseHandler);
1457
- if (this.client.sftp) {
1501
+ if (this.sftp) {
1458
1502
  this.client.end();
1459
1503
  } else {
1460
1504
  // no actual connection exists - just resolve