ssh2-sftp-client 7.2.3 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,7 +1,3 @@
1
- /**
2
- * ssh2 sftp client for node
3
- */
4
-
5
1
  'use strict';
6
2
 
7
3
  const { Client } = require('ssh2');
@@ -10,7 +6,6 @@ const concat = require('concat-stream');
10
6
  const promiseRetry = require('promise-retry');
11
7
  const { join, parse } = require('path');
12
8
  const {
13
- fmtError,
14
9
  addTempListeners,
15
10
  removeTempListeners,
16
11
  haveConnection,
@@ -18,12 +13,12 @@ const {
18
13
  localExists,
19
14
  haveLocalAccess,
20
15
  haveLocalCreate,
21
- sleep,
22
16
  } = require('./utils');
23
17
  const { errorCode } = require('./constants');
24
18
 
25
19
  class SftpClient {
26
20
  constructor(clientName) {
21
+ this.version = '9.0.0';
27
22
  this.client = new Client();
28
23
  this.sftp = undefined;
29
24
  this.clientName = clientName ? clientName : 'sftp';
@@ -34,7 +29,6 @@ class SftpClient {
34
29
  this.remotePathSep = '/';
35
30
  this.remotePlatform = 'unix';
36
31
  this.debug = undefined;
37
- this.tempListeners = {};
38
32
 
39
33
  this.client.on('close', () => {
40
34
  if (this.endCalled || this.closeHandled) {
@@ -66,7 +60,6 @@ class SftpClient {
66
60
  console.log(
67
61
  `ssh2-sftp-client: Unexpected error: ${err.message}. Error code: ${err.code}`
68
62
  );
69
- //throw fmtError(err, 'Global');
70
63
  }
71
64
  });
72
65
  }
@@ -83,6 +76,51 @@ class SftpClient {
83
76
  }
84
77
  }
85
78
 
79
+ fmtError(err, name = 'sftp', eCode, retryCount) {
80
+ let msg = '';
81
+ let code = '';
82
+ const retry = retryCount
83
+ ? ` after ${retryCount} ${retryCount > 1 ? 'attempts' : 'attempt'}`
84
+ : '';
85
+
86
+ if (err === undefined) {
87
+ msg = `${name}: Undefined error - probably a bug!`;
88
+ code = errorCode.generic;
89
+ } else if (typeof err === 'string') {
90
+ msg = `${name}: ${err}${retry}`;
91
+ code = eCode ? eCode : errorCode.generic;
92
+ } else if (err.custom) {
93
+ msg = `${name}->${err.message}${retry}`;
94
+ code = err.code;
95
+ } else {
96
+ switch (err.code) {
97
+ case 'ENOTFOUND':
98
+ msg =
99
+ `${name}: ${err.level} error. ` +
100
+ `Address lookup failed for host ${err.hostname}${retry}`;
101
+ break;
102
+ case 'ECONNREFUSED':
103
+ msg =
104
+ `${name}: ${err.level} error. Remote host at ` +
105
+ `${err.address} refused connection${retry}`;
106
+ break;
107
+ case 'ECONNRESET':
108
+ msg =
109
+ `${name}: Remote host has reset the connection: ` +
110
+ `${err.message}${retry}`;
111
+ break;
112
+ default:
113
+ msg = `${name}: ${err.message}${retry}`;
114
+ }
115
+ code = err.code ? err.code : errorCode.generic;
116
+ }
117
+ let newError = new Error(msg);
118
+ newError.code = code;
119
+ newError.custom = true;
120
+ this.debugMsg(`${newError.message} (${newError.code})`);
121
+ return newError;
122
+ }
123
+
86
124
  /**
87
125
  * Add a listner to the client object. This is rarely necessary and can be
88
126
  * the source of errors. It is the client's responsibility to remove the
@@ -93,12 +131,10 @@ class SftpClient {
93
131
  * @param {function} callback - function called when event triggers
94
132
  */
95
133
  on(eventType, callback) {
96
- this.debugMsg(`Adding listener to ${eventType} event`);
97
134
  this.client.prependListener(eventType, callback);
98
135
  }
99
136
 
100
137
  removeListener(eventType, callback) {
101
- this.debugMsg(`Removing listener from ${eventType} event`);
102
138
  this.client.removeListener(eventType, callback);
103
139
  }
104
140
 
@@ -119,10 +155,9 @@ class SftpClient {
119
155
  *
120
156
  */
121
157
  getConnection(config) {
122
- let doReady;
158
+ let doReady, listeners;
123
159
  return new Promise((resolve, reject) => {
124
- addTempListeners(this, 'getConnection', reject);
125
- this.debugMsg('getConnection: created promise');
160
+ listeners = addTempListeners(this, 'getConnection', reject);
126
161
  doReady = () => {
127
162
  this.debugMsg(
128
163
  'getConnection ready listener: got connection - promise resolved'
@@ -131,34 +166,25 @@ class SftpClient {
131
166
  };
132
167
  this.on('ready', doReady);
133
168
  this.client.connect(config);
134
- }).finally(async () => {
135
- this.debugMsg('getConnection: finally clause fired');
136
- await sleep(500);
169
+ }).finally(() => {
137
170
  this.removeListener('ready', doReady);
138
- removeTempListeners(this, 'getConnection');
171
+ removeTempListeners(this, listeners, 'getConnection');
139
172
  this._resetEventFlags();
140
173
  });
141
174
  }
142
175
 
143
176
  getSftpChannel() {
144
177
  return new Promise((resolve, reject) => {
145
- addTempListeners(this, 'getSftpChannel', reject);
146
- this.debugMsg('getSftpChannel: created promise');
147
178
  this.client.sftp((err, sftp) => {
148
179
  if (err) {
149
- this.debugMsg(`getSftpChannel: SFTP Channel Error: ${err.message}`);
150
180
  this.client.end();
151
- reject(fmtError(err, 'getSftpChannel', err.code));
181
+ reject(this.fmtError(err, 'getSftpChannel', err.code));
152
182
  } else {
153
183
  this.debugMsg('getSftpChannel: SFTP channel established');
154
184
  this.sftp = sftp;
155
185
  resolve(sftp);
156
186
  }
157
187
  });
158
- }).finally(() => {
159
- this.debugMsg('getSftpChannel: finally clause fired');
160
- removeTempListeners(this, 'getSftpChannel');
161
- this._resetEventFlags();
162
188
  });
163
189
  }
164
190
 
@@ -175,47 +201,68 @@ class SftpClient {
175
201
  *
176
202
  */
177
203
  async connect(config) {
204
+ let listeners;
205
+
178
206
  try {
207
+ listeners = addTempListeners(this, 'connect');
179
208
  if (config.debug) {
180
209
  this.debug = config.debug;
181
210
  this.debugMsg('connect: Debugging turned on');
211
+ this.debugMsg(
212
+ `ssh2-sftp-client Version: ${this.version} `,
213
+ process.versions
214
+ );
182
215
  }
183
216
  if (this.sftp) {
184
- this.debugMsg('connect: Already connected - reject');
185
- throw fmtError(
217
+ throw this.fmtError(
186
218
  'An existing SFTP connection is already defined',
187
219
  'connect',
188
220
  errorCode.connect
189
221
  );
190
222
  }
191
- await promiseRetry(
192
- (retry, attempt) => {
223
+ let retryOpts = {
224
+ retries: config.retries || 1,
225
+ factor: config.factor || 2,
226
+ minTimeout: config.retry_minTimeout || 25000,
227
+ };
228
+ await promiseRetry(retryOpts, async (retry, attempt) => {
229
+ try {
193
230
  this.debugMsg(`connect: Connect attempt ${attempt}`);
194
- return this.getConnection(config).catch((err) => {
195
- this.debugMsg(
196
- `getConnection retry catch: ${err.message} Code: ${err.code}`
197
- );
198
- switch (err.code) {
199
- case 'ENOTFOUND':
200
- case 'ECONNREFUSED':
201
- case 'ERR_SOCKET_BAD_PORT':
202
- throw err;
203
- default:
204
- retry(err);
231
+ await this.getConnection(config);
232
+ } catch (err) {
233
+ switch (err.code) {
234
+ case 'ENOTFOUND':
235
+ case 'ECONNREFUSED':
236
+ case 'ERR_SOCKET_BAD_PORT':
237
+ throw err;
238
+ case undefined: {
239
+ if (
240
+ err.message.endsWith(
241
+ 'All configured authentication methods failed'
242
+ )
243
+ ) {
244
+ throw this.fmtError(
245
+ err.message,
246
+ 'getConnection',
247
+ errorCode.badAuth
248
+ );
249
+ }
250
+ retry(err);
251
+ break;
205
252
  }
206
- });
207
- },
208
- {
209
- retries: config.retries || 1,
210
- factor: config.retry_factor || 2,
211
- minTimeout: config.retry_minTimeout || 1000,
253
+ default:
254
+ retry(err);
255
+ }
212
256
  }
213
- );
214
- return this.getSftpChannel();
257
+ });
258
+ let sftp = await this.getSftpChannel();
259
+ return sftp;
215
260
  } catch (err) {
216
- this.debugMsg(`connect: Error ${err.message}`);
261
+ this.end();
262
+ throw err.custom ? err : this.fmtError(err, 'connect');
263
+ } finally {
264
+ removeTempListeners(this, listeners, 'connect');
217
265
  this._resetEventFlags();
218
- throw fmtError(err, 'connect');
219
266
  }
220
267
  }
221
268
 
@@ -230,32 +277,42 @@ class SftpClient {
230
277
  * @param {String} remotePath - remote path, may be relative
231
278
  * @returns {Promise<String>} - remote absolute path or ''
232
279
  */
233
- realPath(remotePath) {
280
+ _realPath(rPath) {
234
281
  return new Promise((resolve, reject) => {
235
- this.debugMsg(`realPath -> ${remotePath}`);
236
- addTempListeners(this, 'realPath', reject);
237
- if (haveConnection(this, 'realPath', reject)) {
238
- this.sftp.realpath(remotePath, (err, absPath) => {
239
- if (err) {
240
- this.debugMsg(`realPath Error: ${err.message} Code: ${err.code}`);
241
- if (err.code === 2) {
242
- resolve('');
243
- } else {
244
- reject(
245
- fmtError(`${err.message} ${remotePath}`, 'realPath', err.code)
246
- );
247
- }
282
+ this.debugMsg(`_realPath -> ${rPath}`);
283
+ this.sftp.realpath(rPath, (err, absPath) => {
284
+ if (err) {
285
+ if (err.code === 2) {
286
+ this.debugMsg('_realPath <- ""');
287
+ resolve('');
288
+ } else {
289
+ reject(
290
+ this.fmtError(`${err.message} ${rPath}`, 'realPath', err.code)
291
+ );
248
292
  }
249
- this.debugMsg(`realPath <- ${absPath}`);
250
- resolve(absPath);
251
- });
252
- }
253
- }).finally(() => {
254
- removeTempListeners(this, 'realPath');
255
- this._resetEventFlags();
293
+ }
294
+ this.debugMsg(`_realPath <- ${absPath}`);
295
+ resolve(absPath);
296
+ });
256
297
  });
257
298
  }
258
299
 
300
+ async realPath(remotePath) {
301
+ let listeners;
302
+ try {
303
+ listeners = addTempListeners(this, 'realPath');
304
+ haveConnection(this, 'realPath');
305
+ return await this._realPath(remotePath);
306
+ } catch (e) {
307
+ throw e.custom
308
+ ? e
309
+ : this.fmtError(`${e.message} ${remotePath}`, 'realPath', e.code);
310
+ } finally {
311
+ removeTempListeners(this, listeners, 'realPath');
312
+ this._resetEventFlags();
313
+ }
314
+ }
315
+
259
316
  /**
260
317
  * @async
261
318
  *
@@ -273,59 +330,57 @@ class SftpClient {
273
330
  * @param {String} remotePath - a string containing the path to a file
274
331
  * @return {Promise<Object>} stats - attributes info
275
332
  */
276
- async stat(remotePath) {
277
- const _stat = (aPath) => {
278
- return new Promise((resolve, reject) => {
279
- this.debugMsg(`_stat: ${aPath}`);
280
- addTempListeners(this, '_stat', reject);
281
- this.sftp.stat(aPath, (err, stats) => {
282
- if (err) {
283
- this.debugMsg(`_stat: Error ${err.message} code: ${err.code}`);
284
- if (err.code === 2 || err.code === 4) {
285
- reject(
286
- fmtError(
287
- `No such file: ${remotePath}`,
288
- '_stat',
289
- errorCode.notexist
290
- )
291
- );
292
- } else {
293
- reject(
294
- fmtError(`${err.message} ${remotePath}`, '_stat', err.code)
295
- );
296
- }
333
+ _stat(aPath) {
334
+ return new Promise((resolve, reject) => {
335
+ this.debugMsg(`_stat: ${aPath}`);
336
+ this.sftp.stat(aPath, (err, stats) => {
337
+ if (err) {
338
+ if (err.code === 2 || err.code === 4) {
339
+ reject(
340
+ this.fmtError(
341
+ `No such file: ${aPath}`,
342
+ '_stat',
343
+ errorCode.notexist
344
+ )
345
+ );
297
346
  } else {
298
- let result = {
299
- mode: stats.mode,
300
- uid: stats.uid,
301
- gid: stats.gid,
302
- size: stats.size,
303
- accessTime: stats.atime * 1000,
304
- modifyTime: stats.mtime * 1000,
305
- isDirectory: stats.isDirectory(),
306
- isFile: stats.isFile(),
307
- isBlockDevice: stats.isBlockDevice(),
308
- isCharacterDevice: stats.isCharacterDevice(),
309
- isSymbolicLink: stats.isSymbolicLink(),
310
- isFIFO: stats.isFIFO(),
311
- isSocket: stats.isSocket(),
312
- };
313
- this.debugMsg('_stat: stats <- ', result);
314
- resolve(result);
347
+ reject(this.fmtError(`${err.message} ${aPath}`, '_stat', err.code));
315
348
  }
316
- });
317
- }).finally(() => {
318
- removeTempListeners(this, '_stat');
349
+ } else {
350
+ const result = {
351
+ mode: stats.mode,
352
+ uid: stats.uid,
353
+ gid: stats.gid,
354
+ size: stats.size,
355
+ accessTime: stats.atime * 1000,
356
+ modifyTime: stats.mtime * 1000,
357
+ isDirectory: stats.isDirectory(),
358
+ isFile: stats.isFile(),
359
+ isBlockDevice: stats.isBlockDevice(),
360
+ isCharacterDevice: stats.isCharacterDevice(),
361
+ isSymbolicLink: stats.isSymbolicLink(),
362
+ isFIFO: stats.isFIFO(),
363
+ isSocket: stats.isSocket(),
364
+ };
365
+ this.debugMsg('_stat: stats <- ', result);
366
+ resolve(result);
367
+ }
319
368
  });
320
- };
369
+ });
370
+ }
321
371
 
372
+ async stat(remotePath) {
373
+ let listeners;
322
374
  try {
375
+ listeners = addTempListeners(this, 'stat');
323
376
  haveConnection(this, 'stat');
324
- let absPath = await normalizeRemotePath(this, remotePath);
325
- return _stat(absPath);
377
+ const absPath = await normalizeRemotePath(this, remotePath);
378
+ return await this._stat(absPath);
326
379
  } catch (err) {
380
+ throw err.custom ? err : this.fmtError(err, 'stat', err.code);
381
+ } finally {
382
+ removeTempListeners(this, listeners, 'stat');
327
383
  this._resetEventFlags();
328
- throw err.custom ? err : fmtError(err, 'stat', err.code);
329
384
  }
330
385
  }
331
386
 
@@ -340,48 +395,49 @@ class SftpClient {
340
395
  * @return {Promise<Boolean|String>} returns false if object does not exist. Returns type of
341
396
  * object if it does
342
397
  */
343
- async exists(remotePath) {
398
+ async _exists(rPath) {
344
399
  try {
345
- if (haveConnection(this, 'exists')) {
346
- if (remotePath === '.') {
347
- this.debugMsg('exists: . = d');
348
- return 'd';
349
- }
350
- let absPath = await normalizeRemotePath(this, remotePath);
351
- try {
352
- this.debugMsg(`exists: ${remotePath} -> ${absPath}`);
353
- let info = await this.stat(absPath);
354
- this.debugMsg('exists: <- ', info);
355
- if (info.isDirectory) {
356
- this.debugMsg(`exists: ${remotePath} = d`);
357
- return 'd';
358
- }
359
- if (info.isSymbolicLink) {
360
- this.debugMsg(`exists: ${remotePath} = l`);
361
- return 'l';
362
- }
363
- if (info.isFile) {
364
- this.debugMsg(`exists: ${remotePath} = -`);
365
- return '-';
366
- }
367
- this.debugMsg(`exists: ${remotePath} = false`);
368
- return false;
369
- } catch (err) {
370
- if (err.code === errorCode.notexist) {
371
- this.debugMsg(
372
- `exists: ${remotePath} = false errorCode = ${err.code}`
373
- );
374
- return false;
375
- }
376
- this.debugMsg(`exists: throw error ${err.message} ${err.code}`);
377
- throw err;
378
- }
400
+ const absPath = await normalizeRemotePath(this, rPath);
401
+ this.debugMsg(`exists: ${rPath} -> ${absPath}`);
402
+ const info = await this._stat(absPath);
403
+ this.debugMsg('exists: <- ', info);
404
+ if (info.isDirectory) {
405
+ this.debugMsg(`exists: ${rPath} = d`);
406
+ return 'd';
407
+ }
408
+ if (info.isSymbolicLink) {
409
+ this.debugMsg(`exists: ${rPath} = l`);
410
+ return 'l';
379
411
  }
380
- this.debugMsg(`exists: default ${remotePath} = false`);
412
+ if (info.isFile) {
413
+ this.debugMsg(`exists: ${rPath} = -`);
414
+ return '-';
415
+ }
416
+ this.debugMsg(`exists: ${rPath} = false`);
381
417
  return false;
382
418
  } catch (err) {
419
+ if (err.code === errorCode.notexist) {
420
+ this.debugMsg(`exists: ${rPath} = false errorCode = ${err.code}`);
421
+ return false;
422
+ }
423
+ throw err.custom ? err : this.fmtError(err.message, 'exists', err.code);
424
+ }
425
+ }
426
+
427
+ async exists(remotePath) {
428
+ let listeners;
429
+ try {
430
+ listeners = addTempListeners(this, 'exists');
431
+ haveConnection(this, 'exists');
432
+ if (remotePath === '.') {
433
+ return 'd';
434
+ }
435
+ return await this._exists(remotePath);
436
+ } catch (err) {
437
+ throw err.custom ? err : this.fmtError(err, 'exists', err.code);
438
+ } finally {
439
+ removeTempListeners(this, listeners, 'exists');
383
440
  this._resetEventFlags();
384
- throw err.custom ? err : fmtError(err, 'exists', err.code);
385
441
  }
386
442
  }
387
443
 
@@ -395,60 +451,61 @@ class SftpClient {
395
451
  * accessTime, rights {user, group other}, owner and group.
396
452
  *
397
453
  * @param {String} remotePath - path to remote directory
398
- * @param {RegExp} pattern - regular expression to match filenames
454
+ * @param {function} filter - a filter function used to select return entries
399
455
  * @returns {Promise<Array>} array of file description objects
400
456
  */
401
- list(remotePath, pattern = /.*/) {
457
+ _list(remotePath, filter) {
402
458
  return new Promise((resolve, reject) => {
403
- if (haveConnection(this, 'list', reject)) {
404
- const reg = /-/gi;
405
- this.debugMsg(`list: ${remotePath} filter: ${pattern}`);
406
- addTempListeners(this, 'list', reject);
407
- this.sftp.readdir(remotePath, (err, fileList) => {
408
- if (err) {
409
- this.debugMsg(`list: Error ${err.message} code: ${err.code}`);
410
- reject(fmtError(`${err.message} ${remotePath}`, 'list', err.code));
459
+ this.sftp.readdir(remotePath, (err, fileList) => {
460
+ if (err) {
461
+ reject(
462
+ this.fmtError(`${err.message} ${remotePath}`, 'list', err.code)
463
+ );
464
+ } else {
465
+ const reg = /-/gi;
466
+ const newList = fileList.map((item) => {
467
+ return {
468
+ type: item.longname.slice(0, 1),
469
+ name: item.filename,
470
+ size: item.attrs.size,
471
+ modifyTime: item.attrs.mtime * 1000,
472
+ accessTime: item.attrs.atime * 1000,
473
+ rights: {
474
+ user: item.longname.slice(1, 4).replace(reg, ''),
475
+ group: item.longname.slice(4, 7).replace(reg, ''),
476
+ other: item.longname.slice(7, 10).replace(reg, ''),
477
+ },
478
+ owner: item.attrs.uid,
479
+ group: item.attrs.gid,
480
+ longname: item.longname,
481
+ };
482
+ });
483
+ if (filter) {
484
+ resolve(newList.filter((item) => filter(item)));
411
485
  } else {
412
- let newList = [];
413
- // reset file info
414
- if (fileList) {
415
- newList = fileList.map((item) => {
416
- return {
417
- type: item.longname.slice(0, 1),
418
- name: item.filename,
419
- size: item.attrs.size,
420
- modifyTime: item.attrs.mtime * 1000,
421
- accessTime: item.attrs.atime * 1000,
422
- rights: {
423
- user: item.longname.slice(1, 4).replace(reg, ''),
424
- group: item.longname.slice(4, 7).replace(reg, ''),
425
- other: item.longname.slice(7, 10).replace(reg, ''),
426
- },
427
- owner: item.attrs.uid,
428
- group: item.attrs.gid,
429
- };
430
- });
431
- }
432
- // provide some compatibility for auxList
433
- let regex;
434
- if (pattern instanceof RegExp) {
435
- regex = pattern;
436
- } else {
437
- let newPattern = pattern.replace(/\*([^*])*?/gi, '.*');
438
- regex = new RegExp(newPattern);
439
- }
440
- let filteredList = newList.filter((item) => regex.test(item.name));
441
- this.debugMsg('list: result: ', filteredList);
442
- resolve(filteredList);
486
+ resolve(newList);
443
487
  }
444
- });
445
- }
446
- }).finally(() => {
447
- removeTempListeners(this, 'list');
448
- this._resetEventFlags();
488
+ }
489
+ });
449
490
  });
450
491
  }
451
492
 
493
+ async list(remotePath, filter) {
494
+ let listeners;
495
+ try {
496
+ listeners = addTempListeners(this, 'list');
497
+ haveConnection(this, 'list');
498
+ return await this._list(remotePath, filter);
499
+ } catch (e) {
500
+ throw e.custom
501
+ ? e
502
+ : this.fmtError(`${e.message} ${remotePath}`, 'list', e.code);
503
+ } finally {
504
+ removeTempListeners(this, listeners, 'list');
505
+ this._resetEventFlags();
506
+ }
507
+ }
508
+
452
509
  /**
453
510
  * get file
454
511
  *
@@ -462,113 +519,87 @@ class SftpClient {
462
519
  * @param {Object} options - options object with supported properties of readStreamOptions,
463
520
  * writeStreamOptions and pipeOptions.
464
521
  *
522
+ * *Important Note*: The ability to set ''autoClose' on read/write streams and 'end' on pipe() calls
523
+ * is no longer supported. New methods 'createReadStream()' and 'createWriteStream()' have been
524
+ * added to support low-level access to stream objects.
525
+ *
465
526
  * @return {Promise<String|Stream|Buffer>}
466
527
  */
467
- get(
468
- remotePath,
469
- dst,
470
- options = { readStreamOptions: {}, writeStreamOptions: {}, pipeOptions: {} }
471
- ) {
528
+ _get(rPath, dst, opts) {
472
529
  let rdr, wtr;
473
-
474
530
  return new Promise((resolve, reject) => {
475
- if (haveConnection(this, 'get', reject)) {
476
- this.debugMsg(`get -> ${remotePath} `, options);
477
- addTempListeners(this, 'get', reject);
478
- rdr = this.sftp.createReadStream(
479
- remotePath,
480
- options.readStreamOptions ? options.readStreamOptions : {}
481
- );
482
- rdr.once('error', (err) => {
483
- reject(fmtError(`${err.message} ${remotePath}`, 'get', err.code));
531
+ opts = {
532
+ ...opts,
533
+ readStreamOptions: { autoClose: true },
534
+ writeStreamOptions: { autoClose: true },
535
+ pipeOptions: { end: true },
536
+ };
537
+ rdr = this.sftp.createReadStream(rPath, opts.readStreamOptions);
538
+ rdr.once('error', (err) => {
539
+ reject(this.fmtError(`${err.message} ${rPath}`, '_get', err.code));
540
+ });
541
+ if (dst === undefined) {
542
+ // no dst specified, return buffer of data
543
+ this.debugMsg('get returning buffer of data');
544
+ wtr = concat((buff) => {
545
+ resolve(buff);
484
546
  });
485
- if (dst === undefined) {
486
- // no dst specified, return buffer of data
487
- this.debugMsg('get returning buffer of data');
488
- wtr = concat((buff) => {
489
- //rdr.removeAllListeners('error');
490
- resolve(buff);
491
- });
547
+ } else if (typeof dst === 'string') {
548
+ // dst local file path
549
+ this.debugMsg('get returning local file');
550
+ const localCheck = haveLocalCreate(dst);
551
+ if (!localCheck.status) {
552
+ reject(
553
+ this.fmtError(
554
+ `Bad path: ${dst}: ${localCheck.details}`,
555
+ 'get',
556
+ localCheck.code
557
+ )
558
+ );
559
+ return;
492
560
  } else {
493
- if (typeof dst === 'string') {
494
- // dst local file path
495
- this.debugMsg('get returning local file');
496
- const localCheck = haveLocalCreate(dst);
497
- if (!localCheck.status) {
498
- return reject(
499
- fmtError(
500
- `Bad path: ${dst}: ${localCheck.details}`,
501
- 'get',
502
- localCheck.code
503
- )
504
- );
505
- }
506
- wtr = fs.createWriteStream(
507
- dst,
508
- options.writeStreamOptions ? options.writeStreamOptions : {}
509
- );
510
- } else {
511
- this.debugMsg('get returning data into supplied stream');
512
- wtr = dst;
513
- }
514
- wtr.once('error', (err) => {
515
- reject(
516
- fmtError(
517
- `${err.message} ${typeof dst === 'string' ? dst : ''}`,
518
- 'get',
519
- err.code
520
- )
521
- );
522
- });
523
- if (
524
- Object.hasOwnProperty.call(options, 'pipeOptions') &&
525
- Object.hasOwnProperty.call(options.pipeOptions, 'end') &&
526
- !options.pipeOptions.end
527
- ) {
528
- rdr.once('end', () => {
529
- this.debugMsg('get resolved on reader end event');
530
- if (typeof dst === 'string') {
531
- resolve(dst);
532
- } else {
533
- resolve(wtr);
534
- }
535
- });
536
- } else {
537
- wtr.once('finish', () => {
538
- this.debugMsg('get resolved on writer finish event');
539
- if (typeof dst === 'string') {
540
- resolve(dst);
541
- } else {
542
- resolve(wtr);
543
- }
544
- });
545
- }
561
+ wtr = fs.createWriteStream(dst, opts.writeStreamOptions);
546
562
  }
547
- rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
548
- }
549
- }).finally(() => {
550
- removeTempListeners(this, 'get');
551
- this._resetEventFlags();
552
- if (
553
- rdr &&
554
- Object.hasOwnProperty.call(options, 'readStreamOptions') &&
555
- Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
556
- options.readStreamOptions.autoClose === false
557
- ) {
558
- rdr.destroy();
559
- }
560
- if (
561
- wtr &&
562
- Object.hasOwnProperty.call(options, 'writeStreamOptions') &&
563
- Object.hasOwnProperty.call(options.writeStreamOptions, 'autoClose') &&
564
- options.writeStreamOptions.autoClose === false &&
565
- typeof dst === 'string'
566
- ) {
567
- wtr.destroy();
563
+ } else {
564
+ this.debugMsg('get: returning data into supplied stream');
565
+ wtr = dst;
568
566
  }
567
+ wtr.once('error', (err) => {
568
+ reject(
569
+ this.fmtError(
570
+ `${err.message} ${typeof dst === 'string' ? dst : '<stream>'}`,
571
+ 'get',
572
+ err.code
573
+ )
574
+ );
575
+ });
576
+ rdr.once('end', () => {
577
+ if (typeof dst === 'string') {
578
+ resolve(dst);
579
+ } else {
580
+ resolve(wtr);
581
+ }
582
+ });
583
+ rdr.pipe(wtr, opts.pipeOptions);
569
584
  });
570
585
  }
571
586
 
587
+ async get(remotePath, dst, options) {
588
+ let listeners;
589
+ try {
590
+ listeners = addTempListeners(this, 'get');
591
+ haveConnection(this, 'get');
592
+ return await this._get(remotePath, dst, options);
593
+ } catch (e) {
594
+ throw e.custom
595
+ ? e
596
+ : this.fmtError(`${e.message} ${remotePath}`, 'get', e.code);
597
+ } finally {
598
+ removeTempListeners(this, listeners, 'get');
599
+ this._resetEventFlags();
600
+ }
601
+ }
602
+
572
603
  /**
573
604
  * Use SSH2 fastGet for downloading the file.
574
605
  * Downloads a file at remotePath to localPath using parallel reads
@@ -579,46 +610,45 @@ class SftpClient {
579
610
  * @param {Object} options
580
611
  * @return {Promise<String>} the result of downloading the file
581
612
  */
613
+ _fastGet(rPath, lPath, opts) {
614
+ return new Promise((resolve, reject) => {
615
+ this.sftp.fastGet(rPath, lPath, opts, (err) => {
616
+ if (err) {
617
+ reject(
618
+ this.fmtError(`${err.message} Remote: ${rPath} Local: ${lPath}`)
619
+ );
620
+ }
621
+ resolve(`${rPath} was successfully download to ${lPath}!`);
622
+ });
623
+ });
624
+ }
625
+
582
626
  async fastGet(remotePath, localPath, options) {
627
+ let listeners;
583
628
  try {
629
+ listeners = addTempListeners(this, 'fastGet');
630
+ haveConnection(this, 'fastGet');
584
631
  const ftype = await this.exists(remotePath);
585
632
  if (ftype !== '-') {
586
- const msg =
587
- ftype === false
588
- ? `No such file ${remotePath}`
589
- : `Not a regular file ${remotePath}`;
590
- let err = new Error(msg);
591
- err.code = errorCode.badPath;
592
- throw err;
633
+ const msg = `${
634
+ !ftype ? 'No such file ' : 'Not a regular file'
635
+ } ${remotePath}`;
636
+ throw this.fmtError(msg, 'fastGet', errorCode.badPath);
593
637
  }
594
638
  const localCheck = haveLocalCreate(localPath);
595
639
  if (!localCheck.status) {
596
- let err = new Error(`Bad path: ${localPath}: ${localCheck.details}`);
597
- err.code = errorCode.badPath;
598
- throw err;
640
+ throw this.fmtError(
641
+ `Bad path: ${localPath}: ${localCheck.details}`,
642
+ 'fastGet',
643
+ errorCode.badPath
644
+ );
599
645
  }
600
- let rslt = await new Promise((resolve, reject) => {
601
- if (haveConnection(this, 'fastGet', reject)) {
602
- this.debugMsg(
603
- `fastGet -> remote: ${remotePath} local: ${localPath} `,
604
- options
605
- );
606
- addTempListeners(this, 'fastGet', reject);
607
- this.sftp.fastGet(remotePath, localPath, options, (err) => {
608
- if (err) {
609
- this.debugMsg(`fastGet error ${err.message} code: ${err.code}`);
610
- reject(err);
611
- }
612
- resolve(`${remotePath} was successfully download to ${localPath}!`);
613
- });
614
- }
615
- }).finally(() => {
616
- removeTempListeners(this, 'fastGet');
617
- });
618
- return rslt;
646
+ return await this._fastGet(remotePath, localPath, options);
619
647
  } catch (err) {
648
+ throw this.fmtError(err, 'fastGet');
649
+ } finally {
650
+ removeTempListeners(this, listeners, 'fastGet');
620
651
  this._resetEventFlags();
621
- throw fmtError(err, 'fastGet');
622
652
  }
623
653
  }
624
654
 
@@ -635,52 +665,50 @@ class SftpClient {
635
665
  * @param {Object} options
636
666
  * @return {Promise<String>} the result of downloading the file
637
667
  */
638
- fastPut(localPath, remotePath, options) {
639
- this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
668
+ _fastPut(lPath, rPath, opts) {
640
669
  return new Promise((resolve, reject) => {
670
+ this.sftp.fastPut(lPath, rPath, opts, (err) => {
671
+ if (err) {
672
+ reject(
673
+ this.fmtError(
674
+ `${err.message} Local: ${lPath} Remote: ${rPath}`,
675
+ 'fastPut',
676
+ err.code
677
+ )
678
+ );
679
+ }
680
+ resolve(`${lPath} was successfully uploaded to ${rPath}!`);
681
+ });
682
+ });
683
+ }
684
+
685
+ async fastPut(localPath, remotePath, options) {
686
+ let listeners;
687
+ try {
688
+ listeners = addTempListeners(this, 'fastPut');
689
+ this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
690
+ haveConnection(this, 'fastPut');
641
691
  const localCheck = haveLocalAccess(localPath);
642
692
  if (!localCheck.status) {
643
- reject(
644
- fmtError(
645
- `Bad path: ${localPath}: ${localCheck.details}`,
646
- 'fastPut',
647
- localCheck.code
648
- )
693
+ throw this.fmtError(
694
+ `Bad path: ${localPath}: ${localCheck.details}`,
695
+ 'fastPut',
696
+ localCheck.code
649
697
  );
650
698
  } else if (localCheck.status && localExists(localPath) === 'd') {
651
- reject(
652
- fmtError(
653
- `Bad path: ${localPath} not a regular file`,
654
- 'fastPut',
655
- errorCode.badPath
656
- )
657
- );
658
- } else if (haveConnection(this, 'fastPut', reject)) {
659
- this.debugMsg(
660
- `fastPut -> local: ${localPath} remote: ${remotePath} opts: ${JSON.stringify(
661
- options
662
- )}`
699
+ throw this.fmtError(
700
+ `Bad path: ${localPath} not a regular file`,
701
+ 'fastgPut',
702
+ errorCode.badPath
663
703
  );
664
- addTempListeners(this, 'fastPut', reject);
665
- this.sftp.fastPut(localPath, remotePath, options, (err) => {
666
- if (err) {
667
- this.debugMsg(`fastPut error ${err.message} ${err.code}`);
668
- reject(
669
- fmtError(
670
- `${err.message} Local: ${localPath} Remote: ${remotePath}`,
671
- 'fastPut',
672
- err.code
673
- )
674
- );
675
- }
676
- this.debugMsg('fastPut file transferred');
677
- resolve(`${localPath} was successfully uploaded to ${remotePath}!`);
678
- });
679
704
  }
680
- }).finally(() => {
681
- removeTempListeners(this, 'fastPut');
705
+ return await this._fastPut(localPath, remotePath, options);
706
+ } catch (e) {
707
+ throw e.custom ? e : this.fmtError(e.message, 'fastPut', e.code);
708
+ } finally {
709
+ removeTempListeners(this, listeners, 'fastPut');
682
710
  this._resetEventFlags();
683
- });
711
+ }
684
712
  }
685
713
 
686
714
  /**
@@ -693,91 +721,75 @@ class SftpClient {
693
721
  * @param {Object} options - options used for read, write stream and pipe configuration
694
722
  * value supported by node. Allowed properties are readStreamOptions,
695
723
  * writeStreamOptions and pipeOptions.
724
+ *
725
+ * *Important Note*: The ability to set ''autoClose' on read/write streams and 'end' on pipe() calls
726
+ * is no longer supported. New methods 'createReadStream()' and 'createWriteStream()' have been
727
+ * added to support low-level access to stream objects.
728
+ *
696
729
  * @return {Promise<String>}
697
730
  */
698
- put(
699
- localSrc,
700
- remotePath,
701
- options = {
702
- readStreamOptions: {},
703
- writeStreamOptions: { autoClose: true },
704
- pipeOptions: {},
705
- }
706
- ) {
731
+ _put(lPath, rPath, opts) {
707
732
  let wtr, rdr;
708
-
709
733
  return new Promise((resolve, reject) => {
734
+ opts = {
735
+ ...opts,
736
+ readStreamOptions: { autoClose: true },
737
+ writeStreamOptions: { autoClose: true },
738
+ pipeOptions: { end: true },
739
+ };
740
+ wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
741
+ wtr.once('error', (err) => {
742
+ reject(this.fmtError(`${err.message} ${rPath}`, 'put', err.code));
743
+ });
744
+ wtr.once('close', () => {
745
+ resolve(`Uploaded data stream to ${rPath}`);
746
+ });
747
+ if (lPath instanceof Buffer) {
748
+ this.debugMsg('put source is a buffer');
749
+ wtr.end(lPath);
750
+ } else {
751
+ rdr =
752
+ typeof lPath === 'string'
753
+ ? fs.createReadStream(lPath, opts.readStreamOptions)
754
+ : lPath;
755
+ rdr.once('error', (err) => {
756
+ reject(
757
+ this.fmtError(
758
+ `${err.message} ${
759
+ typeof lPath === 'string' ? lPath : '<stream>'
760
+ }`,
761
+ '_put',
762
+ err.code
763
+ )
764
+ );
765
+ });
766
+ rdr.pipe(wtr, opts.pipeOptions);
767
+ }
768
+ });
769
+ }
770
+
771
+ async put(localSrc, remotePath, options) {
772
+ let listeners;
773
+ try {
774
+ listeners = addTempListeners(this, 'put');
775
+ haveConnection(this, 'put');
710
776
  if (typeof localSrc === 'string') {
711
777
  const localCheck = haveLocalAccess(localSrc);
712
778
  if (!localCheck.status) {
713
- this.debugMsg(`put: local source check error ${localCheck.details}`);
714
- return reject(
715
- fmtError(
716
- `Bad path: ${localSrc}: ${localCheck.details}`,
717
- 'put',
718
- localCheck.code
719
- )
779
+ throw this.fmtError(
780
+ `Bad path: ${localSrc} ${localCheck.details}`,
781
+ 'put',
782
+ localCheck.code
720
783
  );
721
784
  }
722
785
  }
723
- if (haveConnection(this, 'put')) {
724
- addTempListeners(this, 'put', reject);
725
- wtr = this.sftp.createWriteStream(
726
- remotePath,
727
- options.writeStreamOptions
728
- ? { ...options.writeStreamOptions, autoClose: true }
729
- : {}
730
- );
731
- wtr.once('error', (err) => {
732
- this.debugMsg(`put: write stream error ${err.message}`);
733
- reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
734
- });
735
- wtr.once('close', () => {
736
- this.debugMsg('put: promise resolved');
737
- resolve(`Uploaded data stream to ${remotePath}`);
738
- });
739
- if (localSrc instanceof Buffer) {
740
- this.debugMsg('put source is a buffer');
741
- wtr.end(localSrc);
742
- } else {
743
- if (typeof localSrc === 'string') {
744
- this.debugMsg(`put source is a file path: ${localSrc}`);
745
- rdr = fs.createReadStream(
746
- localSrc,
747
- options.readStreamOptions ? options.readStreamOptions : {}
748
- );
749
- } else {
750
- this.debugMsg('put source is a stream');
751
- rdr = localSrc;
752
- }
753
- rdr.once('error', (err) => {
754
- this.debugMsg(`put: read stream error ${err.message}`);
755
- reject(
756
- fmtError(
757
- `${err.message} ${
758
- typeof localSrc === 'string' ? localSrc : ''
759
- }`,
760
- 'put',
761
- err.code
762
- )
763
- );
764
- });
765
- rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
766
- }
767
- }
768
- }).finally(() => {
769
- removeTempListeners(this, 'put');
786
+ return await this._put(localSrc, remotePath, options);
787
+ } catch (e) {
788
+ throw e.custom ? e : this.fmtError(e.message, 'put', e.code);
789
+ } finally {
790
+ removeTempListeners(this, listeners, 'put');
770
791
  this._resetEventFlags();
771
- if (
772
- rdr &&
773
- Object.hasOwnProperty.call(options, 'readStreamOptions') &&
774
- Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
775
- options.readStreamOptions.autoClose === false &&
776
- typeof localSrc === 'string'
777
- ) {
778
- rdr.destroy();
779
- }
780
- });
792
+ }
781
793
  }
782
794
 
783
795
  /**
@@ -788,44 +800,53 @@ class SftpClient {
788
800
  * @param {Object} options
789
801
  * @return {Promise<String>}
790
802
  */
803
+ _append(input, rPath, opts) {
804
+ return new Promise((resolve, reject) => {
805
+ this.debugMsg(`append -> remote: ${rPath} `, opts);
806
+ opts.flags = 'a';
807
+ const stream = this.sftp.createWriteStream(rPath, opts);
808
+ stream.on('error', (err) => {
809
+ reject(this.fmtError(`${err.message} ${rPath}`, 'append', err.code));
810
+ });
811
+ stream.on('close', () => {
812
+ resolve(`Appended data to ${rPath}`);
813
+ });
814
+ if (input instanceof Buffer) {
815
+ stream.write(input);
816
+ stream.end();
817
+ } else {
818
+ input.pipe(stream);
819
+ }
820
+ });
821
+ }
822
+
791
823
  async append(input, remotePath, options = {}) {
792
- const fileType = await this.exists(remotePath);
793
- if (fileType && fileType === 'd') {
794
- throw fmtError(
795
- `Bad path: ${remotePath}: cannot append to a directory`,
796
- 'append',
797
- errorCode.badPath
798
- );
799
- }
800
- return await new Promise((resolve, reject) => {
801
- if (haveConnection(this, 'append', reject)) {
802
- if (typeof input === 'string') {
803
- reject(fmtError('Cannot append one file to another', 'append'));
804
- } else {
805
- this.debugMsg(`append -> remote: ${remotePath} `, options);
806
- addTempListeners(this, 'append', reject);
807
- options.flags = 'a';
808
- let stream = this.sftp.createWriteStream(remotePath, options);
809
- stream.on('error', (err_1) => {
810
- reject(
811
- fmtError(`${err_1.message} ${remotePath}`, 'append', err_1.code)
812
- );
813
- });
814
- stream.on('finish', () => {
815
- resolve(`Appended data to ${remotePath}`);
816
- });
817
- if (input instanceof Buffer) {
818
- stream.write(input);
819
- stream.end();
820
- } else {
821
- input.pipe(stream);
822
- }
823
- }
824
+ let listeners;
825
+ try {
826
+ listeners = addTempListeners(this, 'append');
827
+ if (typeof input === 'string') {
828
+ throw this.fmtError(
829
+ 'Cannot append one file to another',
830
+ 'append',
831
+ errorCode.badPath
832
+ );
824
833
  }
825
- }).finally(() => {
826
- removeTempListeners(this, 'append');
834
+ haveConnection(this, 'append');
835
+ const fileType = await this.exists(remotePath);
836
+ if (fileType && fileType === 'd') {
837
+ throw this.fmtError(
838
+ `Bad path: ${remotePath}: cannot append to a directory`,
839
+ 'append',
840
+ errorCode.badPath
841
+ );
842
+ }
843
+ await this._append(input, remotePath, options);
844
+ } catch (e) {
845
+ throw e.custom ? e : this.fmtError(e.message, 'append', e.code);
846
+ } finally {
847
+ removeTempListeners(this, listeners, 'append');
827
848
  this._resetEventFlags();
828
- });
849
+ }
829
850
  }
830
851
 
831
852
  /**
@@ -837,67 +858,85 @@ class SftpClient {
837
858
  * @param {boolean} recursive - if true, recursively create directories
838
859
  * @return {Promise<String>}
839
860
  */
840
- async mkdir(remotePath, recursive = false) {
841
- const _mkdir = (p) => {
842
- return new Promise((resolve, reject) => {
843
- this.debugMsg(`_mkdir: create ${p}`);
844
- addTempListeners(this, '_mkdir', reject);
845
- this.sftp.mkdir(p, (err) => {
846
- if (err) {
847
- this.debugMsg(`_mkdir: Error ${err.message} code: ${err.code}`);
848
- if (err.code === 4) {
849
- //fix for windows dodgy error messages
850
- let error = new Error(`Bad path: ${p} permission denied`);
851
- error.code = errorCode.badPath;
852
- reject(error);
853
- } else if (err.code === 2) {
854
- let error = new Error(
855
- `Bad path: ${p} parent not a directory or not exist`
856
- );
857
- error.code = errorCode.badPath;
858
- reject(error);
859
- } else {
860
- reject(err);
861
- }
861
+ _doMkdir(p) {
862
+ return new Promise((resolve, reject) => {
863
+ this.sftp.mkdir(p, (err) => {
864
+ if (err) {
865
+ if (err.code === 4) {
866
+ //fix for windows dodgy error messages
867
+ reject(
868
+ this.fmtError(
869
+ `Bad path: ${p} permission denied`,
870
+ '_doMkdir',
871
+ errorCode.badPath
872
+ )
873
+ );
874
+ } else if (err.code === 2) {
875
+ reject(
876
+ this.fmtError(
877
+ `Bad path: ${p} parent not a directory or not exist`,
878
+ '_doMkdir',
879
+ errorCode.badPath
880
+ )
881
+ );
862
882
  } else {
863
- this.debugMsg('_mkdir: directory created');
864
- resolve(`${p} directory created`);
883
+ reject(this.fmtError(`${err.message} ${p}`, '_doMkdir', err.code));
865
884
  }
866
- });
867
- }).finally(() => {
868
- removeTempListeners(this, '_mkdir');
869
- this._resetEventFlags();
885
+ } else {
886
+ resolve(`${p} directory created`);
887
+ }
870
888
  });
871
- };
889
+ });
890
+ }
872
891
 
892
+ async _mkdir(remotePath, recursive) {
873
893
  try {
874
- haveConnection(this, 'mkdir');
875
- let rPath = await normalizeRemotePath(this, remotePath);
876
- let targetExists = await this.exists(rPath);
894
+ const rPath = await normalizeRemotePath(this, remotePath);
895
+ const targetExists = await this.exists(rPath);
877
896
  if (targetExists && targetExists !== 'd') {
878
- let error = new Error(`Bad path: ${rPath} already exists as a file`);
879
- error.code = errorCode.badPath;
880
- throw error;
897
+ throw this.fmtError(
898
+ `Bad path: ${rPath} already exists as a file`,
899
+ '_mkdir',
900
+ errorCode.badPath
901
+ );
881
902
  } else if (targetExists) {
882
903
  return `${rPath} already exists`;
883
904
  }
884
905
  if (!recursive) {
885
- return await _mkdir(rPath);
906
+ return await this._doMkdir(rPath);
886
907
  }
887
- let dir = parse(rPath).dir;
908
+ const dir = parse(rPath).dir;
888
909
  if (dir) {
889
- let dirExists = await this.exists(dir);
910
+ const dirExists = await this.exists(dir);
890
911
  if (!dirExists) {
891
- await this.mkdir(dir, true);
912
+ await this._mkdir(dir, true);
892
913
  } else if (dirExists !== 'd') {
893
- let error = new Error(`Bad path: ${dir} not a directory`);
894
- error.code = errorCode.badPath;
895
- throw error;
914
+ throw this.fmtError(
915
+ `Bad path: ${dir} not a directory`,
916
+ '_mkdir',
917
+ errorCode.badPath
918
+ );
896
919
  }
897
920
  }
898
- return await _mkdir(rPath);
921
+ return await this._doMkdir(rPath);
922
+ } catch (err) {
923
+ throw err.custom
924
+ ? err
925
+ : this.fmtError(`${err.message} ${remotePath}`, '_mkdir', err.code);
926
+ }
927
+ }
928
+
929
+ async mkdir(remotePath, recursive = false) {
930
+ let listeners;
931
+ try {
932
+ listeners = addTempListeners(this, '_mkdir');
933
+ haveConnection(this, 'mkdir');
934
+ return await this._mkdir(remotePath, recursive);
899
935
  } catch (err) {
900
- throw fmtError(`${err.message}`, 'mkdir', err.code);
936
+ throw this.fmtError(`${err.message}`, 'mkdir', err.code);
937
+ } finally {
938
+ removeTempListeners(this, listeners, 'append');
939
+ this._resetEventFlags();
901
940
  }
902
941
  }
903
942
 
@@ -915,42 +954,68 @@ class SftpClient {
915
954
  const _rmdir = (p) => {
916
955
  return new Promise((resolve, reject) => {
917
956
  this.debugMsg(`rmdir -> ${p}`);
918
- addTempListeners(this, 'rmdir', reject);
919
957
  this.sftp.rmdir(p, (err) => {
920
958
  if (err) {
921
- this.debugMsg(`rmdir error ${err.message} code: ${err.code}`);
922
- reject(fmtError(`${err.message} ${p}`, '_rmdir', err.code));
959
+ reject(this.fmtError(`${err.message} ${p}`, 'rmdir', err.code));
923
960
  }
924
961
  resolve('Successfully removed directory');
925
962
  });
926
- }).finally(() => {
927
- removeTempListeners(this, 'rmdir');
928
963
  });
929
964
  };
930
965
 
966
+ const _dormdir = async (p, recur) => {
967
+ try {
968
+ if (recur) {
969
+ const list = await this.list(p);
970
+ if (list.length) {
971
+ const files = list.filter((item) => item.type !== 'd');
972
+ const dirs = list.filter((item) => item.type === 'd');
973
+ this.debugMsg('rmdir contents (files): ', files);
974
+ this.debugMsg('rmdir contents (dirs): ', dirs);
975
+ for (const d of dirs) {
976
+ await _dormdir(`${p}${this.remotePathSep}${d.name}`, true);
977
+ }
978
+ const promiseList = [];
979
+ for (const f of files) {
980
+ promiseList.push(
981
+ this._delete(`${p}${this.remotePathSep}${f.name}`)
982
+ );
983
+ }
984
+ await Promise.all(promiseList);
985
+ }
986
+ }
987
+ return await _rmdir(p);
988
+ } catch (err) {
989
+ throw err.custom ? err : this.fmtError(err, '_dormdir', err.code);
990
+ }
991
+ };
992
+
993
+ let listeners;
931
994
  try {
995
+ listeners = addTempListeners(this, 'rmdir');
932
996
  haveConnection(this, 'rmdir');
933
- let absPath = await normalizeRemotePath(this, remotePath);
934
- if (!recursive) {
935
- return _rmdir(absPath);
936
- }
937
- let list = await this.list(absPath);
938
- if (list.length) {
939
- let files = list.filter((item) => item.type !== 'd');
940
- let dirs = list.filter((item) => item.type === 'd');
941
- this.debugMsg('rmdir contents (files): ', files);
942
- this.debugMsg('rmdir contents (dirs): ', dirs);
943
- for (let f of files) {
944
- await this.delete(`${absPath}${this.remotePathSep}${f.name}`);
945
- }
946
- for (let d of dirs) {
947
- await this.rmdir(`${absPath}${this.remotePathSep}${d.name}`, true);
948
- }
997
+ const absPath = await normalizeRemotePath(this, remotePath);
998
+ const dirStatus = await this.exists(absPath);
999
+ if (dirStatus && dirStatus !== 'd') {
1000
+ throw this.fmtError(
1001
+ `Bad path: ${absPath} not a directory`,
1002
+ 'rmdir',
1003
+ errorCode.badPath
1004
+ );
1005
+ } else if (!dirStatus) {
1006
+ throw this.fmtError(
1007
+ `Bad path: ${absPath} No such file`,
1008
+ 'rmdir',
1009
+ errorCode.badPath
1010
+ );
1011
+ } else {
1012
+ return await _dormdir(absPath, recursive);
949
1013
  }
950
- return _rmdir(absPath);
951
1014
  } catch (err) {
1015
+ throw err.custom ? err : this.fmtError(err.message, 'rmdir', err.code);
1016
+ } finally {
1017
+ removeTempListeners(this, listeners, 'rmdir');
952
1018
  this._resetEventFlags();
953
- throw err.custom ? err : fmtError(err, 'rmdir', err.code);
954
1019
  }
955
1020
  }
956
1021
 
@@ -965,32 +1030,37 @@ class SftpClient {
965
1030
  * @return {Promise<String>} with string 'Successfully deleted file' once resolved
966
1031
  *
967
1032
  */
968
- delete(remotePath, notFoundOK = false) {
1033
+ _delete(rPath, notFoundOK) {
969
1034
  return new Promise((resolve, reject) => {
970
- if (haveConnection(this, 'delete', reject)) {
971
- this.debugMsg(`delete -> ${remotePath}`);
972
- addTempListeners(this, 'delete', reject);
973
- this.sftp.unlink(remotePath, (err) => {
974
- if (err) {
975
- this.debugMsg(`delete error ${err.message} code: ${err.code}`);
976
- if (notFoundOK && err.code === 2) {
977
- this.debugMsg('delete ignore missing target error');
978
- resolve(`Successfully deleted ${remotePath}`);
979
- } else {
980
- reject(
981
- fmtError(`${err.message} ${remotePath}`, 'delete', err.code)
982
- );
983
- }
1035
+ this.sftp.unlink(rPath, (err) => {
1036
+ if (err) {
1037
+ if (notFoundOK && err.code === 2) {
1038
+ resolve(`Successfully deleted ${rPath}`);
1039
+ } else {
1040
+ reject(
1041
+ this.fmtError(`${err.message} ${rPath}`, 'delete', err.code)
1042
+ );
984
1043
  }
985
- resolve(`Successfully deleted ${remotePath}`);
986
- });
987
- }
988
- }).finally(() => {
989
- removeTempListeners(this, 'delete');
990
- this._resetEventFlags();
1044
+ }
1045
+ resolve(`Successfully deleted ${rPath}`);
1046
+ });
991
1047
  });
992
1048
  }
993
1049
 
1050
+ async delete(remotePath, notFoundOK = false) {
1051
+ let listeners;
1052
+ try {
1053
+ listeners = addTempListeners(this, 'delete');
1054
+ haveConnection(this, 'delete');
1055
+ return await this._delete(remotePath, notFoundOK);
1056
+ } catch (err) {
1057
+ throw err.custom ? err : this.fmtError(err.message, 'delete', err.code);
1058
+ } finally {
1059
+ removeTempListeners(this, listeners, 'delete');
1060
+ this._resetEventFlags();
1061
+ }
1062
+ }
1063
+
994
1064
  /**
995
1065
  * @async
996
1066
  *
@@ -1002,31 +1072,43 @@ class SftpClient {
1002
1072
  * @return {Promise<String>}
1003
1073
  *
1004
1074
  */
1005
- rename(fromPath, toPath) {
1075
+ _rename(fPath, tPath) {
1006
1076
  return new Promise((resolve, reject) => {
1007
- if (haveConnection(this, 'rename', reject)) {
1008
- this.debugMsg(`rename -> ${fromPath} ${toPath}`);
1009
- addTempListeners(this, 'rename', reject);
1010
- this.sftp.rename(fromPath, toPath, (err) => {
1011
- if (err) {
1012
- this.debugMsg(`rename error ${err.message} code: ${err.code}`);
1013
- reject(
1014
- fmtError(
1015
- `${err.message} From: ${fromPath} To: ${toPath}`,
1016
- 'rename',
1017
- err.code
1018
- )
1019
- );
1020
- }
1021
- resolve(`Successfully renamed ${fromPath} to ${toPath}`);
1022
- });
1023
- }
1024
- }).finally(() => {
1025
- removeTempListeners(this, 'rename');
1026
- this._resetEventFlags();
1077
+ this.sftp.rename(fPath, tPath, (err) => {
1078
+ if (err) {
1079
+ reject(
1080
+ this.fmtError(
1081
+ `${err.message} From: ${fPath} To: ${tPath}`,
1082
+ '_rename',
1083
+ err.code
1084
+ )
1085
+ );
1086
+ }
1087
+ resolve(`Successfully renamed ${fPath} to ${tPath}`);
1088
+ });
1027
1089
  });
1028
1090
  }
1029
1091
 
1092
+ async rename(fromPath, toPath) {
1093
+ let listeners;
1094
+ try {
1095
+ listeners = addTempListeners(this, 'rename');
1096
+ haveConnection(this, 'rename');
1097
+ return await this._rename(fromPath, toPath);
1098
+ } catch (err) {
1099
+ throw err.custom
1100
+ ? err
1101
+ : this.fmtError(
1102
+ `${err.message} ${fromPath} ${toPath}`,
1103
+ 'rename',
1104
+ err.code
1105
+ );
1106
+ } finally {
1107
+ removeTempListeners(this, listeners, 'rename');
1108
+ this._resetEventFlags();
1109
+ }
1110
+ }
1111
+
1030
1112
  /**
1031
1113
  * @async
1032
1114
  *
@@ -1039,31 +1121,43 @@ class SftpClient {
1039
1121
  * @return {Promise<String>}
1040
1122
  *
1041
1123
  */
1042
- posixRename(fromPath, toPath) {
1124
+ _posixRename(fPath, tPath) {
1043
1125
  return new Promise((resolve, reject) => {
1044
- if (haveConnection(this, 'posixRename', reject)) {
1045
- this.debugMsg(`posixRename -> ${fromPath} ${toPath}`);
1046
- addTempListeners(this, 'posixRename', reject);
1047
- this.sftp.ext_openssh_rename(fromPath, toPath, (err) => {
1048
- if (err) {
1049
- this.debugMsg(`posixRename error ${err.message} code: ${err.code}`);
1050
- reject(
1051
- fmtError(
1052
- `${err.message} From: ${fromPath} To: ${toPath}`,
1053
- 'posixRename',
1054
- err.code
1055
- )
1056
- );
1057
- }
1058
- resolve(`Successful POSIX rename ${fromPath} to ${toPath}`);
1059
- });
1060
- }
1061
- }).finally(() => {
1062
- removeTempListeners(this, 'posixRename');
1063
- this._resetEventFlags();
1126
+ this.sftp.ext_openssh_rename(fPath, tPath, (err) => {
1127
+ if (err) {
1128
+ reject(
1129
+ this.fmtError(
1130
+ `${err.message} From: ${fPath} To: ${tPath}`,
1131
+ '_posixRename',
1132
+ err.code
1133
+ )
1134
+ );
1135
+ }
1136
+ resolve(`Successful POSIX rename ${fPath} to ${tPath}`);
1137
+ });
1064
1138
  });
1065
1139
  }
1066
1140
 
1141
+ async posixRename(fromPath, toPath) {
1142
+ let listeners;
1143
+ try {
1144
+ listeners = addTempListeners(this, 'posixRename');
1145
+ haveConnection(this, 'posixRename');
1146
+ return await this._posixRename(fromPath, toPath);
1147
+ } catch (err) {
1148
+ throw err.custom
1149
+ ? err
1150
+ : this.fmtError(
1151
+ `${err.message} ${fromPath} ${toPath}`,
1152
+ 'posixRename',
1153
+ err.code
1154
+ );
1155
+ } finally {
1156
+ removeTempListeners(this, listeners, 'posixRename');
1157
+ this._resetEventFlags();
1158
+ }
1159
+ }
1160
+
1067
1161
  /**
1068
1162
  * @async
1069
1163
  *
@@ -1074,22 +1168,33 @@ class SftpClient {
1074
1168
  *
1075
1169
  * @return {Promise<String>}
1076
1170
  */
1077
- chmod(remotePath, mode) {
1171
+ _chmod(rPath, mode) {
1078
1172
  return new Promise((resolve, reject) => {
1079
- this.debugMsg(`chmod -> ${remotePath} ${mode}`);
1080
- addTempListeners(this, 'chmod', reject);
1081
- this.sftp.chmod(remotePath, mode, (err) => {
1173
+ this.sftp.chmod(rPath, mode, (err) => {
1082
1174
  if (err) {
1083
- reject(fmtError(`${err.message} ${remotePath}`, 'chmod', err.code));
1175
+ reject(this.fmtError(`${err.message} ${rPath}`, '_chmod', err.code));
1084
1176
  }
1085
1177
  resolve('Successfully change file mode');
1086
1178
  });
1087
- }).finally(() => {
1088
- removeTempListeners(this, 'chmod');
1089
- this._resetEventFlags();
1090
1179
  });
1091
1180
  }
1092
1181
 
1182
+ async chmod(remotePath, mode) {
1183
+ let listeners;
1184
+ try {
1185
+ listeners = addTempListeners(this, 'chmod');
1186
+ haveConnection(this, 'chmod');
1187
+ return await this._chmod(remotePath, mode);
1188
+ } catch (err) {
1189
+ throw err.custom
1190
+ ? err
1191
+ : this.fmtError(`${err.message} ${remotePath}`, 'chmod', err.code);
1192
+ } finally {
1193
+ removeTempListeners(this, listeners, 'chmod');
1194
+ this._resetEventFlags();
1195
+ }
1196
+ }
1197
+
1093
1198
  /**
1094
1199
  * @async
1095
1200
  *
@@ -1098,54 +1203,95 @@ class SftpClient {
1098
1203
  * server.
1099
1204
  * @param {String} srcDir - local source directory
1100
1205
  * @param {String} dstDir - remote destination directory
1101
- * @param {RegExp} filter - (Optional) a regular expression used to select
1102
- * files and directories to upload
1206
+ * @param {Object} options - (Optional) An object with 2 supported properties,
1207
+ * 'filter' and 'useFastput'. The first argument is the full path of the item
1208
+ * to be uploaded and the second argument is a boolean, which will be true if
1209
+ * the target path is for a directory. If the function returns true, the item
1210
+ * will be uploaded and excluded when it returns false. The 'useFastput' property is a
1211
+ * boolean value. When true, the 'fastPut()' method will be used to upload files. Default
1212
+ * is to use the slower, but more supported 'put()' method.
1213
+ *
1103
1214
  * @returns {Promise<String>}
1104
1215
  */
1105
- async uploadDir(srcDir, dstDir, filter = /.*/) {
1216
+ async _uploadDir(srcDir, dstDir, options) {
1106
1217
  try {
1107
- this.debugMsg(`uploadDir -> ${srcDir} ${dstDir}`);
1218
+ const absDstDir = await normalizeRemotePath(this, dstDir);
1219
+ this.debugMsg(`uploadDir <- SRC = ${srcDir} DST = ${absDstDir}`);
1108
1220
  const srcType = localExists(srcDir);
1221
+ if (!srcType) {
1222
+ throw this.fmtError(
1223
+ `Bad path: ${srcDir} not exist`,
1224
+ '_uploadDir',
1225
+ errorCode.badPath
1226
+ );
1227
+ }
1109
1228
  if (srcType !== 'd') {
1110
- throw fmtError(
1229
+ throw this.fmtError(
1111
1230
  `Bad path: ${srcDir}: not a directory`,
1112
- 'uploadDir',
1231
+ '_uploadDir',
1113
1232
  errorCode.badPath
1114
1233
  );
1115
1234
  }
1116
- haveConnection(this, 'uploadDir');
1117
- let dstStatus = await this.exists(dstDir);
1235
+ const dstStatus = await this.exists(absDstDir);
1118
1236
  if (dstStatus && dstStatus !== 'd') {
1119
- throw fmtError(`Bad path ${dstDir}`, 'uploadDir', errorCode.badPath);
1237
+ throw this.fmtError(
1238
+ `Bad path ${absDstDir} Not a directory`,
1239
+ '_uploadDir',
1240
+ errorCode.badPath
1241
+ );
1120
1242
  }
1121
1243
  if (!dstStatus) {
1122
- await this.mkdir(dstDir, true);
1244
+ await this._mkdir(absDstDir, true);
1123
1245
  }
1124
1246
  let dirEntries = fs.readdirSync(srcDir, {
1125
1247
  encoding: 'utf8',
1126
1248
  withFileTypes: true,
1127
1249
  });
1128
- dirEntries = dirEntries.filter((item) => filter.test(item.name));
1129
- for (let e of dirEntries) {
1250
+ if (options?.filter) {
1251
+ dirEntries = dirEntries.filter((item) =>
1252
+ options.filter(join(srcDir, item.name), item.isDirectory())
1253
+ );
1254
+ }
1255
+ let fileUploads = [];
1256
+ for (const e of dirEntries) {
1257
+ const newSrc = join(srcDir, e.name);
1258
+ const newDst = `${absDstDir}${this.remotePathSep}${e.name}`;
1130
1259
  if (e.isDirectory()) {
1131
- let newSrc = join(srcDir, e.name);
1132
- let newDst = dstDir + this.remotePathSep + e.name;
1133
- await this.uploadDir(newSrc, newDst, filter);
1260
+ await this.uploadDir(newSrc, newDst, options);
1134
1261
  } else if (e.isFile()) {
1135
- let src = join(srcDir, e.name);
1136
- let dst = dstDir + this.remotePathSep + e.name;
1137
- await this.put(src, dst);
1138
- this.client.emit('upload', { source: src, destination: dst });
1262
+ if (options?.useFastput) {
1263
+ fileUploads.push(this._fastPut(newSrc, newDst));
1264
+ } else {
1265
+ fileUploads.push(this._put(newSrc, newDst));
1266
+ }
1267
+ this.client.emit('upload', { source: newSrc, destination: newDst });
1139
1268
  } else {
1140
1269
  this.debugMsg(
1141
1270
  `uploadDir: File ignored: ${e.name} not a regular file`
1142
1271
  );
1143
1272
  }
1273
+ await Promise.all(fileUploads);
1144
1274
  }
1145
- return `${srcDir} uploaded to ${dstDir}`;
1275
+ return `${srcDir} uploaded to ${absDstDir}`;
1276
+ } catch (err) {
1277
+ throw err.custom
1278
+ ? err
1279
+ : this.fmtError(`${err.message} ${srcDir}`, '_uploadDir', err.code);
1280
+ }
1281
+ }
1282
+
1283
+ async uploadDir(srcDir, dstDir, options) {
1284
+ let listeners;
1285
+ try {
1286
+ listeners = addTempListeners(this, 'uploadDir');
1287
+ this.debugMsg(`uploadDir -> SRC = ${srcDir} DST = ${dstDir}`);
1288
+ haveConnection(this, 'uploadDir');
1289
+ return await this._uploadDir(srcDir, dstDir, options);
1146
1290
  } catch (err) {
1291
+ throw err.custom ? err : this.fmtError(err, 'uploadDir');
1292
+ } finally {
1293
+ removeTempListeners(this, listeners, 'chmod');
1147
1294
  this._resetEventFlags();
1148
- throw err.custom ? err : fmtError(err, 'uploadDir');
1149
1295
  }
1150
1296
  }
1151
1297
 
@@ -1157,18 +1303,29 @@ class SftpClient {
1157
1303
  * file system.
1158
1304
  * @param {String} srcDir - remote source directory
1159
1305
  * @param {String} dstDir - local destination directory
1160
- * @param {RegExp} filter - (Optional) a regular expression used to select
1161
- * files and directories to upload
1306
+ * @param {Object} options - (Optional) Object with 2 supported properties,
1307
+ * 'filter' and 'useFastget'. The filter property is a function of two
1308
+ * arguments. The first argument is the full path of the item to be downloaded
1309
+ * and the second argument is a boolean, which will be true if the target path
1310
+ * is for a directory. If the function returns true, the item will be
1311
+ * downloaded and excluded if teh function returns false.
1312
+ *
1162
1313
  * @returns {Promise<String>}
1163
1314
  */
1164
- async downloadDir(srcDir, dstDir, filter = /.*/) {
1315
+ async _downloadDir(srcDir, dstDir, options) {
1165
1316
  try {
1166
- this.debugMsg(`downloadDir -> ${srcDir} ${dstDir}`);
1167
- haveConnection(this, 'downloadDir');
1168
- let fileList = await this.list(srcDir, filter);
1317
+ let fileList = await this._list(srcDir);
1318
+ if (options?.filter) {
1319
+ fileList = fileList.filter((item) =>
1320
+ options.filter(
1321
+ `${srcDir}${this.remotePathSep}${item.name}`,
1322
+ item.type === 'd' ? true : false
1323
+ )
1324
+ );
1325
+ }
1169
1326
  const localCheck = haveLocalCreate(dstDir);
1170
1327
  if (!localCheck.status && localCheck.details === 'permission denied') {
1171
- throw fmtError(
1328
+ throw this.fmtError(
1172
1329
  `Bad path: ${dstDir}: ${localCheck.details}`,
1173
1330
  'downloadDir',
1174
1331
  localCheck.code
@@ -1176,35 +1333,182 @@ class SftpClient {
1176
1333
  } else if (localCheck.status && !localCheck.type) {
1177
1334
  fs.mkdirSync(dstDir, { recursive: true });
1178
1335
  } else if (localCheck.status && localCheck.type !== 'd') {
1179
- throw fmtError(
1336
+ throw this.fmtError(
1180
1337
  `Bad path: ${dstDir}: not a directory`,
1181
1338
  'downloadDir',
1182
1339
  errorCode.badPath
1183
1340
  );
1184
1341
  }
1185
- for (let f of fileList) {
1342
+ let downloadFiles = [];
1343
+ for (const f of fileList) {
1344
+ const newSrc = `${srcDir}${this.remotePathSep}${f.name}`;
1345
+ const newDst = join(dstDir, f.name);
1186
1346
  if (f.type === 'd') {
1187
- let newSrc = srcDir + this.remotePathSep + f.name;
1188
- let newDst = join(dstDir, f.name);
1189
- await this.downloadDir(newSrc, newDst, filter);
1347
+ await this._downloadDir(newSrc, newDst, options);
1190
1348
  } else if (f.type === '-') {
1191
- let src = srcDir + this.remotePathSep + f.name;
1192
- let dst = join(dstDir, f.name);
1193
- await this.get(src, dst);
1194
- this.client.emit('download', { source: src, destination: dst });
1349
+ if (options?.useFasget) {
1350
+ downloadFiles.push(this._fastGet(newSrc, newDst));
1351
+ } else {
1352
+ downloadFiles.push(this._get(newSrc, newDst));
1353
+ }
1354
+ this.client.emit('download', { source: newSrc, destination: newDst });
1195
1355
  } else {
1196
1356
  this.debugMsg(
1197
1357
  `downloadDir: File ignored: ${f.name} not regular file`
1198
1358
  );
1199
1359
  }
1200
1360
  }
1361
+ await Promise.all(downloadFiles);
1201
1362
  return `${srcDir} downloaded to ${dstDir}`;
1202
1363
  } catch (err) {
1364
+ throw err.custom
1365
+ ? err
1366
+ : this.fmtError(`${err.message} ${srcDir}`, '_downloadDir', err.code);
1367
+ }
1368
+ }
1369
+
1370
+ async downloadDir(srcDir, dstDir, options) {
1371
+ let listeners;
1372
+ try {
1373
+ listeners = addTempListeners(this, 'downloadDir');
1374
+ haveConnection(this, 'downloadDir');
1375
+ return await this._downloadDir(srcDir, dstDir, options);
1376
+ } catch (err) {
1377
+ throw err.custom ? err : this.fmtError(err, 'downloadDir', err.code);
1378
+ } finally {
1379
+ removeTempListeners(this, listeners, 'downloadDir');
1380
+ this._resetEventFlags();
1381
+ }
1382
+ }
1383
+
1384
+ /**
1385
+ *
1386
+ * Returns a read stream object. This is a low level method which will return a read stream
1387
+ * connected to the remote file object specified as an argument. Client code is fully responsible
1388
+ * for managing this stream object i.e. adding any necessary listeners and disposing of the object etc.
1389
+ * See the SSH2 sftp documentation for details on possible options which can be used.
1390
+ *
1391
+ * @param {String} remotePath - path to remote file to attach stream to
1392
+ * @param {Object} options - options to pass to the create stream process
1393
+ *
1394
+ * @returns {Object} a read stream object
1395
+ *
1396
+ */
1397
+ createReadStream(remotePath, options) {
1398
+ let listeners;
1399
+ try {
1400
+ listeners = addTempListeners(this, 'createReadStream');
1401
+ haveConnection(this, 'createReadStream');
1402
+ const stream = this.sftp.createReadStream(remotePath, options);
1403
+ return stream;
1404
+ } catch (err) {
1405
+ throw err.custom
1406
+ ? err
1407
+ : this.fmtError(err.message, 'createReadStream', err.code);
1408
+ } finally {
1409
+ removeTempListeners(this, listeners, 'createReadStreame');
1410
+ this._resetEventFlags();
1411
+ }
1412
+ }
1413
+
1414
+ /**
1415
+ *
1416
+ * Create a write stream object connected to a file on the remote sftp server.
1417
+ * This is a low level method which will return a write stream for the remote file specified
1418
+ * in the 'remotePath' argument. Client code to responsible for managing this object once created.
1419
+ * This includes disposing of file handles, setting up any necessary event listeners etc.
1420
+ *
1421
+ * @param {String} remotePath - path to the remote file on the sftp server
1422
+ * @param (Object} options - options to pass to the create write stream process)
1423
+ *
1424
+ * @returns {Object} a stream object
1425
+ *
1426
+ */
1427
+ createWriteStream(remotePath, options) {
1428
+ let listeners;
1429
+ try {
1430
+ listeners = addTempListeners(this, 'createWriteStream');
1431
+ haveConnection(this, 'createWriteStream');
1432
+ const stream = this.sftp.createWriteStream(remotePath, options);
1433
+ return stream;
1434
+ } catch (err) {
1435
+ throw err.custom
1436
+ ? err
1437
+ : this.fmtError(err.message, 'createWriteStream', err.code);
1438
+ } finally {
1439
+ removeTempListeners(this, listeners, 'createWriteStream');
1203
1440
  this._resetEventFlags();
1204
- throw err.custom ? err : fmtError(err, 'downloadDir', err.code);
1205
1441
  }
1206
1442
  }
1207
1443
 
1444
+ /**
1445
+ * @async
1446
+ *
1447
+ * Make a remote copy of a remote file. Create a copy of a remote file on the remote
1448
+ * server. It is assumed the directory where the copy will be placed already exists.
1449
+ * The destination file must not already exist.
1450
+ *
1451
+ * @param {String} srcPath - path to the remote file to be copied
1452
+ * @param {String} dstPath - destination path for the copy.
1453
+ *
1454
+ * @returns {String}.
1455
+ *
1456
+ */
1457
+ _rcopy(srcPath, dstPath) {
1458
+ return new Promise((resolve, reject) => {
1459
+ const ws = this.sftp.createWriteStream(dstPath);
1460
+ const rs = this.sftp.createReadStream(srcPath);
1461
+ ws.on('error', (err) => {
1462
+ reject(this.fmtError(`${err.message} ${dstPath}`, '_rcopy'));
1463
+ });
1464
+ rs.on('error', (err) => {
1465
+ reject(this.fmtError(`${err.message} ${srcPath}`, '_rcopy'));
1466
+ });
1467
+ ws.on('close', () => {
1468
+ resolve(`${srcPath} copied to ${dstPath}`);
1469
+ });
1470
+ rs.pipe(ws);
1471
+ });
1472
+ }
1473
+
1474
+ async rcopy(src, dst) {
1475
+ let listeners;
1476
+ try {
1477
+ listeners = addTempListeners(this, 'rcopy');
1478
+ haveConnection(this, 'rcopy');
1479
+ const srcPath = await normalizeRemotePath(this, src);
1480
+ const srcExists = await this.exists(srcPath);
1481
+ if (!srcExists) {
1482
+ throw this.fmtError(
1483
+ `Source does not exist ${srcPath}`,
1484
+ 'rcopy',
1485
+ errorCode.badPath
1486
+ );
1487
+ }
1488
+ if (srcExists !== '-') {
1489
+ throw this.fmtError(
1490
+ `Source not a file ${srcPath}`,
1491
+ 'rcopy',
1492
+ errorCode.badPath
1493
+ );
1494
+ }
1495
+ const dstPath = await normalizeRemotePath(this, dst);
1496
+ const dstExists = await this.exists(dstPath);
1497
+ if (dstExists) {
1498
+ throw this.fmtError(
1499
+ `Destination already exists ${dstPath}`,
1500
+ 'rcopy',
1501
+ errorCode.badPath
1502
+ );
1503
+ }
1504
+ return await this._rcopy(srcPath, dstPath);
1505
+ } catch (err) {
1506
+ throw err.custom ? err : this.fmtError(err, 'rcopy');
1507
+ } finally {
1508
+ removeTempListeners(this, listeners, 'rcopy');
1509
+ this._resetEventFlags();
1510
+ }
1511
+ }
1208
1512
  /**
1209
1513
  * @async
1210
1514
  *
@@ -1213,10 +1517,10 @@ class SftpClient {
1213
1517
  * @returns {Promise<Boolean>}
1214
1518
  */
1215
1519
  end() {
1216
- let endCloseHandler;
1520
+ let endCloseHandler, listeners;
1217
1521
  return new Promise((resolve, reject) => {
1522
+ listeners = addTempListeners(this, 'end', reject);
1218
1523
  this.endCalled = true;
1219
- addTempListeners(this, 'end', reject);
1220
1524
  endCloseHandler = () => {
1221
1525
  this.sftp = undefined;
1222
1526
  this.debugMsg('end: Connection closed');
@@ -1224,12 +1528,10 @@ class SftpClient {
1224
1528
  };
1225
1529
  this.on('close', endCloseHandler);
1226
1530
  if (haveConnection(this, 'end', reject)) {
1227
- this.debugMsg('end: Have connection - calling end()');
1228
1531
  this.client.end();
1229
1532
  }
1230
1533
  }).finally(() => {
1231
- this.debugMsg('end: finally clause fired');
1232
- removeTempListeners(this, 'end');
1534
+ removeTempListeners(this, listeners, 'end');
1233
1535
  this.removeListener('close', endCloseHandler);
1234
1536
  this.endCalled = false;
1235
1537
  this._resetEventFlags();