ssh2-sftp-client 8.1.0 → 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';
@@ -81,6 +76,51 @@ class SftpClient {
81
76
  }
82
77
  }
83
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
+
84
124
  /**
85
125
  * Add a listner to the client object. This is rarely necessary and can be
86
126
  * the source of errors. It is the client's responsibility to remove the
@@ -91,12 +131,10 @@ class SftpClient {
91
131
  * @param {function} callback - function called when event triggers
92
132
  */
93
133
  on(eventType, callback) {
94
- this.debugMsg(`Adding listener to ${eventType} event`);
95
134
  this.client.prependListener(eventType, callback);
96
135
  }
97
136
 
98
137
  removeListener(eventType, callback) {
99
- this.debugMsg(`Removing listener from ${eventType} event`);
100
138
  this.client.removeListener(eventType, callback);
101
139
  }
102
140
 
@@ -120,7 +158,6 @@ class SftpClient {
120
158
  let doReady, listeners;
121
159
  return new Promise((resolve, reject) => {
122
160
  listeners = addTempListeners(this, 'getConnection', reject);
123
- this.debugMsg('getConnection: created promise');
124
161
  doReady = () => {
125
162
  this.debugMsg(
126
163
  'getConnection ready listener: got connection - promise resolved'
@@ -129,9 +166,7 @@ class SftpClient {
129
166
  };
130
167
  this.on('ready', doReady);
131
168
  this.client.connect(config);
132
- }).finally(async () => {
133
- this.debugMsg('getConnection: finally clause fired');
134
- await sleep(500);
169
+ }).finally(() => {
135
170
  this.removeListener('ready', doReady);
136
171
  removeTempListeners(this, listeners, 'getConnection');
137
172
  this._resetEventFlags();
@@ -139,25 +174,17 @@ class SftpClient {
139
174
  }
140
175
 
141
176
  getSftpChannel() {
142
- let listeners;
143
177
  return new Promise((resolve, reject) => {
144
- listeners = addTempListeners(this, 'getSftpChannel', reject);
145
- this.debugMsg('getSftpChannel: created promise');
146
178
  this.client.sftp((err, sftp) => {
147
179
  if (err) {
148
- this.debugMsg(`getSftpChannel: SFTP Channel Error: ${err.message}`);
149
180
  this.client.end();
150
- reject(fmtError(err, 'getSftpChannel', err.code));
181
+ reject(this.fmtError(err, 'getSftpChannel', err.code));
151
182
  } else {
152
183
  this.debugMsg('getSftpChannel: SFTP channel established');
153
184
  this.sftp = sftp;
154
185
  resolve(sftp);
155
186
  }
156
187
  });
157
- }).finally(() => {
158
- this.debugMsg('getSftpChannel: finally clause fired');
159
- removeTempListeners(this, listeners, 'getSftpChannel');
160
- this._resetEventFlags();
161
188
  });
162
189
  }
163
190
 
@@ -174,47 +201,68 @@ class SftpClient {
174
201
  *
175
202
  */
176
203
  async connect(config) {
204
+ let listeners;
205
+
177
206
  try {
207
+ listeners = addTempListeners(this, 'connect');
178
208
  if (config.debug) {
179
209
  this.debug = config.debug;
180
210
  this.debugMsg('connect: Debugging turned on');
211
+ this.debugMsg(
212
+ `ssh2-sftp-client Version: ${this.version} `,
213
+ process.versions
214
+ );
181
215
  }
182
216
  if (this.sftp) {
183
- this.debugMsg('connect: Already connected - reject');
184
- throw fmtError(
217
+ throw this.fmtError(
185
218
  'An existing SFTP connection is already defined',
186
219
  'connect',
187
220
  errorCode.connect
188
221
  );
189
222
  }
190
- await promiseRetry(
191
- (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 {
192
230
  this.debugMsg(`connect: Connect attempt ${attempt}`);
193
- return this.getConnection(config).catch((err) => {
194
- this.debugMsg(
195
- `getConnection retry catch: ${err.message} Code: ${err.code}`
196
- );
197
- switch (err.code) {
198
- case 'ENOTFOUND':
199
- case 'ECONNREFUSED':
200
- case 'ERR_SOCKET_BAD_PORT':
201
- throw err;
202
- default:
203
- 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;
204
252
  }
205
- });
206
- },
207
- {
208
- retries: config.retries || 1,
209
- factor: config.retry_factor || 2,
210
- minTimeout: config.retry_minTimeout || 1000,
253
+ default:
254
+ retry(err);
255
+ }
211
256
  }
212
- );
213
- return this.getSftpChannel();
257
+ });
258
+ let sftp = await this.getSftpChannel();
259
+ return sftp;
214
260
  } catch (err) {
215
- 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');
216
265
  this._resetEventFlags();
217
- throw fmtError(err, 'connect');
218
266
  }
219
267
  }
220
268
 
@@ -229,31 +277,40 @@ class SftpClient {
229
277
  * @param {String} remotePath - remote path, may be relative
230
278
  * @returns {Promise<String>} - remote absolute path or ''
231
279
  */
232
- realPath(remotePath) {
233
- let listeners;
280
+ _realPath(rPath) {
234
281
  return new Promise((resolve, reject) => {
235
- listeners = addTempListeners(this, 'realPath', reject);
236
- this.debugMsg(`realPath -> ${remotePath}`);
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(() => {
293
+ }
294
+ this.debugMsg(`_realPath <- ${absPath}`);
295
+ resolve(absPath);
296
+ });
297
+ });
298
+ }
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 {
254
311
  removeTempListeners(this, listeners, 'realPath');
255
312
  this._resetEventFlags();
256
- });
313
+ }
257
314
  }
258
315
 
259
316
  /**
@@ -273,60 +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
- let listeners;
279
- return new Promise((resolve, reject) => {
280
- listeners = addTempListeners(this, '_stat', reject);
281
- this.debugMsg(`_stat: ${aPath}`);
282
- this.sftp.stat(aPath, (err, stats) => {
283
- if (err) {
284
- this.debugMsg(`_stat: Error ${err.message} code: ${err.code}`);
285
- if (err.code === 2 || err.code === 4) {
286
- reject(
287
- fmtError(
288
- `No such file: ${remotePath}`,
289
- '_stat',
290
- errorCode.notexist
291
- )
292
- );
293
- } else {
294
- reject(
295
- fmtError(`${err.message} ${remotePath}`, '_stat', err.code)
296
- );
297
- }
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
+ );
298
346
  } else {
299
- let result = {
300
- mode: stats.mode,
301
- uid: stats.uid,
302
- gid: stats.gid,
303
- size: stats.size,
304
- accessTime: stats.atime * 1000,
305
- modifyTime: stats.mtime * 1000,
306
- isDirectory: stats.isDirectory(),
307
- isFile: stats.isFile(),
308
- isBlockDevice: stats.isBlockDevice(),
309
- isCharacterDevice: stats.isCharacterDevice(),
310
- isSymbolicLink: stats.isSymbolicLink(),
311
- isFIFO: stats.isFIFO(),
312
- isSocket: stats.isSocket(),
313
- };
314
- this.debugMsg('_stat: stats <- ', result);
315
- resolve(result);
347
+ reject(this.fmtError(`${err.message} ${aPath}`, '_stat', err.code));
316
348
  }
317
- });
318
- }).finally(() => {
319
- removeTempListeners(this, listeners, '_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
+ }
320
368
  });
321
- };
369
+ });
370
+ }
322
371
 
372
+ async stat(remotePath) {
373
+ let listeners;
323
374
  try {
375
+ listeners = addTempListeners(this, 'stat');
324
376
  haveConnection(this, 'stat');
325
- let absPath = await normalizeRemotePath(this, remotePath);
326
- return _stat(absPath);
377
+ const absPath = await normalizeRemotePath(this, remotePath);
378
+ return await this._stat(absPath);
327
379
  } catch (err) {
380
+ throw err.custom ? err : this.fmtError(err, 'stat', err.code);
381
+ } finally {
382
+ removeTempListeners(this, listeners, 'stat');
328
383
  this._resetEventFlags();
329
- throw err.custom ? err : fmtError(err, 'stat', err.code);
330
384
  }
331
385
  }
332
386
 
@@ -341,48 +395,49 @@ class SftpClient {
341
395
  * @return {Promise<Boolean|String>} returns false if object does not exist. Returns type of
342
396
  * object if it does
343
397
  */
344
- async exists(remotePath) {
398
+ async _exists(rPath) {
345
399
  try {
346
- if (haveConnection(this, 'exists')) {
347
- if (remotePath === '.') {
348
- this.debugMsg('exists: . = d');
349
- return 'd';
350
- }
351
- let absPath = await normalizeRemotePath(this, remotePath);
352
- try {
353
- this.debugMsg(`exists: ${remotePath} -> ${absPath}`);
354
- let info = await this.stat(absPath);
355
- this.debugMsg('exists: <- ', info);
356
- if (info.isDirectory) {
357
- this.debugMsg(`exists: ${remotePath} = d`);
358
- return 'd';
359
- }
360
- if (info.isSymbolicLink) {
361
- this.debugMsg(`exists: ${remotePath} = l`);
362
- return 'l';
363
- }
364
- if (info.isFile) {
365
- this.debugMsg(`exists: ${remotePath} = -`);
366
- return '-';
367
- }
368
- this.debugMsg(`exists: ${remotePath} = false`);
369
- return false;
370
- } catch (err) {
371
- if (err.code === errorCode.notexist) {
372
- this.debugMsg(
373
- `exists: ${remotePath} = false errorCode = ${err.code}`
374
- );
375
- return false;
376
- }
377
- this.debugMsg(`exists: throw error ${err.message} ${err.code}`);
378
- throw err;
379
- }
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';
380
411
  }
381
- this.debugMsg(`exists: default ${remotePath} = false`);
412
+ if (info.isFile) {
413
+ this.debugMsg(`exists: ${rPath} = -`);
414
+ return '-';
415
+ }
416
+ this.debugMsg(`exists: ${rPath} = false`);
382
417
  return false;
383
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');
384
440
  this._resetEventFlags();
385
- throw err.custom ? err : fmtError(err, 'exists', err.code);
386
441
  }
387
442
  }
388
443
 
@@ -396,60 +451,59 @@ class SftpClient {
396
451
  * accessTime, rights {user, group other}, owner and group.
397
452
  *
398
453
  * @param {String} remotePath - path to remote directory
399
- * @param {RegExp} pattern - regular expression to match filenames
454
+ * @param {function} filter - a filter function used to select return entries
400
455
  * @returns {Promise<Array>} array of file description objects
401
456
  */
402
- list(remotePath, pattern = /.*/) {
403
- let listeners;
457
+ _list(remotePath, filter) {
404
458
  return new Promise((resolve, reject) => {
405
- listeners = addTempListeners(this, 'list', reject);
406
- if (haveConnection(this, 'list', reject)) {
407
- const reg = /-/gi;
408
- this.debugMsg(`list: ${remotePath} filter: ${pattern}`);
409
- this.sftp.readdir(remotePath, (err, fileList) => {
410
- if (err) {
411
- this.debugMsg(`list: Error ${err.message} code: ${err.code}`);
412
- 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)));
413
485
  } else {
414
- let newList = [];
415
- // reset file info
416
- if (fileList) {
417
- newList = fileList.map((item) => {
418
- return {
419
- type: item.longname.slice(0, 1),
420
- name: item.filename,
421
- size: item.attrs.size,
422
- modifyTime: item.attrs.mtime * 1000,
423
- accessTime: item.attrs.atime * 1000,
424
- rights: {
425
- user: item.longname.slice(1, 4).replace(reg, ''),
426
- group: item.longname.slice(4, 7).replace(reg, ''),
427
- other: item.longname.slice(7, 10).replace(reg, ''),
428
- },
429
- owner: item.attrs.uid,
430
- group: item.attrs.gid,
431
- longname: item.longname,
432
- };
433
- });
434
- }
435
- // provide some compatibility for auxList
436
- let regex;
437
- if (pattern instanceof RegExp) {
438
- regex = pattern;
439
- } else {
440
- let newPattern = pattern.replace(/\*([^*])*?/gi, '.*');
441
- regex = new RegExp(newPattern);
442
- }
443
- let filteredList = newList.filter((item) => regex.test(item.name));
444
- this.debugMsg('list: result: ', filteredList);
445
- resolve(filteredList);
486
+ resolve(newList);
446
487
  }
447
- });
448
- }
449
- }).finally(() => {
488
+ }
489
+ });
490
+ });
491
+ }
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 {
450
504
  removeTempListeners(this, listeners, 'list');
451
505
  this._resetEventFlags();
452
- });
506
+ }
453
507
  }
454
508
 
455
509
  /**
@@ -465,110 +519,85 @@ class SftpClient {
465
519
  * @param {Object} options - options object with supported properties of readStreamOptions,
466
520
  * writeStreamOptions and pipeOptions.
467
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
+ *
468
526
  * @return {Promise<String|Stream|Buffer>}
469
527
  */
470
- get(
471
- remotePath,
472
- dst,
473
- options = { readStreamOptions: {}, writeStreamOptions: {}, pipeOptions: {} }
474
- ) {
475
- let rdr, wtr, listeners;
528
+ _get(rPath, dst, opts) {
529
+ let rdr, wtr;
476
530
  return new Promise((resolve, reject) => {
477
- listeners = addTempListeners(this, 'get', reject);
478
- if (haveConnection(this, 'get', reject)) {
479
- this.debugMsg(`get -> ${remotePath} `, options);
480
- rdr = this.sftp.createReadStream(
481
- remotePath,
482
- options.readStreamOptions ? options.readStreamOptions : {}
483
- );
484
- rdr.once('error', (err) => {
485
- 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);
486
546
  });
487
- if (dst === undefined) {
488
- // no dst specified, return buffer of data
489
- this.debugMsg('get returning buffer of data');
490
- wtr = concat((buff) => {
491
- //rdr.removeAllListeners('error');
492
- resolve(buff);
493
- });
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;
494
560
  } else {
495
- if (typeof dst === 'string') {
496
- // dst local file path
497
- this.debugMsg('get returning local file');
498
- const localCheck = haveLocalCreate(dst);
499
- if (!localCheck.status) {
500
- return reject(
501
- fmtError(
502
- `Bad path: ${dst}: ${localCheck.details}`,
503
- 'get',
504
- localCheck.code
505
- )
506
- );
507
- }
508
- wtr = fs.createWriteStream(
509
- dst,
510
- options.writeStreamOptions ? options.writeStreamOptions : {}
511
- );
512
- } else {
513
- this.debugMsg('get returning data into supplied stream');
514
- wtr = dst;
515
- }
516
- wtr.once('error', (err) => {
517
- reject(
518
- fmtError(
519
- `${err.message} ${typeof dst === 'string' ? dst : ''}`,
520
- 'get',
521
- err.code
522
- )
523
- );
524
- });
525
- if (
526
- Object.hasOwnProperty.call(options, 'pipeOptions') &&
527
- Object.hasOwnProperty.call(options.pipeOptions, 'end') &&
528
- !options.pipeOptions.end
529
- ) {
530
- rdr.once('end', () => {
531
- this.debugMsg('get resolved on reader end event');
532
- if (typeof dst === 'string') {
533
- resolve(dst);
534
- } else {
535
- resolve(wtr);
536
- }
537
- });
538
- } else {
539
- wtr.once('finish', () => {
540
- this.debugMsg('get resolved on writer finish event');
541
- if (typeof dst === 'string') {
542
- resolve(dst);
543
- } else {
544
- resolve(wtr);
545
- }
546
- });
547
- }
561
+ wtr = fs.createWriteStream(dst, opts.writeStreamOptions);
548
562
  }
549
- rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
563
+ } else {
564
+ this.debugMsg('get: returning data into supplied stream');
565
+ wtr = dst;
550
566
  }
551
- }).finally(() => {
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);
584
+ });
585
+ }
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 {
552
598
  removeTempListeners(this, listeners, 'get');
553
599
  this._resetEventFlags();
554
- if (
555
- rdr &&
556
- Object.hasOwnProperty.call(options, 'readStreamOptions') &&
557
- Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
558
- options.readStreamOptions.autoClose === false
559
- ) {
560
- rdr.destroy();
561
- }
562
- if (
563
- wtr &&
564
- Object.hasOwnProperty.call(options, 'writeStreamOptions') &&
565
- Object.hasOwnProperty.call(options.writeStreamOptions, 'autoClose') &&
566
- options.writeStreamOptions.autoClose === false &&
567
- typeof dst === 'string'
568
- ) {
569
- wtr.destroy();
570
- }
571
- });
600
+ }
572
601
  }
573
602
 
574
603
  /**
@@ -581,47 +610,45 @@ class SftpClient {
581
610
  * @param {Object} options
582
611
  * @return {Promise<String>} the result of downloading the file
583
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
+
584
626
  async fastGet(remotePath, localPath, options) {
627
+ let listeners;
585
628
  try {
629
+ listeners = addTempListeners(this, 'fastGet');
630
+ haveConnection(this, 'fastGet');
586
631
  const ftype = await this.exists(remotePath);
587
632
  if (ftype !== '-') {
588
- const msg =
589
- ftype === false
590
- ? `No such file ${remotePath}`
591
- : `Not a regular file ${remotePath}`;
592
- let err = new Error(msg);
593
- err.code = errorCode.badPath;
594
- throw err;
633
+ const msg = `${
634
+ !ftype ? 'No such file ' : 'Not a regular file'
635
+ } ${remotePath}`;
636
+ throw this.fmtError(msg, 'fastGet', errorCode.badPath);
595
637
  }
596
638
  const localCheck = haveLocalCreate(localPath);
597
639
  if (!localCheck.status) {
598
- let err = new Error(`Bad path: ${localPath}: ${localCheck.details}`);
599
- err.code = errorCode.badPath;
600
- throw err;
640
+ throw this.fmtError(
641
+ `Bad path: ${localPath}: ${localCheck.details}`,
642
+ 'fastGet',
643
+ errorCode.badPath
644
+ );
601
645
  }
602
- let listeners;
603
- let rslt = await new Promise((resolve, reject) => {
604
- listeners = addTempListeners(this, 'fastGet', reject);
605
- if (haveConnection(this, 'fastGet', reject)) {
606
- this.debugMsg(
607
- `fastGet -> remote: ${remotePath} local: ${localPath} `,
608
- options
609
- );
610
- this.sftp.fastGet(remotePath, localPath, options, (err) => {
611
- if (err) {
612
- this.debugMsg(`fastGet error ${err.message} code: ${err.code}`);
613
- reject(err);
614
- }
615
- resolve(`${remotePath} was successfully download to ${localPath}!`);
616
- });
617
- }
618
- }).finally(() => {
619
- removeTempListeners(this, listeners, 'fastGet');
620
- });
621
- return rslt;
646
+ return await this._fastGet(remotePath, localPath, options);
622
647
  } catch (err) {
648
+ throw this.fmtError(err, 'fastGet');
649
+ } finally {
650
+ removeTempListeners(this, listeners, 'fastGet');
623
651
  this._resetEventFlags();
624
- throw fmtError(err, 'fastGet');
625
652
  }
626
653
  }
627
654
 
@@ -638,53 +665,50 @@ class SftpClient {
638
665
  * @param {Object} options
639
666
  * @return {Promise<String>} the result of downloading the file
640
667
  */
641
- fastPut(localPath, remotePath, options) {
642
- let listeners;
643
- this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
668
+ _fastPut(lPath, rPath, opts) {
644
669
  return new Promise((resolve, reject) => {
645
- listeners = addTempListeners(this, 'fastPut', 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');
646
691
  const localCheck = haveLocalAccess(localPath);
647
692
  if (!localCheck.status) {
648
- reject(
649
- fmtError(
650
- `Bad path: ${localPath}: ${localCheck.details}`,
651
- 'fastPut',
652
- localCheck.code
653
- )
693
+ throw this.fmtError(
694
+ `Bad path: ${localPath}: ${localCheck.details}`,
695
+ 'fastPut',
696
+ localCheck.code
654
697
  );
655
698
  } else if (localCheck.status && localExists(localPath) === 'd') {
656
- reject(
657
- fmtError(
658
- `Bad path: ${localPath} not a regular file`,
659
- 'fastPut',
660
- errorCode.badPath
661
- )
662
- );
663
- } else if (haveConnection(this, 'fastPut', reject)) {
664
- this.debugMsg(
665
- `fastPut -> local: ${localPath} remote: ${remotePath} opts: ${JSON.stringify(
666
- options
667
- )}`
699
+ throw this.fmtError(
700
+ `Bad path: ${localPath} not a regular file`,
701
+ 'fastgPut',
702
+ errorCode.badPath
668
703
  );
669
- this.sftp.fastPut(localPath, remotePath, options, (err) => {
670
- if (err) {
671
- this.debugMsg(`fastPut error ${err.message} ${err.code}`);
672
- reject(
673
- fmtError(
674
- `${err.message} Local: ${localPath} Remote: ${remotePath}`,
675
- 'fastPut',
676
- err.code
677
- )
678
- );
679
- }
680
- this.debugMsg('fastPut file transferred');
681
- resolve(`${localPath} was successfully uploaded to ${remotePath}!`);
682
- });
683
704
  }
684
- }).finally(() => {
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 {
685
709
  removeTempListeners(this, listeners, 'fastPut');
686
710
  this._resetEventFlags();
687
- });
711
+ }
688
712
  }
689
713
 
690
714
  /**
@@ -697,90 +721,75 @@ class SftpClient {
697
721
  * @param {Object} options - options used for read, write stream and pipe configuration
698
722
  * value supported by node. Allowed properties are readStreamOptions,
699
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
+ *
700
729
  * @return {Promise<String>}
701
730
  */
702
- put(
703
- localSrc,
704
- remotePath,
705
- options = {
706
- readStreamOptions: {},
707
- writeStreamOptions: { autoClose: true },
708
- pipeOptions: {},
709
- }
710
- ) {
711
- let wtr, rdr, listeners;
731
+ _put(lPath, rPath, opts) {
732
+ let wtr, rdr;
712
733
  return new Promise((resolve, reject) => {
713
- listeners = addTempListeners(this, 'put', 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');
714
776
  if (typeof localSrc === 'string') {
715
777
  const localCheck = haveLocalAccess(localSrc);
716
778
  if (!localCheck.status) {
717
- this.debugMsg(`put: local source check error ${localCheck.details}`);
718
- return reject(
719
- fmtError(
720
- `Bad path: ${localSrc}: ${localCheck.details}`,
721
- 'put',
722
- localCheck.code
723
- )
779
+ throw this.fmtError(
780
+ `Bad path: ${localSrc} ${localCheck.details}`,
781
+ 'put',
782
+ localCheck.code
724
783
  );
725
784
  }
726
785
  }
727
- if (haveConnection(this, 'put')) {
728
- wtr = this.sftp.createWriteStream(
729
- remotePath,
730
- options.writeStreamOptions
731
- ? { ...options.writeStreamOptions, autoClose: true }
732
- : {}
733
- );
734
- wtr.once('error', (err) => {
735
- this.debugMsg(`put: write stream error ${err.message}`);
736
- reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
737
- });
738
- wtr.once('close', () => {
739
- this.debugMsg('put: promise resolved');
740
- resolve(`Uploaded data stream to ${remotePath}`);
741
- });
742
- if (localSrc instanceof Buffer) {
743
- this.debugMsg('put source is a buffer');
744
- wtr.end(localSrc);
745
- } else {
746
- if (typeof localSrc === 'string') {
747
- this.debugMsg(`put source is a file path: ${localSrc}`);
748
- rdr = fs.createReadStream(
749
- localSrc,
750
- options.readStreamOptions ? options.readStreamOptions : {}
751
- );
752
- } else {
753
- this.debugMsg('put source is a stream');
754
- rdr = localSrc;
755
- }
756
- rdr.once('error', (err) => {
757
- this.debugMsg(`put: read stream error ${err.message}`);
758
- reject(
759
- fmtError(
760
- `${err.message} ${
761
- typeof localSrc === 'string' ? localSrc : ''
762
- }`,
763
- 'put',
764
- err.code
765
- )
766
- );
767
- });
768
- rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
769
- }
770
- }
771
- }).finally(() => {
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 {
772
790
  removeTempListeners(this, listeners, 'put');
773
791
  this._resetEventFlags();
774
- if (
775
- rdr &&
776
- Object.hasOwnProperty.call(options, 'readStreamOptions') &&
777
- Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
778
- options.readStreamOptions.autoClose === false &&
779
- typeof localSrc === 'string'
780
- ) {
781
- rdr.destroy();
782
- }
783
- });
792
+ }
784
793
  }
785
794
 
786
795
  /**
@@ -791,59 +800,49 @@ class SftpClient {
791
800
  * @param {Object} options
792
801
  * @return {Promise<String>}
793
802
  */
794
-
795
- async append(input, remotePath, options = {}) {
796
- const _append = (input, remotePath, options) => {
797
- return new Promise((resolve, reject) => {
798
- this.debugMsg(`append -> remote: ${remotePath} `, options);
799
- options.flags = 'a';
800
- let stream = this.sftp.createWriteStream(remotePath, options);
801
- stream.on('error', (err) => {
802
- this.debugMsg(
803
- `append: Error ${err.message} appending to ${remotePath}`
804
- );
805
- reject(fmtError(`${err.message} ${remotePath}`, 'append', err.code));
806
- });
807
- stream.on('close', () => {
808
- this.debugMsg(`append: data appended to ${remotePath}`);
809
- resolve(`Appended data to ${remotePath}`);
810
- });
811
- if (input instanceof Buffer) {
812
- this.debugMsg('append: writing data buffer to remote file');
813
- stream.write(input);
814
- stream.end();
815
- } else {
816
- this.debugMsg('append: writing stream to remote file');
817
- input.pipe(stream);
818
- }
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));
819
810
  });
820
- };
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
+ }
821
822
 
823
+ async append(input, remotePath, options = {}) {
822
824
  let listeners;
823
825
  try {
824
826
  listeners = addTempListeners(this, 'append');
825
827
  if (typeof input === 'string') {
826
- this.debugMsg('append: attempt to append two files - throw');
827
- throw fmtError(
828
+ throw this.fmtError(
828
829
  'Cannot append one file to another',
829
830
  'append',
830
831
  errorCode.badPath
831
832
  );
832
833
  }
833
- if (haveConnection(this, 'append')) {
834
- const fileType = await this.exists(remotePath);
835
- if (fileType && fileType === 'd') {
836
- this.debugMsg(`append: Error ${remotePath} not a file`);
837
- throw fmtError(
838
- `Bad path: ${remotePath}: cannot append to a directory`,
839
- 'append',
840
- errorCode.badPath
841
- );
842
- }
843
- await _append(input, remotePath, options);
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
+ );
844
842
  }
843
+ await this._append(input, remotePath, options);
845
844
  } catch (e) {
846
- throw e.custom ? e : fmtError(e.message, 'append', e.code);
845
+ throw e.custom ? e : this.fmtError(e.message, 'append', e.code);
847
846
  } finally {
848
847
  removeTempListeners(this, listeners, 'append');
849
848
  this._resetEventFlags();
@@ -859,68 +858,85 @@ class SftpClient {
859
858
  * @param {boolean} recursive - if true, recursively create directories
860
859
  * @return {Promise<String>}
861
860
  */
862
- async mkdir(remotePath, recursive = false) {
863
- const _mkdir = (p) => {
864
- let listeners;
865
- return new Promise((resolve, reject) => {
866
- listeners = addTempListeners(this, '_mkdir', reject);
867
- this.debugMsg(`_mkdir: create ${p}`);
868
- this.sftp.mkdir(p, (err) => {
869
- if (err) {
870
- this.debugMsg(`_mkdir: Error ${err.message} code: ${err.code}`);
871
- if (err.code === 4) {
872
- //fix for windows dodgy error messages
873
- let error = new Error(`Bad path: ${p} permission denied`);
874
- error.code = errorCode.badPath;
875
- reject(error);
876
- } else if (err.code === 2) {
877
- let error = new Error(
878
- `Bad path: ${p} parent not a directory or not exist`
879
- );
880
- error.code = errorCode.badPath;
881
- reject(error);
882
- } else {
883
- reject(err);
884
- }
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
+ );
885
882
  } else {
886
- this.debugMsg('_mkdir: directory created');
887
- resolve(`${p} directory created`);
883
+ reject(this.fmtError(`${err.message} ${p}`, '_doMkdir', err.code));
888
884
  }
889
- });
890
- }).finally(() => {
891
- removeTempListeners(this, listeners, '_mkdir');
892
- this._resetEventFlags();
885
+ } else {
886
+ resolve(`${p} directory created`);
887
+ }
893
888
  });
894
- };
889
+ });
890
+ }
895
891
 
892
+ async _mkdir(remotePath, recursive) {
896
893
  try {
897
- haveConnection(this, 'mkdir');
898
- let rPath = await normalizeRemotePath(this, remotePath);
899
- let targetExists = await this.exists(rPath);
894
+ const rPath = await normalizeRemotePath(this, remotePath);
895
+ const targetExists = await this.exists(rPath);
900
896
  if (targetExists && targetExists !== 'd') {
901
- let error = new Error(`Bad path: ${rPath} already exists as a file`);
902
- error.code = errorCode.badPath;
903
- throw error;
897
+ throw this.fmtError(
898
+ `Bad path: ${rPath} already exists as a file`,
899
+ '_mkdir',
900
+ errorCode.badPath
901
+ );
904
902
  } else if (targetExists) {
905
903
  return `${rPath} already exists`;
906
904
  }
907
905
  if (!recursive) {
908
- return await _mkdir(rPath);
906
+ return await this._doMkdir(rPath);
909
907
  }
910
- let dir = parse(rPath).dir;
908
+ const dir = parse(rPath).dir;
911
909
  if (dir) {
912
- let dirExists = await this.exists(dir);
910
+ const dirExists = await this.exists(dir);
913
911
  if (!dirExists) {
914
- await this.mkdir(dir, true);
912
+ await this._mkdir(dir, true);
915
913
  } else if (dirExists !== 'd') {
916
- let error = new Error(`Bad path: ${dir} not a directory`);
917
- error.code = errorCode.badPath;
918
- throw error;
914
+ throw this.fmtError(
915
+ `Bad path: ${dir} not a directory`,
916
+ '_mkdir',
917
+ errorCode.badPath
918
+ );
919
919
  }
920
920
  }
921
- 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);
922
935
  } catch (err) {
923
- 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();
924
940
  }
925
941
  }
926
942
 
@@ -935,54 +951,42 @@ class SftpClient {
935
951
  * @return {Promise<String>}
936
952
  */
937
953
  async rmdir(remotePath, recursive = false) {
938
- const _delete = (remotePath) => {
939
- return new Promise((resolve, reject) => {
940
- this.sftp.unlink(remotePath, (err) => {
941
- if (err && err.code !== 2) {
942
- reject(fmtError(`${err.message} ${remotePath}`, 'rmdir', err.code));
943
- }
944
- resolve(true);
945
- });
946
- });
947
- };
948
-
949
954
  const _rmdir = (p) => {
950
955
  return new Promise((resolve, reject) => {
951
956
  this.debugMsg(`rmdir -> ${p}`);
952
957
  this.sftp.rmdir(p, (err) => {
953
958
  if (err) {
954
- this.debugMsg(`rmdir error ${err.message} code: ${err.code}`);
955
- reject(fmtError(`${err.message} ${p}`, 'rmdir', err.code));
959
+ reject(this.fmtError(`${err.message} ${p}`, 'rmdir', err.code));
956
960
  }
957
961
  resolve('Successfully removed directory');
958
962
  });
959
- }).finally(() => {
960
- removeTempListeners(this, listeners, '_rmdir');
961
963
  });
962
964
  };
963
965
 
964
966
  const _dormdir = async (p, recur) => {
965
967
  try {
966
968
  if (recur) {
967
- let list = await this.list(p);
969
+ const list = await this.list(p);
968
970
  if (list.length) {
969
- let files = list.filter((item) => item.type !== 'd');
970
- let dirs = list.filter((item) => item.type === 'd');
971
+ const files = list.filter((item) => item.type !== 'd');
972
+ const dirs = list.filter((item) => item.type === 'd');
971
973
  this.debugMsg('rmdir contents (files): ', files);
972
974
  this.debugMsg('rmdir contents (dirs): ', dirs);
973
- for (let d of dirs) {
975
+ for (const d of dirs) {
974
976
  await _dormdir(`${p}${this.remotePathSep}${d.name}`, true);
975
977
  }
976
- let promiseList = [];
977
- for (let f of files) {
978
- promiseList.push(_delete(`${p}${this.remotePathSep}${f.name}`));
978
+ const promiseList = [];
979
+ for (const f of files) {
980
+ promiseList.push(
981
+ this._delete(`${p}${this.remotePathSep}${f.name}`)
982
+ );
979
983
  }
980
984
  await Promise.all(promiseList);
981
985
  }
982
986
  }
983
987
  return await _rmdir(p);
984
988
  } catch (err) {
985
- throw err.custom ? err : fmtError(err, '_dormdir', err.code);
989
+ throw err.custom ? err : this.fmtError(err, '_dormdir', err.code);
986
990
  }
987
991
  };
988
992
 
@@ -990,16 +994,16 @@ class SftpClient {
990
994
  try {
991
995
  listeners = addTempListeners(this, 'rmdir');
992
996
  haveConnection(this, 'rmdir');
993
- let absPath = await normalizeRemotePath(this, remotePath);
994
- let dirStatus = await this.exists(absPath);
997
+ const absPath = await normalizeRemotePath(this, remotePath);
998
+ const dirStatus = await this.exists(absPath);
995
999
  if (dirStatus && dirStatus !== 'd') {
996
- throw fmtError(
1000
+ throw this.fmtError(
997
1001
  `Bad path: ${absPath} not a directory`,
998
1002
  'rmdir',
999
1003
  errorCode.badPath
1000
1004
  );
1001
1005
  } else if (!dirStatus) {
1002
- throw fmtError(
1006
+ throw this.fmtError(
1003
1007
  `Bad path: ${absPath} No such file`,
1004
1008
  'rmdir',
1005
1009
  errorCode.badPath
@@ -1008,10 +1012,10 @@ class SftpClient {
1008
1012
  return await _dormdir(absPath, recursive);
1009
1013
  }
1010
1014
  } catch (err) {
1011
- this._resetEventFlags();
1012
- throw err.custom ? err : fmtError(err, 'rmdir', err.code);
1015
+ throw err.custom ? err : this.fmtError(err.message, 'rmdir', err.code);
1013
1016
  } finally {
1014
1017
  removeTempListeners(this, listeners, 'rmdir');
1018
+ this._resetEventFlags();
1015
1019
  }
1016
1020
  }
1017
1021
 
@@ -1026,31 +1030,35 @@ class SftpClient {
1026
1030
  * @return {Promise<String>} with string 'Successfully deleted file' once resolved
1027
1031
  *
1028
1032
  */
1029
- delete(remotePath, notFoundOK = false) {
1030
- let listeners;
1033
+ _delete(rPath, notFoundOK) {
1031
1034
  return new Promise((resolve, reject) => {
1032
- listeners = addTempListeners(this, 'delete', reject);
1033
- if (haveConnection(this, 'delete', reject)) {
1034
- this.debugMsg(`delete -> ${remotePath}`);
1035
- this.sftp.unlink(remotePath, (err) => {
1036
- if (err) {
1037
- this.debugMsg(`delete error ${err.message} code: ${err.code}`);
1038
- if (notFoundOK && err.code === 2) {
1039
- this.debugMsg('delete ignore missing target error');
1040
- resolve(`Successfully deleted ${remotePath}`);
1041
- } else {
1042
- reject(
1043
- fmtError(`${err.message} ${remotePath}`, 'delete', err.code)
1044
- );
1045
- }
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
+ );
1046
1043
  }
1047
- resolve(`Successfully deleted ${remotePath}`);
1048
- });
1049
- }
1050
- }).finally(() => {
1044
+ }
1045
+ resolve(`Successfully deleted ${rPath}`);
1046
+ });
1047
+ });
1048
+ }
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 {
1051
1059
  removeTempListeners(this, listeners, 'delete');
1052
1060
  this._resetEventFlags();
1053
- });
1061
+ }
1054
1062
  }
1055
1063
 
1056
1064
  /**
@@ -1064,30 +1072,41 @@ class SftpClient {
1064
1072
  * @return {Promise<String>}
1065
1073
  *
1066
1074
  */
1067
- rename(fromPath, toPath) {
1068
- let listeners;
1075
+ _rename(fPath, tPath) {
1069
1076
  return new Promise((resolve, reject) => {
1070
- listeners = addTempListeners(this, 'rename', reject);
1071
- if (haveConnection(this, 'rename', reject)) {
1072
- this.debugMsg(`rename -> ${fromPath} ${toPath}`);
1073
- this.sftp.rename(fromPath, toPath, (err) => {
1074
- if (err) {
1075
- this.debugMsg(`rename error ${err.message} code: ${err.code}`);
1076
- reject(
1077
- fmtError(
1078
- `${err.message} From: ${fromPath} To: ${toPath}`,
1079
- 'rename',
1080
- err.code
1081
- )
1082
- );
1083
- }
1084
- resolve(`Successfully renamed ${fromPath} to ${toPath}`);
1085
- });
1086
- }
1087
- }).finally(() => {
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
+ });
1089
+ });
1090
+ }
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 {
1088
1107
  removeTempListeners(this, listeners, 'rename');
1089
1108
  this._resetEventFlags();
1090
- });
1109
+ }
1091
1110
  }
1092
1111
 
1093
1112
  /**
@@ -1102,30 +1121,41 @@ class SftpClient {
1102
1121
  * @return {Promise<String>}
1103
1122
  *
1104
1123
  */
1105
- posixRename(fromPath, toPath) {
1106
- let listeners;
1124
+ _posixRename(fPath, tPath) {
1107
1125
  return new Promise((resolve, reject) => {
1108
- listeners = addTempListeners(this, 'posixRename', reject);
1109
- if (haveConnection(this, 'posixRename', reject)) {
1110
- this.debugMsg(`posixRename -> ${fromPath} ${toPath}`);
1111
- this.sftp.ext_openssh_rename(fromPath, toPath, (err) => {
1112
- if (err) {
1113
- this.debugMsg(`posixRename error ${err.message} code: ${err.code}`);
1114
- reject(
1115
- fmtError(
1116
- `${err.message} From: ${fromPath} To: ${toPath}`,
1117
- 'posixRename',
1118
- err.code
1119
- )
1120
- );
1121
- }
1122
- resolve(`Successful POSIX rename ${fromPath} to ${toPath}`);
1123
- });
1124
- }
1125
- }).finally(() => {
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
+ });
1138
+ });
1139
+ }
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 {
1126
1156
  removeTempListeners(this, listeners, 'posixRename');
1127
1157
  this._resetEventFlags();
1128
- });
1158
+ }
1129
1159
  }
1130
1160
 
1131
1161
  /**
@@ -1138,21 +1168,31 @@ class SftpClient {
1138
1168
  *
1139
1169
  * @return {Promise<String>}
1140
1170
  */
1141
- chmod(remotePath, mode) {
1142
- let listeners;
1171
+ _chmod(rPath, mode) {
1143
1172
  return new Promise((resolve, reject) => {
1144
- listeners = addTempListeners(this, 'chmod', reject);
1145
- this.debugMsg(`chmod -> ${remotePath} ${mode}`);
1146
- this.sftp.chmod(remotePath, mode, (err) => {
1173
+ this.sftp.chmod(rPath, mode, (err) => {
1147
1174
  if (err) {
1148
- reject(fmtError(`${err.message} ${remotePath}`, 'chmod', err.code));
1175
+ reject(this.fmtError(`${err.message} ${rPath}`, '_chmod', err.code));
1149
1176
  }
1150
1177
  resolve('Successfully change file mode');
1151
1178
  });
1152
- }).finally(() => {
1179
+ });
1180
+ }
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 {
1153
1193
  removeTempListeners(this, listeners, 'chmod');
1154
1194
  this._resetEventFlags();
1155
- });
1195
+ }
1156
1196
  }
1157
1197
 
1158
1198
  /**
@@ -1163,72 +1203,95 @@ class SftpClient {
1163
1203
  * server.
1164
1204
  * @param {String} srcDir - local source directory
1165
1205
  * @param {String} dstDir - remote destination directory
1166
- * @param {function(String,Boolean):Boolean} filter - (Optional) The first argument is the full path of the item to be uploaded and the second argument is a boolean, which will be true if the target path is for a directory. If the function returns true, the item will be uploaded
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
+ *
1167
1214
  * @returns {Promise<String>}
1168
1215
  */
1169
- async uploadDir(srcDir, dstDir, filter) {
1216
+ async _uploadDir(srcDir, dstDir, options) {
1170
1217
  try {
1171
- this.debugMsg(`uploadDir -> SRC = ${srcDir} DST = ${dstDir}`);
1172
- haveConnection(this, 'uploadDir');
1173
- //let absSrcDir = fs.realpathSync(srcDir);
1174
- let absDstDir = await normalizeRemotePath(this, dstDir);
1175
-
1218
+ const absDstDir = await normalizeRemotePath(this, dstDir);
1176
1219
  this.debugMsg(`uploadDir <- SRC = ${srcDir} DST = ${absDstDir}`);
1177
1220
  const srcType = localExists(srcDir);
1178
1221
  if (!srcType) {
1179
- throw fmtError(
1222
+ throw this.fmtError(
1180
1223
  `Bad path: ${srcDir} not exist`,
1181
- 'uploadDir',
1224
+ '_uploadDir',
1182
1225
  errorCode.badPath
1183
1226
  );
1184
1227
  }
1185
1228
  if (srcType !== 'd') {
1186
- throw fmtError(
1229
+ throw this.fmtError(
1187
1230
  `Bad path: ${srcDir}: not a directory`,
1188
- 'uploadDir',
1231
+ '_uploadDir',
1189
1232
  errorCode.badPath
1190
1233
  );
1191
1234
  }
1192
- let dstStatus = await this.exists(absDstDir);
1235
+ const dstStatus = await this.exists(absDstDir);
1193
1236
  if (dstStatus && dstStatus !== 'd') {
1194
- this.debugMsg(`UploadDir: DST ${absDstDir} exists but not a directory`);
1195
- throw fmtError(
1237
+ throw this.fmtError(
1196
1238
  `Bad path ${absDstDir} Not a directory`,
1197
- 'uploadDir',
1239
+ '_uploadDir',
1198
1240
  errorCode.badPath
1199
1241
  );
1200
1242
  }
1201
1243
  if (!dstStatus) {
1202
- this.debugMsg(`UploadDir: Creating directory ${absDstDir}`);
1203
- await this.mkdir(absDstDir, true);
1244
+ await this._mkdir(absDstDir, true);
1204
1245
  }
1205
1246
  let dirEntries = fs.readdirSync(srcDir, {
1206
1247
  encoding: 'utf8',
1207
1248
  withFileTypes: true,
1208
1249
  });
1209
- if (filter) {
1250
+ if (options?.filter) {
1210
1251
  dirEntries = dirEntries.filter((item) =>
1211
- filter(join(srcDir, item.name), item.isDirectory())
1252
+ options.filter(join(srcDir, item.name), item.isDirectory())
1212
1253
  );
1213
1254
  }
1214
- for (let e of dirEntries) {
1215
- let newSrc = join(srcDir, e.name);
1216
- let newDst = `${absDstDir}${this.remotePathSep}${e.name}`;
1255
+ let fileUploads = [];
1256
+ for (const e of dirEntries) {
1257
+ const newSrc = join(srcDir, e.name);
1258
+ const newDst = `${absDstDir}${this.remotePathSep}${e.name}`;
1217
1259
  if (e.isDirectory()) {
1218
- await this.uploadDir(newSrc, newDst, filter);
1260
+ await this.uploadDir(newSrc, newDst, options);
1219
1261
  } else if (e.isFile()) {
1220
- await this.put(newSrc, newDst);
1262
+ if (options?.useFastput) {
1263
+ fileUploads.push(this._fastPut(newSrc, newDst));
1264
+ } else {
1265
+ fileUploads.push(this._put(newSrc, newDst));
1266
+ }
1221
1267
  this.client.emit('upload', { source: newSrc, destination: newDst });
1222
1268
  } else {
1223
1269
  this.debugMsg(
1224
1270
  `uploadDir: File ignored: ${e.name} not a regular file`
1225
1271
  );
1226
1272
  }
1273
+ await Promise.all(fileUploads);
1227
1274
  }
1228
1275
  return `${srcDir} uploaded to ${absDstDir}`;
1229
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);
1290
+ } catch (err) {
1291
+ throw err.custom ? err : this.fmtError(err, 'uploadDir');
1292
+ } finally {
1293
+ removeTempListeners(this, listeners, 'chmod');
1230
1294
  this._resetEventFlags();
1231
- throw err.custom ? err : fmtError(err, 'uploadDir');
1232
1295
  }
1233
1296
  }
1234
1297
 
@@ -1240,17 +1303,21 @@ class SftpClient {
1240
1303
  * file system.
1241
1304
  * @param {String} srcDir - remote source directory
1242
1305
  * @param {String} dstDir - local destination directory
1243
- * @param {function(String,Boolean):Boolean} filter - (Optional) The first argument is the full path of the item to be downloaded and the second argument is a boolean, which will be true if the target path is for a directory. If the function returns true, the item will be downloaded
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
+ *
1244
1313
  * @returns {Promise<String>}
1245
1314
  */
1246
- async downloadDir(srcDir, dstDir, filter) {
1315
+ async _downloadDir(srcDir, dstDir, options) {
1247
1316
  try {
1248
- this.debugMsg(`downloadDir -> ${srcDir} ${dstDir}`);
1249
- haveConnection(this, 'downloadDir');
1250
- let fileList = await this.list(srcDir);
1251
- if (filter) {
1317
+ let fileList = await this._list(srcDir);
1318
+ if (options?.filter) {
1252
1319
  fileList = fileList.filter((item) =>
1253
- filter(
1320
+ options.filter(
1254
1321
  `${srcDir}${this.remotePathSep}${item.name}`,
1255
1322
  item.type === 'd' ? true : false
1256
1323
  )
@@ -1258,7 +1325,7 @@ class SftpClient {
1258
1325
  }
1259
1326
  const localCheck = haveLocalCreate(dstDir);
1260
1327
  if (!localCheck.status && localCheck.details === 'permission denied') {
1261
- throw fmtError(
1328
+ throw this.fmtError(
1262
1329
  `Bad path: ${dstDir}: ${localCheck.details}`,
1263
1330
  'downloadDir',
1264
1331
  localCheck.code
@@ -1266,19 +1333,24 @@ class SftpClient {
1266
1333
  } else if (localCheck.status && !localCheck.type) {
1267
1334
  fs.mkdirSync(dstDir, { recursive: true });
1268
1335
  } else if (localCheck.status && localCheck.type !== 'd') {
1269
- throw fmtError(
1336
+ throw this.fmtError(
1270
1337
  `Bad path: ${dstDir}: not a directory`,
1271
1338
  'downloadDir',
1272
1339
  errorCode.badPath
1273
1340
  );
1274
1341
  }
1275
- for (let f of fileList) {
1276
- let newSrc = `${srcDir}${this.remotePathSep}${f.name}`;
1277
- let newDst = join(dstDir, f.name);
1342
+ let downloadFiles = [];
1343
+ for (const f of fileList) {
1344
+ const newSrc = `${srcDir}${this.remotePathSep}${f.name}`;
1345
+ const newDst = join(dstDir, f.name);
1278
1346
  if (f.type === 'd') {
1279
- await this.downloadDir(newSrc, newDst, filter);
1347
+ await this._downloadDir(newSrc, newDst, options);
1280
1348
  } else if (f.type === '-') {
1281
- await this.get(newSrc, newDst);
1349
+ if (options?.useFasget) {
1350
+ downloadFiles.push(this._fastGet(newSrc, newDst));
1351
+ } else {
1352
+ downloadFiles.push(this._get(newSrc, newDst));
1353
+ }
1282
1354
  this.client.emit('download', { source: newSrc, destination: newDst });
1283
1355
  } else {
1284
1356
  this.debugMsg(
@@ -1286,13 +1358,157 @@ class SftpClient {
1286
1358
  );
1287
1359
  }
1288
1360
  }
1361
+ await Promise.all(downloadFiles);
1289
1362
  return `${srcDir} downloaded to ${dstDir}`;
1290
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');
1291
1440
  this._resetEventFlags();
1292
- throw err.custom ? err : fmtError(err, 'downloadDir', err.code);
1293
1441
  }
1294
1442
  }
1295
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
+ }
1296
1512
  /**
1297
1513
  * @async
1298
1514
  *
@@ -1312,11 +1528,9 @@ class SftpClient {
1312
1528
  };
1313
1529
  this.on('close', endCloseHandler);
1314
1530
  if (haveConnection(this, 'end', reject)) {
1315
- this.debugMsg('end: Have connection - calling end()');
1316
1531
  this.client.end();
1317
1532
  }
1318
1533
  }).finally(() => {
1319
- this.debugMsg('end: finally clause fired');
1320
1534
  removeTempListeners(this, listeners, 'end');
1321
1535
  this.removeListener('close', endCloseHandler);
1322
1536
  this.endCalled = false;