ssh2-sftp-client 6.0.1 → 7.0.3

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
@@ -4,20 +4,23 @@
4
4
 
5
5
  'use strict';
6
6
 
7
- const {Client} = require('ssh2');
7
+ const { Client } = require('ssh2');
8
8
  const fs = require('fs');
9
9
  const concat = require('concat-stream');
10
10
  const promiseRetry = require('promise-retry');
11
- const {join, parse} = require('path');
11
+ const { join, parse } = require('path');
12
12
  const {
13
13
  fmtError,
14
14
  addTempListeners,
15
15
  removeTempListeners,
16
16
  haveConnection,
17
17
  normalizeRemotePath,
18
- localExists
18
+ localExists,
19
+ haveLocalAccess,
20
+ haveLocalCreate,
21
+ sleep,
19
22
  } = require('./utils');
20
- const {errorCode} = require('./constants');
23
+ const { errorCode } = require('./constants');
21
24
 
22
25
  class SftpClient {
23
26
  constructor(clientName) {
@@ -26,31 +29,43 @@ class SftpClient {
26
29
  this.clientName = clientName ? clientName : 'sftp';
27
30
  this.endCalled = false;
28
31
  this.errorHandled = false;
32
+ this.closeHandled = false;
33
+ this.endHandled = false;
29
34
  this.remotePathSep = '/';
30
35
  this.remotePlatform = 'unix';
31
36
  this.debug = undefined;
32
37
 
33
38
  this.client.on('close', () => {
34
- if (!this.endCalled) {
35
- this.debugMsg('Unexpected close event raised by server');
39
+ if (this.endCalled || this.closeHandled) {
40
+ // we are processing an expected end event or close event handled elsewhere
41
+ this.debugMsg('Global: Ignoring handled close event');
42
+ } else {
43
+ this.debugMsg('Global: Handling unexpected close event');
36
44
  this.sftp = undefined;
37
45
  }
38
46
  });
47
+
39
48
  this.client.on('end', () => {
40
- if (!this.endCalled) {
41
- this.debugMsg('Unexpected end event raised by server');
49
+ if (this.endCalled || this.endHandled) {
50
+ // end event expected or handled elsewhere
51
+ this.debugMsg('Global: Ignoring hanlded end event');
52
+ } else {
53
+ this.debugMsg('Global: Handling unexpected end event');
42
54
  this.sftp = undefined;
43
55
  }
44
56
  });
57
+
45
58
  this.client.on('error', (err) => {
46
- if (!this.errorHandled) {
47
- throw fmtError(
48
- `Unexpected error: ${err.message}`,
49
- 'global-error-handler',
50
- err.code
51
- );
59
+ if (this.endCalled || this.errorHandled) {
60
+ // error event expected or handled elsewhere
61
+ this.debugMsg('Global: Ignoring handled error');
52
62
  } else {
53
- this.errorHandled = false;
63
+ this.debugMsg(`Global; Handling unexpected error; ${err.message}`);
64
+ this.sftp = undefined;
65
+ console.log(
66
+ `ssh2-sftp-client: Unexpected error: ${err.message}. Error code: ${err.code}`
67
+ );
68
+ //throw fmtError(err, 'Global');
54
69
  }
55
70
  });
56
71
  }
@@ -77,81 +92,125 @@ class SftpClient {
77
92
  * @param {function} callback - function called when event triggers
78
93
  */
79
94
  on(eventType, callback) {
80
- this.debugMsg(`Adding listener to ${eventType}`);
81
- this.client.on(eventType, callback);
95
+ this.debugMsg(`Adding listener to ${eventType} event`);
96
+ this.client.prependListener(eventType, callback);
82
97
  }
83
98
 
84
99
  removeListener(eventType, callback) {
85
- this.debugMsg(`Removing listener from ${eventType}`);
100
+ this.debugMsg(`Removing listener from ${eventType} event`);
86
101
  this.client.removeListener(eventType, callback);
87
102
  }
88
103
 
104
+ _resetEventFlags() {
105
+ this.closeHandled = false;
106
+ this.endHandled = false;
107
+ this.errorHandled = false;
108
+ }
109
+
89
110
  /**
90
111
  * @async
91
112
  *
92
113
  * Create a new SFTP connection to a remote SFTP server
93
114
  *
94
115
  * @param {Object} config - an SFTP configuration object
95
- * @param {string} connectMethod - ???
96
116
  *
97
117
  * @return {Promise} which will resolve to an sftp client object
98
118
  *
99
119
  */
100
- sftpConnect(config) {
101
- let connectReady;
120
+ getConnection(config) {
121
+ let doReady;
122
+ return (
123
+ new Promise((resolve, reject) => {
124
+ addTempListeners(this, 'getConnection', reject);
125
+ this.debugMsg('getConnection: created promise');
126
+ doReady = () => {
127
+ this.debugMsg('getConnection: got connection - promise resolved');
128
+ resolve(true);
129
+ };
130
+ this.on('ready', doReady);
131
+ this.client.connect(config);
132
+ })
133
+ // .catch((err) => {
134
+ // return Promise.reject(err);
135
+ // })
136
+ .finally(async (resp) => {
137
+ this.debugMsg('getConnection: finally clause fired');
138
+ await sleep(500);
139
+ this.removeListener('ready', doReady);
140
+ removeTempListeners(this, 'getConnection');
141
+ this._resetEventFlags();
142
+ return resp;
143
+ })
144
+ );
145
+ }
102
146
 
147
+ getSftpChannel() {
103
148
  return new Promise((resolve, reject) => {
104
- addTempListeners(this, 'sftpConnect', reject);
105
- connectReady = () => {
106
- this.client.sftp((err, sftp) => {
107
- if (err) {
108
- this.debugMsg(`SFTP channel error: ${err.message} ${err.code}`);
109
- reject(fmtError(err, 'sftpConnect', err.code));
110
- } else {
111
- this.debugMsg('SFTP channel established');
112
- resolve(sftp);
113
- }
114
- });
115
- };
116
- // addTempListeners(this, 'sftpConnect', reject);
117
- this.client.on('ready', connectReady).connect(config);
118
- }).finally((rsp) => {
119
- this.removeListener('ready', connectReady);
120
- removeTempListeners(this.client);
121
- return rsp;
149
+ addTempListeners(this, 'getSftpChannel', reject);
150
+ this.debugMsg('getSftpChannel: created promise');
151
+ this.client.sftp((err, sftp) => {
152
+ if (err) {
153
+ this.debugMsg(`getSftpChannel: SFTP Channel Error: ${err.message}`);
154
+ reject(fmtError(err, 'getSftpChannel', err.code));
155
+ } else {
156
+ this.debugMsg('getSftpChannel: SFTP channel established');
157
+ this.sftp = sftp;
158
+ resolve(sftp);
159
+ }
160
+ });
161
+ }).finally((resp) => {
162
+ this.debugMsg('getSftpChannel: finally clause fired');
163
+ removeTempListeners(this, 'getSftpChannel');
164
+ this._resetEventFlags();
122
165
  });
123
166
  }
124
167
 
125
- connect(config) {
126
- if (config.debug) {
127
- this.debug = config.debug;
128
- this.debugMsg('Debugging turned on');
129
- }
130
- if (this.sftp) {
131
- this.debugMsg('Already connected - reject');
132
- return Promise.reject(
133
- fmtError(
168
+ /**
169
+ * @async
170
+ *
171
+ * Create a new SFTP connection to a remote SFTP server.
172
+ * The connection options are the same as those offered
173
+ * by the underlying SSH2 module.
174
+ *
175
+ * @param {Object} config - an SFTP configuration object
176
+ *
177
+ * @return {Promise} which will resolve to an sftp client object
178
+ *
179
+ */
180
+ async connect(config) {
181
+ try {
182
+ if (config.debug) {
183
+ this.debug = config.debug;
184
+ this.debugMsg('connect: Debugging turned on');
185
+ }
186
+ if (this.sftp) {
187
+ this.debugMsg('connect: Already connected - reject');
188
+ throw fmtError(
134
189
  'An existing SFTP connection is already defined',
135
190
  'connect',
136
191
  errorCode.connect
137
- )
192
+ );
193
+ }
194
+ await promiseRetry(
195
+ (retry, attempt) => {
196
+ this.debugMsg(`connect: Connect attempt ${attempt}`);
197
+ return this.getConnection(config).catch((err) => {
198
+ this.debugMsg('getConnection retry catch');
199
+ retry(err);
200
+ });
201
+ },
202
+ {
203
+ retries: config.retries || 1,
204
+ factor: config.retry_factor || 2,
205
+ minTimeout: config.retry_minTimeout || 1000,
206
+ }
138
207
  );
208
+ await this.getSftpChannel();
209
+ } catch (err) {
210
+ this.debugMsg(`connect: Error ${err.message}`);
211
+ this._resetEventFlags();
212
+ throw fmtError(err, 'connect');
139
213
  }
140
- return promiseRetry(
141
- (retry, attempt) => {
142
- this.debugMsg(`Connect attempt ${attempt}`);
143
- return this.sftpConnect(config).catch((err) => {
144
- retry(err);
145
- });
146
- },
147
- {
148
- retries: config.retries || 1,
149
- factor: config.retry_factor || 2,
150
- minTimeout: config.retry_minTimeout || 1000
151
- }
152
- ).then((sftp) => {
153
- this.sftp = sftp;
154
- });
155
214
  }
156
215
 
157
216
  /**
@@ -160,11 +219,10 @@ class SftpClient {
160
219
  * Returns the real absolute path on the remote server. Is able to handle
161
220
  * both '.' and '..' in path names, but not '~'. If the path is relative
162
221
  * then the current working directory is prepended to create an absolute path.
163
- * Returns undefined if the
164
- * path does not exists.
222
+ * Returns undefined if the path does not exists.
165
223
  *
166
224
  * @param {String} remotePath - remote path, may be relative
167
- * @returns {}
225
+ * @returns {Promise} - remote absolute path or undefined
168
226
  */
169
227
  realPath(remotePath) {
170
228
  return new Promise((resolve, reject) => {
@@ -187,7 +245,8 @@ class SftpClient {
187
245
  });
188
246
  }
189
247
  }).finally((rsp) => {
190
- removeTempListeners(this.client);
248
+ removeTempListeners(this, 'realPath');
249
+ this._resetEventFlags();
191
250
  return rsp;
192
251
  });
193
252
  }
@@ -199,17 +258,17 @@ class SftpClient {
199
258
  /**
200
259
  * Retrieves attributes for path
201
260
  *
202
- * @param {String} path, a string containing the path to a file
203
- * @return {Promise} stats, attributes info
261
+ * @param {String} remotePath - a string containing the path to a file
262
+ * @return {Promise} stats - attributes info
204
263
  */
205
264
  async stat(remotePath) {
206
265
  const _stat = (aPath) => {
207
266
  return new Promise((resolve, reject) => {
208
- this.debugMsg(`stat -> ${aPath}`);
209
- addTempListeners(this, 'stat', reject);
267
+ this.debugMsg(`_stat: ${aPath}`);
268
+ addTempListeners(this, '_stat', reject);
210
269
  this.sftp.stat(aPath, (err, stats) => {
211
270
  if (err) {
212
- this.debugMsg(`stat error ${err.message} code: ${err.code}`);
271
+ this.debugMsg(`_stat: Error ${err.message} code: ${err.code}`);
213
272
  if (err.code === 2 || err.code === 4) {
214
273
  reject(
215
274
  fmtError(
@@ -224,8 +283,7 @@ class SftpClient {
224
283
  );
225
284
  }
226
285
  } else {
227
- this.debugMsg('stats <- ', stats);
228
- resolve({
286
+ let result = {
229
287
  mode: stats.mode,
230
288
  uid: stats.uid,
231
289
  gid: stats.gid,
@@ -238,12 +296,14 @@ class SftpClient {
238
296
  isCharacterDevice: stats.isCharacterDevice(),
239
297
  isSymbolicLink: stats.isSymbolicLink(),
240
298
  isFIFO: stats.isFIFO(),
241
- isSocket: stats.isSocket()
242
- });
299
+ isSocket: stats.isSocket(),
300
+ };
301
+ this.debugMsg('_stat: stats <- ', result);
302
+ resolve(result);
243
303
  }
244
304
  });
245
305
  }).finally((rsp) => {
246
- removeTempListeners(this.client);
306
+ removeTempListeners(this, 'stat');
247
307
  return rsp;
248
308
  });
249
309
  };
@@ -253,11 +313,8 @@ class SftpClient {
253
313
  let absPath = await normalizeRemotePath(this, remotePath);
254
314
  return _stat(absPath);
255
315
  } catch (err) {
256
- if (err.custom) {
257
- throw err;
258
- } else {
259
- throw fmtError(err, 'stat', err.code);
260
- }
316
+ this._resetEventFlags();
317
+ throw err.custom ? err : fmtError(err, 'stat', err.code);
261
318
  }
262
319
  }
263
320
 
@@ -267,47 +324,53 @@ class SftpClient {
267
324
  * Tests to see if an object exists. If it does, return the type of that object
268
325
  * (in the format returned by list). If it does not exist, return false.
269
326
  *
270
- * @param {string} path - path to the object on the sftp server.
327
+ * @param {string} remotePath - path to the object on the sftp server.
271
328
  *
272
- * @return {boolean} returns false if object does not exist. Returns type of
329
+ * @return {Promise} returns false if object does not exist. Returns type of
273
330
  * object if it does
274
331
  */
275
332
  async exists(remotePath) {
276
333
  try {
277
334
  if (haveConnection(this, 'exists')) {
278
335
  if (remotePath === '.') {
336
+ this.debugMsg('exists: . = d');
279
337
  return 'd';
280
338
  }
281
339
  let absPath = await normalizeRemotePath(this, remotePath);
282
340
  try {
283
- this.debugMsg(`exists -> ${absPath}`);
341
+ this.debugMsg(`exists: ${remotePath} -> ${absPath}`);
284
342
  let info = await this.stat(absPath);
285
- this.debugMsg('exists <- ', info);
343
+ this.debugMsg('exists: <- ', info);
286
344
  if (info.isDirectory) {
345
+ this.debugMsg(`exists: ${remotePath} = d`);
287
346
  return 'd';
288
347
  }
289
348
  if (info.isSymbolicLink) {
349
+ this.debugMsg(`exists: ${remotePath} = l`);
290
350
  return 'l';
291
351
  }
292
352
  if (info.isFile) {
353
+ this.debugMsg(`exists: ${remotePath} = -`);
293
354
  return '-';
294
355
  }
356
+ this.debugMsg(`exists: ${remotePath} = false`);
295
357
  return false;
296
358
  } catch (err) {
297
359
  if (err.code === errorCode.notexist) {
360
+ this.debugMsg(
361
+ `exists: ${remotePath} = false errorCode = ${err.code}`
362
+ );
298
363
  return false;
299
364
  }
365
+ this.debugMsg(`exists: throw error ${err.message} ${err.code}`);
300
366
  throw err;
301
367
  }
302
- } else {
303
- return false;
304
368
  }
369
+ this.debugMsg(`exists: default ${remotePath} = false`);
370
+ return false;
305
371
  } catch (err) {
306
- if (err.custom) {
307
- throw err;
308
- } else {
309
- throw fmtError(err, 'exists', err.code);
310
- }
372
+ this._resetEventFlags();
373
+ throw err.custom ? err : fmtError(err, 'exists', err.code);
311
374
  }
312
375
  }
313
376
 
@@ -322,21 +385,20 @@ class SftpClient {
322
385
  *
323
386
  * @param {String} remotePath - path to remote directory
324
387
  * @param {RegExp} pattern - regular expression to match filenames
325
- * @returns {Array} file description objects
388
+ * @returns {Promise} array of file description objects
326
389
  * @throws {Error}
327
390
  */
328
391
  list(remotePath, pattern = /.*/) {
329
392
  return new Promise((resolve, reject) => {
330
393
  if (haveConnection(this, 'list', reject)) {
331
394
  const reg = /-/gi;
332
- this.debugMsg(`list -> ${remotePath} filter -> ${pattern}`);
395
+ this.debugMsg(`list: ${remotePath} filter: ${pattern}`);
333
396
  addTempListeners(this, 'list', reject);
334
397
  this.sftp.readdir(remotePath, (err, fileList) => {
335
398
  if (err) {
336
- this.debugMsg(`list error ${err.message} code: ${err.code}`);
399
+ this.debugMsg(`list: Error ${err.message} code: ${err.code}`);
337
400
  reject(fmtError(`${err.message} ${remotePath}`, 'list', err.code));
338
401
  } else {
339
- this.debugMsg('list <- ', fileList);
340
402
  let newList = [];
341
403
  // reset file info
342
404
  if (fileList) {
@@ -350,10 +412,10 @@ class SftpClient {
350
412
  rights: {
351
413
  user: item.longname.substr(1, 3).replace(reg, ''),
352
414
  group: item.longname.substr(4, 3).replace(reg, ''),
353
- other: item.longname.substr(7, 3).replace(reg, '')
415
+ other: item.longname.substr(7, 3).replace(reg, ''),
354
416
  },
355
417
  owner: item.attrs.uid,
356
- group: item.attrs.gid
418
+ group: item.attrs.gid,
357
419
  };
358
420
  });
359
421
  }
@@ -365,12 +427,15 @@ class SftpClient {
365
427
  let newPattern = pattern.replace(/\*([^*])*?/gi, '.*');
366
428
  regex = new RegExp(newPattern);
367
429
  }
368
- resolve(newList.filter((item) => regex.test(item.name)));
430
+ let filteredList = newList.filter((item) => regex.test(item.name));
431
+ this.debugMsg('list: result: ', filteredList);
432
+ resolve(filteredList);
369
433
  }
370
434
  });
371
435
  }
372
436
  }).finally((rsp) => {
373
- removeTempListeners(this.client);
437
+ removeTempListeners(this, 'list');
438
+ this._resetEventFlags();
374
439
  return rsp;
375
440
  });
376
441
  }
@@ -383,35 +448,56 @@ class SftpClient {
383
448
  * piped into the stream or undefined, in which case the data is returned as
384
449
  * a Buffer object.
385
450
  *
386
- * @param {String} path, remote file path
387
- * @param {string|stream|undefined} dst, data destination
388
- * @param {Object} userOptions, options passed to get
451
+ * @param {String} remotePath - remote file path
452
+ * @param {string|stream|undefined} dst - data destination
453
+ * @param {Object} options - options object with supported properties of readStreamOptions,
454
+ * writeStreamOptions and pipeOptions.
389
455
  *
390
456
  * @return {Promise}
391
457
  */
392
- get(remotePath, dst, options = {}) {
458
+ get(
459
+ remotePath,
460
+ dst,
461
+ options = { readStreamOptions: {}, writeStreamOptions: {}, pipeOptions: {} }
462
+ ) {
463
+ let rdr, wtr;
464
+
393
465
  return new Promise((resolve, reject) => {
394
466
  if (haveConnection(this, 'get', reject)) {
395
467
  this.debugMsg(`get -> ${remotePath} `, options);
396
468
  addTempListeners(this, 'get', reject);
397
- let rdr = this.sftp.createReadStream(remotePath, options);
469
+ rdr = this.sftp.createReadStream(
470
+ remotePath,
471
+ options.readStreamOptions ? options.readStreamOptions : {}
472
+ );
398
473
  rdr.once('error', (err) => {
399
474
  reject(fmtError(`${err.message} ${remotePath}`, 'get', err.code));
400
475
  });
401
476
  if (dst === undefined) {
402
477
  // no dst specified, return buffer of data
403
478
  this.debugMsg('get returning buffer of data');
404
- let concatStream = concat((buff) => {
405
- rdr.removeAllListeners('error');
479
+ wtr = concat((buff) => {
480
+ //rdr.removeAllListeners('error');
406
481
  resolve(buff);
407
482
  });
408
- rdr.pipe(concatStream);
409
483
  } else {
410
- let wtr;
411
484
  if (typeof dst === 'string') {
412
485
  // dst local file path
413
486
  this.debugMsg('get returning local file');
414
- wtr = fs.createWriteStream(dst);
487
+ const localCheck = haveLocalCreate(dst);
488
+ if (!localCheck.status) {
489
+ return reject(
490
+ fmtError(
491
+ `Bad path: ${dst}: ${localCheck.details}`,
492
+ 'get',
493
+ localCheck.code
494
+ )
495
+ );
496
+ }
497
+ wtr = fs.createWriteStream(
498
+ dst,
499
+ options.writeStreamOptions ? options.writeStreamOptions : {}
500
+ );
415
501
  } else {
416
502
  this.debugMsg('get returning data into supplied stream');
417
503
  wtr = dst;
@@ -424,25 +510,35 @@ class SftpClient {
424
510
  err.code
425
511
  )
426
512
  );
427
- if (options.autoClose === false) {
428
- rdr.destroy();
429
- }
430
513
  });
431
- wtr.once('finish', () => {
432
- if (options.autoClose === false) {
433
- rdr.destroy();
434
- }
514
+ rdr.once('end', () => {
435
515
  if (typeof dst === 'string') {
436
516
  resolve(dst);
437
517
  } else {
438
518
  resolve(wtr);
439
519
  }
440
520
  });
441
- rdr.pipe(wtr);
442
521
  }
522
+ rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
443
523
  }
444
524
  }).finally((rsp) => {
445
- removeTempListeners(this.client);
525
+ removeTempListeners(this, 'get');
526
+ this._resetEventFlags();
527
+ if (
528
+ rdr &&
529
+ options.readStreamOptions &&
530
+ options.readStreamOptions.autoClose === false
531
+ ) {
532
+ rdr.destroy();
533
+ }
534
+ if (
535
+ wtr &&
536
+ options.writeStreamOptions &&
537
+ options.writeStreamOptions.autoClose === false &&
538
+ typeof dst === 'string'
539
+ ) {
540
+ wtr.destroy();
541
+ }
446
542
  return rsp;
447
543
  });
448
544
  }
@@ -460,40 +556,47 @@ class SftpClient {
460
556
  * @param {Object} options
461
557
  * @return {Promise} the result of downloading the file
462
558
  */
463
- fastGet(remotePath, localPath, options) {
464
- return this.exists(remotePath)
465
- .then((ftype) => {
466
- if (ftype !== '-') {
467
- let msg =
468
- ftype === false
469
- ? `No such file ${remotePath}`
470
- : `Not a regular file ${remotePath}`;
471
- return Promise.reject(fmtError(msg, 'fastGet', errorCode.badPath));
559
+ async fastGet(remotePath, localPath, options) {
560
+ try {
561
+ const ftype = await this.exists(remotePath);
562
+ if (ftype !== '-') {
563
+ const msg =
564
+ ftype === false
565
+ ? `No such file ${remotePath}`
566
+ : `Not a regular file ${remotePath}`;
567
+ let err = new Error(msg);
568
+ err.code = errorCode.badPath;
569
+ throw err;
570
+ }
571
+ const localCheck = haveLocalCreate(localPath);
572
+ if (!localCheck.status) {
573
+ let err = new Error(`Bad path: ${localPath}: ${localCheck.details}`);
574
+ err.code = errorCode.badPath;
575
+ throw err;
576
+ }
577
+ await new Promise((resolve, reject) => {
578
+ if (haveConnection(this, 'fastGet', reject)) {
579
+ this.debugMsg(
580
+ `fastGet -> remote: ${remotePath} local: ${localPath} `,
581
+ options
582
+ );
583
+ addTempListeners(this, 'fastGet', reject);
584
+ this.sftp.fastGet(remotePath, localPath, options, (err) => {
585
+ if (err) {
586
+ this.debugMsg(`fastGet error ${err.message} code: ${err.code}`);
587
+ reject(err);
588
+ }
589
+ resolve(`${remotePath} was successfully download to ${localPath}!`);
590
+ });
472
591
  }
473
- })
474
- .then(() => {
475
- return new Promise((resolve, reject) => {
476
- if (haveConnection(this, 'fastGet', reject)) {
477
- this.debugMsg(
478
- `fastGet -> remote: ${remotePath} local: ${localPath} `,
479
- options
480
- );
481
- addTempListeners(this, 'fastGet', reject);
482
- this.sftp.fastGet(remotePath, localPath, options, (err) => {
483
- if (err) {
484
- this.debugMsg(`fastGet error ${err.message} code: ${err.code}`);
485
- reject(fmtError(err, 'fastGet'));
486
- }
487
- resolve(
488
- `${remotePath} was successfully download to ${localPath}!`
489
- );
490
- });
491
- }
492
- }).finally((rsp) => {
493
- removeTempListeners(this.client);
494
- return rsp;
495
- });
592
+ }).finally((rsp) => {
593
+ removeTempListeners(this, 'fastGet');
594
+ return rsp;
496
595
  });
596
+ } catch (err) {
597
+ this._resetEventFlags();
598
+ throw fmtError(err, 'fastGet');
599
+ }
497
600
  }
498
601
 
499
602
  /**
@@ -511,60 +614,51 @@ class SftpClient {
511
614
  */
512
615
  fastPut(localPath, remotePath, options) {
513
616
  this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
514
- return localExists(localPath)
515
- .then((localStatus) => {
516
- this.debugMsg(`fastPut <- localStatus ${localStatus}`);
517
- if (localStatus !== '-') {
518
- this.debugMsg('fastPut reject bad source path');
519
- return Promise.reject(
520
- fmtError(`Bad path ${localPath}`, 'fastPut', errorCode.badPath)
521
- );
522
- }
523
- return new Promise((resolve, reject) => {
524
- fs.access(localPath, fs.constants.F_OK | fs.constants.R_OK, (err) => {
525
- if (err) {
526
- this.debugMsg('fastPut reject no access source');
527
- reject(
528
- fmtError(`${err.message} ${localPath}`, 'fastPut', err.code)
529
- );
530
- } else {
531
- this.debugMsg('fastPut source access ok');
532
- resolve(true);
533
- }
534
- });
535
- });
536
- })
537
- .then(() => {
538
- return new Promise((resolve, reject) => {
539
- if (haveConnection(this, 'fastPut', reject)) {
540
- this.debugMsg(
541
- `fastPut -> local: ${localPath} remote: ${remotePath} opts: ${JSON.stringify(
542
- options
543
- )}`
617
+ return new Promise((resolve, reject) => {
618
+ const localCheck = haveLocalAccess(localPath);
619
+ if (!localCheck.status) {
620
+ reject(
621
+ fmtError(
622
+ `Bad path: ${localPath}: ${localCheck.details}`,
623
+ 'fastPut',
624
+ localCheck.code
625
+ )
626
+ );
627
+ } else if (localCheck.status && localExists(localPath) === 'd') {
628
+ reject(
629
+ fmtError(
630
+ `Bad path: ${localPath} not a regular file`,
631
+ 'fastPut',
632
+ errorCode.badPath
633
+ )
634
+ );
635
+ } else if (haveConnection(this, 'fastPut', reject)) {
636
+ this.debugMsg(
637
+ `fastPut -> local: ${localPath} remote: ${remotePath} opts: ${JSON.stringify(
638
+ options
639
+ )}`
640
+ );
641
+ addTempListeners(this, 'fastPut', reject);
642
+ this.sftp.fastPut(localPath, remotePath, options, (err) => {
643
+ if (err) {
644
+ this.debugMsg(`fastPut error ${err.message} ${err.code}`);
645
+ reject(
646
+ fmtError(
647
+ `${err.message} Local: ${localPath} Remote: ${remotePath}`,
648
+ 'fastPut',
649
+ err.code
650
+ )
544
651
  );
545
- addTempListeners(this, 'fastPut', reject);
546
- this.sftp.fastPut(localPath, remotePath, options, (err) => {
547
- if (err) {
548
- this.debugMsg(`fastPut error ${err.message} ${err.code}`);
549
- reject(
550
- fmtError(
551
- `${err.message} Local: ${localPath} Remote: ${remotePath}`,
552
- 'fastPut',
553
- err.code
554
- )
555
- );
556
- }
557
- this.debugMsg('fastPut file transferred');
558
- resolve(
559
- `${localPath} was successfully uploaded to ${remotePath}!`
560
- );
561
- });
562
652
  }
563
- }).finally((rsp) => {
564
- removeTempListeners(this.client);
565
- return rsp;
653
+ this.debugMsg('fastPut file transferred');
654
+ resolve(`${localPath} was successfully uploaded to ${remotePath}!`);
566
655
  });
567
- });
656
+ }
657
+ }).finally((rsp) => {
658
+ removeTempListeners(this, 'fastPut');
659
+ this._resetEventFlags();
660
+ return rsp;
661
+ });
568
662
  }
569
663
 
570
664
  /**
@@ -572,140 +666,148 @@ class SftpClient {
572
666
  * can be a buffer, string or read stream. If 'src' is a string, it
573
667
  * should be the path to a local file.
574
668
  *
575
- * @param {String|Buffer|stream} src - source data to use
669
+ * @param {String|Buffer|stream} localSrc - source data to use
576
670
  * @param {String} remotePath - path to remote file
577
- * @param {Object} options - options used for write stream configuration
578
- * value supported by node streams.
671
+ * @param {Object} options - options used for read, write stream and pipe configuration
672
+ * value supported by node. Allowed properties are readStreamOptions,
673
+ * writeStreamOptions and pipeOptions.
579
674
  * @return {Promise}
580
675
  */
581
- put(localSrc, remotePath, options = {}) {
582
- this.debugMsg(
583
- `put ${
584
- typeof localSrc === 'string' ? localSrc : '<buffer | stream>'
585
- } -> ${remotePath}`,
586
- options
587
- );
588
- return localExists(typeof localSrc === 'string' ? localSrc : 'dummy')
589
- .then((localStatus) => {
590
- if (typeof localSrc === 'string' && localStatus !== '-') {
591
- this.debugMsg(`put: file does not exist ${localSrc} - rejecting`);
592
- return Promise.reject(
593
- fmtError(`Bad path ${localSrc}`, 'put', errorCode.badPath)
676
+ put(
677
+ localSrc,
678
+ remotePath,
679
+ options = { readStreamOptions: {}, writeStreamOptions: {}, pipeOptions: {} }
680
+ ) {
681
+ let wtr, rdr;
682
+
683
+ return new Promise((resolve, reject) => {
684
+ if (typeof localSrc === 'string') {
685
+ const localCheck = haveLocalAccess(localSrc);
686
+ if (!localCheck.status) {
687
+ this.debugMsg(`put: local source check error ${localCheck.details}`);
688
+ return reject(
689
+ fmtError(
690
+ `Bad path: ${localSrc}: ${localCheck.details}`,
691
+ 'put',
692
+ localCheck.code
693
+ )
594
694
  );
595
695
  }
596
- return new Promise((resolve, reject) => {
696
+ }
697
+ if (haveConnection(this, 'put')) {
698
+ addTempListeners(this, 'put', reject);
699
+ wtr = this.sftp.createWriteStream(
700
+ remotePath,
701
+ options.writeStreamOptions ? options.writeStreamOptions : {}
702
+ );
703
+ wtr.once('error', (err) => {
704
+ this.debugMsg(`put: write stream error ${err.message}`);
705
+ reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
706
+ });
707
+ wtr.once('finish', () => {
708
+ this.debugMsg('put: promise resolved');
709
+ resolve(`Uploaded data stream to ${remotePath}`);
710
+ });
711
+ if (localSrc instanceof Buffer) {
712
+ this.debugMsg('put source is a buffer');
713
+ wtr.end(localSrc);
714
+ } else {
597
715
  if (typeof localSrc === 'string') {
598
- fs.access(
716
+ this.debugMsg(`put source is a file path: ${localSrc}`);
717
+ rdr = fs.createReadStream(
599
718
  localSrc,
600
- fs.constants.F_OK | fs.constants.R_OK,
601
- (err) => {
602
- if (err) {
603
- this.debugMsg(`put: Cannot read ${localSrc} - rejecting`);
604
- reject(
605
- fmtError(
606
- `Permission denied ${localSrc}`,
607
- 'put',
608
- errorCode.permission
609
- )
610
- );
611
- } else {
612
- this.debugMsg('put: localSrc file OK');
613
- resolve(true);
614
- }
615
- }
719
+ options.readStreamOptions ? options.readStreamOptions : {}
616
720
  );
617
721
  } else {
618
- this.debugMsg('put: localSrc buffer or string OK');
619
- resolve(true);
722
+ this.debugMsg('put source is a stream');
723
+ rdr = localSrc;
620
724
  }
621
- });
622
- })
623
- .then(() => {
624
- return new Promise((resolve, reject) => {
625
- if (haveConnection(this, 'put', reject)) {
626
- addTempListeners(this, 'put', reject);
627
- let stream = this.sftp.createWriteStream(remotePath, options);
628
- stream.once('error', (err) => {
629
- reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
630
- });
631
- stream.once('finish', () => {
632
- if (options.autoClose === false) {
633
- stream.destroy();
634
- }
635
- resolve(`Uploaded data stream to ${remotePath}`);
636
- });
637
- if (localSrc instanceof Buffer) {
638
- this.debugMsg('put source is a buffer');
639
- stream.end(localSrc);
640
- } else {
641
- let rdr;
642
- if (typeof localSrc === 'string') {
643
- this.debugMsg(`put source is a file path: ${localSrc}`);
644
- rdr = fs.createReadStream(localSrc);
645
- } else {
646
- this.debugMsg('put source is a stream');
647
- rdr = localSrc;
648
- }
649
- rdr.once('error', (err) => {
650
- reject(
651
- fmtError(
652
- `${err.message} ${
653
- typeof localSrc === 'string' ? localSrc : ''
654
- }`,
655
- 'put',
656
- err.code
657
- )
658
- );
659
- if (options.autoClose === false) {
660
- stream.destroy();
661
- }
662
- });
663
- rdr.pipe(stream);
664
- }
665
- }
666
- }).finally((rsp) => {
667
- removeTempListeners(this.client);
668
- return rsp;
669
- });
670
- });
725
+ rdr.once('error', (err) => {
726
+ this.debugMsg(`put: read stream error ${err.message}`);
727
+ reject(
728
+ fmtError(
729
+ `${err.message} ${
730
+ typeof localSrc === 'string' ? localSrc : ''
731
+ }`,
732
+ 'put',
733
+ err.code
734
+ )
735
+ );
736
+ });
737
+ rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
738
+ }
739
+ }
740
+ }).finally((resp) => {
741
+ removeTempListeners(this, 'put');
742
+ this._resetEventFlags();
743
+ if (
744
+ rdr &&
745
+ options.readStreamOptions &&
746
+ options.readStreamOptions.autoClose === false &&
747
+ typeof localSrc === 'string'
748
+ ) {
749
+ rdr.destroy();
750
+ }
751
+ if (
752
+ wtr &&
753
+ options.writeStreamOptions &&
754
+ options.writeStreamOptions.autoClose === false
755
+ ) {
756
+ wtr.destroy();
757
+ }
758
+ return resp;
759
+ });
671
760
  }
672
761
 
673
762
  /**
674
763
  * Append to an existing remote file
675
764
  *
676
765
  * @param {Buffer|stream} input
677
- * @param {String} remotePath,
766
+ * @param {String} remotePath
678
767
  * @param {Object} options
679
768
  * @return {Promise}
680
769
  */
681
770
  append(input, remotePath, options = {}) {
682
- return new Promise((resolve, reject) => {
683
- if (haveConnection(this, 'append', reject)) {
684
- if (typeof input === 'string') {
685
- reject(fmtError('Cannot append one file to another', 'append'));
686
- } else {
687
- this.debugMsg(`append -> remote: ${remotePath} `, options);
688
- addTempListeners(this, 'append', reject);
689
- options.flags = 'a';
690
- let stream = this.sftp.createWriteStream(remotePath, options);
691
- stream.once('error', (err) => {
692
- reject(
693
- fmtError(`${err.message} ${remotePath}`, 'append', err.code)
694
- );
695
- });
696
- stream.once('finish', () => {
697
- resolve(`Appended data to ${remotePath}`);
698
- });
699
- if (input instanceof Buffer) {
700
- stream.end(input);
771
+ return this.exists(remotePath).then((fileType) => {
772
+ if (fileType && fileType === 'd') {
773
+ return Promise.reject(
774
+ fmtError(
775
+ `Bad path: ${remotePath}: cannot append to a directory`,
776
+ 'append',
777
+ errorCode.badPath
778
+ )
779
+ );
780
+ }
781
+ return new Promise((resolve, reject) => {
782
+ if (haveConnection(this, 'append', reject)) {
783
+ if (typeof input === 'string') {
784
+ reject(fmtError('Cannot append one file to another', 'append'));
701
785
  } else {
702
- input.pipe(stream);
786
+ this.debugMsg(`append -> remote: ${remotePath} `, options);
787
+ addTempListeners(this, 'append', reject);
788
+ options.flags = 'a';
789
+ let stream = this.sftp.createWriteStream(remotePath, options);
790
+ stream.on('error', (err) => {
791
+ reject(
792
+ fmtError(`${err.message} ${remotePath}`, 'append', err.code)
793
+ );
794
+ });
795
+ stream.on('finish', () => {
796
+ resolve(`Appended data to ${remotePath}`);
797
+ });
798
+ if (input instanceof Buffer) {
799
+ stream.write(input);
800
+ stream.end();
801
+ } else {
802
+ input.pipe(stream);
803
+ }
703
804
  }
704
805
  }
705
- }
706
- }).finally((rsp) => {
707
- removeTempListeners(this.client);
708
- return rsp;
806
+ }).finally((rsp) => {
807
+ removeTempListeners(this, 'append');
808
+ this._resetEventFlags();
809
+ return rsp;
810
+ });
709
811
  });
710
812
  }
711
813
 
@@ -714,24 +816,40 @@ class SftpClient {
714
816
  *
715
817
  * Make a directory on remote server
716
818
  *
717
- * @param {string} path, remote directory path.
718
- * @param {boolean} recursive, if true, recursively create directories
719
- * @return {Promise}.
819
+ * @param {string} remotePath - remote directory path.
820
+ * @param {boolean} recursive - if true, recursively create directories
821
+ * @return {Promise}
720
822
  */
721
823
  async mkdir(remotePath, recursive = false) {
722
824
  const _mkdir = (p) => {
723
825
  return new Promise((resolve, reject) => {
724
- this.debugMsg(`mkdir -> ${p}`);
725
- addTempListeners(this, 'mkdir', reject);
826
+ this.debugMsg(`_mkdir: create ${p}`);
827
+ addTempListeners(this, '_mkdir', reject);
726
828
  this.sftp.mkdir(p, (err) => {
727
829
  if (err) {
728
- this.debugMsg(`mkdir error ${err.message} code: ${err.code}`);
729
- reject(fmtError(`${err.message} ${p}`, '_mkdir', err.code));
830
+ this.debugMsg(`_mkdir: Error ${err.message} code: ${err.code}`);
831
+ if (err.code === 4) {
832
+ //fix for windows dodgy error messages
833
+ let error = new Error(`Bad path: ${p} permission denied`);
834
+ error.code = errorCode.badPath;
835
+ reject(error);
836
+ } else if (err.code === 2) {
837
+ let error = new Error(
838
+ `Bad path: ${p} parent not a directory or not exist`
839
+ );
840
+ error.code = errorCode.badPath;
841
+ reject(error);
842
+ } else {
843
+ reject(err);
844
+ }
845
+ } else {
846
+ this.debugMsg('_mkdir: directory created');
847
+ resolve(`${p} directory created`);
730
848
  }
731
- resolve(`${p} directory created`);
732
849
  });
733
850
  }).finally((rsp) => {
734
- removeTempListeners(this.client);
851
+ removeTempListeners(this, '_mkdir');
852
+ this._resetEventFlags();
735
853
  return rsp;
736
854
  });
737
855
  };
@@ -740,22 +858,22 @@ class SftpClient {
740
858
  haveConnection(this, 'mkdir');
741
859
  let rPath = await normalizeRemotePath(this, remotePath);
742
860
  if (!recursive) {
743
- return _mkdir(rPath);
861
+ return await _mkdir(rPath);
744
862
  }
745
863
  let dir = parse(rPath).dir;
746
864
  if (dir) {
747
865
  let dirExists = await this.exists(dir);
748
866
  if (!dirExists) {
749
867
  await this.mkdir(dir, true);
868
+ } else if (dirExists !== 'd') {
869
+ let error = new Error(`Bad path: ${dir} not a directory`);
870
+ error.code = errorCode.badPath;
871
+ throw error;
750
872
  }
751
873
  }
752
- return _mkdir(rPath);
874
+ return await _mkdir(rPath);
753
875
  } catch (err) {
754
- if (err.custom) {
755
- throw err;
756
- } else {
757
- throw fmtError(`${err.message} ${remotePath}`, 'mkdir', err.code);
758
- }
876
+ throw fmtError(`${err.message}`, 'mkdir', err.code);
759
877
  }
760
878
  }
761
879
 
@@ -764,10 +882,10 @@ class SftpClient {
764
882
  *
765
883
  * Remove directory on remote server
766
884
  *
767
- * @param {string} path, path to directory to be removed
768
- * @param {boolean} recursive, if true, remove directories/files in target
885
+ * @param {string} remotePath - path to directory to be removed
886
+ * @param {boolean} recursive - if true, remove directories/files in target
769
887
  * directory
770
- * @return {Promise}..
888
+ * @return {Promise}
771
889
  */
772
890
  async rmdir(remotePath, recursive = false) {
773
891
  const _rmdir = (p) => {
@@ -782,7 +900,7 @@ class SftpClient {
782
900
  resolve('Successfully removed directory');
783
901
  });
784
902
  }).finally((rsp) => {
785
- removeTempListeners(this.client);
903
+ removeTempListeners(this, 'rmdir');
786
904
  return rsp;
787
905
  });
788
906
  };
@@ -808,11 +926,8 @@ class SftpClient {
808
926
  }
809
927
  return _rmdir(absPath);
810
928
  } catch (err) {
811
- if (err.custom) {
812
- throw err;
813
- } else {
814
- throw fmtError(err, 'rmdir', err.code);
815
- }
929
+ this._resetEventFlags();
930
+ throw err.custom ? err : fmtError(err, 'rmdir', err.code);
816
931
  }
817
932
  }
818
933
 
@@ -821,10 +936,10 @@ class SftpClient {
821
936
  *
822
937
  * Delete a file on the remote SFTP server
823
938
  *
824
- * @param {string} path - path to the file to delete
939
+ * @param {string} remotePath - path to the file to delete
825
940
  * @param {boolean} notFoundOK - if true, ignore errors for missing target.
826
941
  * Default is false.
827
- * @return {Promise} with string 'Successfully deleeted file' once resolved
942
+ * @return {Promise} with string 'Successfully deleted file' once resolved
828
943
  *
829
944
  */
830
945
  delete(remotePath, notFoundOK = false) {
@@ -848,7 +963,8 @@ class SftpClient {
848
963
  });
849
964
  }
850
965
  }).finally((rsp) => {
851
- removeTempListeners(this.client);
966
+ removeTempListeners(this, 'delete');
967
+ this._resetEventFlags();
852
968
  return rsp;
853
969
  });
854
970
  }
@@ -884,7 +1000,8 @@ class SftpClient {
884
1000
  });
885
1001
  }
886
1002
  }).finally((rsp) => {
887
- removeTempListeners(this.client);
1003
+ removeTempListeners(this, 'rename');
1004
+ this._resetEventFlags();
888
1005
  return rsp;
889
1006
  });
890
1007
  }
@@ -921,7 +1038,8 @@ class SftpClient {
921
1038
  });
922
1039
  }
923
1040
  }).finally((rsp) => {
924
- removeTempListeners(this.client);
1041
+ removeTempListeners(this, 'posixRename');
1042
+ this._resetEventFlags();
925
1043
  return rsp;
926
1044
  });
927
1045
  }
@@ -932,9 +1050,9 @@ class SftpClient {
932
1050
  * Change the mode of a remote file on the SFTP repository
933
1051
  *
934
1052
  * @param {string} remotePath - path to the remote target object.
935
- * @param {Octal} mode - the new mode to set
1053
+ * @param {number | string} mode - the new octal mode to set
936
1054
  *
937
- * @return {Promise}.
1055
+ * @return {Promise}
938
1056
  */
939
1057
  chmod(remotePath, mode) {
940
1058
  return new Promise((resolve, reject) => {
@@ -947,7 +1065,8 @@ class SftpClient {
947
1065
  resolve('Successfully change file mode');
948
1066
  });
949
1067
  }).finally((rsp) => {
950
- removeTempListeners(this.client);
1068
+ removeTempListeners(this, 'chmod');
1069
+ this._resetEventFlags();
951
1070
  return rsp;
952
1071
  });
953
1072
  }
@@ -960,7 +1079,7 @@ class SftpClient {
960
1079
  * server.
961
1080
  * @param {String} srcDir - local source directory
962
1081
  * @param {String} dstDir - remote destination directory
963
- * @param {regex} filter - (Optional) a regular expression used to select
1082
+ * @param {RegExp} filter - (Optional) a regular expression used to select
964
1083
  * files and directories to upload
965
1084
  * @returns {String}
966
1085
  * @throws {Error}
@@ -968,6 +1087,14 @@ class SftpClient {
968
1087
  async uploadDir(srcDir, dstDir, filter = /.*/) {
969
1088
  try {
970
1089
  this.debugMsg(`uploadDir -> ${srcDir} ${dstDir}`);
1090
+ const srcType = localExists(srcDir);
1091
+ if (srcType !== 'd') {
1092
+ throw fmtError(
1093
+ `Bad path: ${srcDir}: not a directory`,
1094
+ 'uploadDir',
1095
+ errorCode.badPath
1096
+ );
1097
+ }
971
1098
  haveConnection(this, 'uploadDir');
972
1099
  let dstStatus = await this.exists(dstDir);
973
1100
  if (dstStatus && dstStatus !== 'd') {
@@ -978,7 +1105,7 @@ class SftpClient {
978
1105
  }
979
1106
  let dirEntries = fs.readdirSync(srcDir, {
980
1107
  encoding: 'utf8',
981
- withFileTypes: true
1108
+ withFileTypes: true,
982
1109
  });
983
1110
  dirEntries = dirEntries.filter((item) => filter.test(item.name));
984
1111
  for (let e of dirEntries) {
@@ -990,7 +1117,7 @@ class SftpClient {
990
1117
  let src = join(srcDir, e.name);
991
1118
  let dst = dstDir + this.remotePathSep + e.name;
992
1119
  await this.fastPut(src, dst);
993
- this.client.emit('upload', {source: src, destination: dst});
1120
+ this.client.emit('upload', { source: src, destination: dst });
994
1121
  } else {
995
1122
  this.debugMsg(
996
1123
  `uploadDir: File ignored: ${e.name} not a regular file`
@@ -999,11 +1126,8 @@ class SftpClient {
999
1126
  }
1000
1127
  return `${srcDir} uploaded to ${dstDir}`;
1001
1128
  } catch (err) {
1002
- if (err.custom) {
1003
- throw err;
1004
- } else {
1005
- throw fmtError(err, 'uploadDir');
1006
- }
1129
+ this._resetEventFlags();
1130
+ throw err.custom ? err : fmtError(err, 'uploadDir');
1007
1131
  }
1008
1132
  }
1009
1133
 
@@ -1015,9 +1139,9 @@ class SftpClient {
1015
1139
  * file system.
1016
1140
  * @param {String} srcDir - remote source directory
1017
1141
  * @param {String} dstDir - local destination directory
1018
- * @param {regex} filter - (Optional) a regular expression used to select
1142
+ * @param {RegExp} filter - (Optional) a regular expression used to select
1019
1143
  * files and directories to upload
1020
- * @returns {String}
1144
+ * @returns {Promise}
1021
1145
  * @throws {Error}
1022
1146
  */
1023
1147
  async downloadDir(srcDir, dstDir, filter = /.*/) {
@@ -1025,12 +1149,21 @@ class SftpClient {
1025
1149
  this.debugMsg(`downloadDir -> ${srcDir} ${dstDir}`);
1026
1150
  haveConnection(this, 'downloadDir');
1027
1151
  let fileList = await this.list(srcDir, filter);
1028
- let dstStatus = await localExists(dstDir);
1029
- if (dstStatus && dstStatus !== 'd') {
1030
- throw fmtError(`Bad path ${dstDir}`, 'downloadDir', errorCode.badPath);
1031
- }
1032
- if (!dstStatus) {
1033
- fs.mkdirSync(dstDir, {recursive: true});
1152
+ const localCheck = haveLocalCreate(dstDir);
1153
+ if (!localCheck.status && localCheck.details === 'permission denied') {
1154
+ throw fmtError(
1155
+ `Bad path: ${dstDir}: ${localCheck.details}`,
1156
+ 'downloadDir',
1157
+ localCheck.code
1158
+ );
1159
+ } else if (localCheck.status && !localCheck.type) {
1160
+ fs.mkdirSync(dstDir, { recursive: true });
1161
+ } else if (localCheck.status && localCheck.type !== 'd') {
1162
+ throw fmtError(
1163
+ `Bad path: ${dstDir}: not a directory`,
1164
+ 'downloadDir',
1165
+ errorCode.badPath
1166
+ );
1034
1167
  }
1035
1168
  for (let f of fileList) {
1036
1169
  if (f.type === 'd') {
@@ -1041,7 +1174,7 @@ class SftpClient {
1041
1174
  let src = srcDir + this.remotePathSep + f.name;
1042
1175
  let dst = join(dstDir, f.name);
1043
1176
  await this.fastGet(src, dst);
1044
- this.client.emit('download', {source: src, destination: dst});
1177
+ this.client.emit('download', { source: src, destination: dst });
1045
1178
  } else {
1046
1179
  this.debugMsg(
1047
1180
  `downloadDir: File ignored: ${f.name} not regular file`
@@ -1050,11 +1183,8 @@ class SftpClient {
1050
1183
  }
1051
1184
  return `${srcDir} downloaded to ${dstDir}`;
1052
1185
  } catch (err) {
1053
- if (err.custom) {
1054
- throw err;
1055
- } else {
1056
- throw fmtError(err, 'downloadDir', err.code);
1057
- }
1186
+ this._resetEventFlags();
1187
+ throw err.custom ? err : fmtError(err, 'downloadDir', err.code);
1058
1188
  }
1059
1189
  }
1060
1190
 
@@ -1071,17 +1201,21 @@ class SftpClient {
1071
1201
  addTempListeners(this, 'end', reject);
1072
1202
  endCloseHandler = () => {
1073
1203
  this.sftp = undefined;
1204
+ this.debugMsg('end: Connection closed');
1074
1205
  resolve(true);
1075
1206
  };
1076
1207
  this.on('close', endCloseHandler);
1077
1208
  if (haveConnection(this, 'end', reject)) {
1078
- this.debugMsg('Have connection - calling end()');
1209
+ this.debugMsg('end: Have connection - calling end()');
1079
1210
  this.client.end();
1080
1211
  }
1081
- }).finally(() => {
1082
- removeTempListeners(this.client);
1212
+ }).finally((resp) => {
1213
+ this.debugMsg('end: finally clause fired');
1214
+ removeTempListeners(this, 'end');
1083
1215
  this.removeListener('close', endCloseHandler);
1084
- return true;
1216
+ this.endCalled = false;
1217
+ this._resetEventFlags();
1218
+ return resp;
1085
1219
  });
1086
1220
  }
1087
1221
  }