ssh2-sftp-client 8.0.0 → 9.0.1

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';
411
+ }
412
+ if (info.isFile) {
413
+ this.debugMsg(`exists: ${rPath} = -`);
414
+ return '-';
380
415
  }
381
- this.debugMsg(`exists: default ${remotePath} = false`);
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,59 +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
- };
432
- });
433
- }
434
- // provide some compatibility for auxList
435
- let regex;
436
- if (pattern instanceof RegExp) {
437
- regex = pattern;
438
- } else {
439
- let newPattern = pattern.replace(/\*([^*])*?/gi, '.*');
440
- regex = new RegExp(newPattern);
441
- }
442
- let filteredList = newList.filter((item) => regex.test(item.name));
443
- this.debugMsg('list: result: ', filteredList);
444
- resolve(filteredList);
486
+ resolve(newList);
445
487
  }
446
- });
447
- }
448
- }).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 {
449
504
  removeTempListeners(this, listeners, 'list');
450
505
  this._resetEventFlags();
451
- });
506
+ }
452
507
  }
453
508
 
454
509
  /**
@@ -464,110 +519,87 @@ class SftpClient {
464
519
  * @param {Object} options - options object with supported properties of readStreamOptions,
465
520
  * writeStreamOptions and pipeOptions.
466
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
+ *
467
526
  * @return {Promise<String|Stream|Buffer>}
468
527
  */
469
- get(
470
- remotePath,
471
- dst,
472
- options = { readStreamOptions: {}, writeStreamOptions: {}, pipeOptions: {} }
473
- ) {
474
- let rdr, wtr, listeners;
528
+ _get(rPath, dst, opts) {
529
+ let rdr, wtr;
475
530
  return new Promise((resolve, reject) => {
476
- listeners = addTempListeners(this, 'get', reject);
477
- if (haveConnection(this, 'get', reject)) {
478
- this.debugMsg(`get -> ${remotePath} `, options);
479
- rdr = this.sftp.createReadStream(
480
- remotePath,
481
- options.readStreamOptions ? options.readStreamOptions : {}
482
- );
483
- rdr.once('error', (err) => {
484
- 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 resolving buffer of data');
544
+ wtr = concat((buff) => {
545
+ resolve(buff);
485
546
  });
486
- if (dst === undefined) {
487
- // no dst specified, return buffer of data
488
- this.debugMsg('get returning buffer of data');
489
- wtr = concat((buff) => {
490
- //rdr.removeAllListeners('error');
491
- resolve(buff);
492
- });
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;
493
560
  } else {
494
- if (typeof dst === 'string') {
495
- // dst local file path
496
- this.debugMsg('get returning local file');
497
- const localCheck = haveLocalCreate(dst);
498
- if (!localCheck.status) {
499
- return reject(
500
- fmtError(
501
- `Bad path: ${dst}: ${localCheck.details}`,
502
- 'get',
503
- localCheck.code
504
- )
505
- );
506
- }
507
- wtr = fs.createWriteStream(
508
- dst,
509
- options.writeStreamOptions ? options.writeStreamOptions : {}
510
- );
511
- } else {
512
- this.debugMsg('get returning data into supplied stream');
513
- wtr = dst;
514
- }
515
- wtr.once('error', (err) => {
516
- reject(
517
- fmtError(
518
- `${err.message} ${typeof dst === 'string' ? dst : ''}`,
519
- 'get',
520
- err.code
521
- )
522
- );
523
- });
524
- if (
525
- Object.hasOwnProperty.call(options, 'pipeOptions') &&
526
- Object.hasOwnProperty.call(options.pipeOptions, 'end') &&
527
- !options.pipeOptions.end
528
- ) {
529
- rdr.once('end', () => {
530
- this.debugMsg('get resolved on reader end event');
531
- if (typeof dst === 'string') {
532
- resolve(dst);
533
- } else {
534
- resolve(wtr);
535
- }
536
- });
537
- } else {
538
- wtr.once('finish', () => {
539
- this.debugMsg('get resolved on writer finish event');
540
- if (typeof dst === 'string') {
541
- resolve(dst);
542
- } else {
543
- resolve(wtr);
544
- }
545
- });
546
- }
561
+ wtr = fs.createWriteStream(dst, opts.writeStreamOptions);
547
562
  }
548
- rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
563
+ } else {
564
+ this.debugMsg('get: returning data into supplied stream');
565
+ wtr = dst;
549
566
  }
550
- }).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
+ this.debugMsg('get: resolving with dst filename');
579
+ resolve(dst);
580
+ } else if (dst !== undefined) {
581
+ this.debugMsg('get: resolving with writer stream object');
582
+ resolve(wtr);
583
+ }
584
+ });
585
+ rdr.pipe(wtr, opts.pipeOptions);
586
+ });
587
+ }
588
+
589
+ async get(remotePath, dst, options) {
590
+ let listeners;
591
+ try {
592
+ listeners = addTempListeners(this, 'get');
593
+ haveConnection(this, 'get');
594
+ return await this._get(remotePath, dst, options);
595
+ } catch (e) {
596
+ throw e.custom
597
+ ? e
598
+ : this.fmtError(`${e.message} ${remotePath}`, 'get', e.code);
599
+ } finally {
551
600
  removeTempListeners(this, listeners, 'get');
552
601
  this._resetEventFlags();
553
- if (
554
- rdr &&
555
- Object.hasOwnProperty.call(options, 'readStreamOptions') &&
556
- Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
557
- options.readStreamOptions.autoClose === false
558
- ) {
559
- rdr.destroy();
560
- }
561
- if (
562
- wtr &&
563
- Object.hasOwnProperty.call(options, 'writeStreamOptions') &&
564
- Object.hasOwnProperty.call(options.writeStreamOptions, 'autoClose') &&
565
- options.writeStreamOptions.autoClose === false &&
566
- typeof dst === 'string'
567
- ) {
568
- wtr.destroy();
569
- }
570
- });
602
+ }
571
603
  }
572
604
 
573
605
  /**
@@ -580,47 +612,45 @@ class SftpClient {
580
612
  * @param {Object} options
581
613
  * @return {Promise<String>} the result of downloading the file
582
614
  */
615
+ _fastGet(rPath, lPath, opts) {
616
+ return new Promise((resolve, reject) => {
617
+ this.sftp.fastGet(rPath, lPath, opts, (err) => {
618
+ if (err) {
619
+ reject(
620
+ this.fmtError(`${err.message} Remote: ${rPath} Local: ${lPath}`)
621
+ );
622
+ }
623
+ resolve(`${rPath} was successfully download to ${lPath}!`);
624
+ });
625
+ });
626
+ }
627
+
583
628
  async fastGet(remotePath, localPath, options) {
629
+ let listeners;
584
630
  try {
631
+ listeners = addTempListeners(this, 'fastGet');
632
+ haveConnection(this, 'fastGet');
585
633
  const ftype = await this.exists(remotePath);
586
634
  if (ftype !== '-') {
587
- const msg =
588
- ftype === false
589
- ? `No such file ${remotePath}`
590
- : `Not a regular file ${remotePath}`;
591
- let err = new Error(msg);
592
- err.code = errorCode.badPath;
593
- throw err;
635
+ const msg = `${
636
+ !ftype ? 'No such file ' : 'Not a regular file'
637
+ } ${remotePath}`;
638
+ throw this.fmtError(msg, 'fastGet', errorCode.badPath);
594
639
  }
595
640
  const localCheck = haveLocalCreate(localPath);
596
641
  if (!localCheck.status) {
597
- let err = new Error(`Bad path: ${localPath}: ${localCheck.details}`);
598
- err.code = errorCode.badPath;
599
- throw err;
642
+ throw this.fmtError(
643
+ `Bad path: ${localPath}: ${localCheck.details}`,
644
+ 'fastGet',
645
+ errorCode.badPath
646
+ );
600
647
  }
601
- let listeners;
602
- let rslt = await new Promise((resolve, reject) => {
603
- listeners = addTempListeners(this, 'fastGet', reject);
604
- if (haveConnection(this, 'fastGet', reject)) {
605
- this.debugMsg(
606
- `fastGet -> remote: ${remotePath} local: ${localPath} `,
607
- options
608
- );
609
- this.sftp.fastGet(remotePath, localPath, options, (err) => {
610
- if (err) {
611
- this.debugMsg(`fastGet error ${err.message} code: ${err.code}`);
612
- reject(err);
613
- }
614
- resolve(`${remotePath} was successfully download to ${localPath}!`);
615
- });
616
- }
617
- }).finally(() => {
618
- removeTempListeners(this, listeners, 'fastGet');
619
- });
620
- return rslt;
648
+ return await this._fastGet(remotePath, localPath, options);
621
649
  } catch (err) {
650
+ throw this.fmtError(err, 'fastGet');
651
+ } finally {
652
+ removeTempListeners(this, listeners, 'fastGet');
622
653
  this._resetEventFlags();
623
- throw fmtError(err, 'fastGet');
624
654
  }
625
655
  }
626
656
 
@@ -637,53 +667,50 @@ class SftpClient {
637
667
  * @param {Object} options
638
668
  * @return {Promise<String>} the result of downloading the file
639
669
  */
640
- fastPut(localPath, remotePath, options) {
641
- let listeners;
642
- this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
670
+ _fastPut(lPath, rPath, opts) {
643
671
  return new Promise((resolve, reject) => {
644
- listeners = addTempListeners(this, 'fastPut', reject);
672
+ this.sftp.fastPut(lPath, rPath, opts, (err) => {
673
+ if (err) {
674
+ reject(
675
+ this.fmtError(
676
+ `${err.message} Local: ${lPath} Remote: ${rPath}`,
677
+ 'fastPut',
678
+ err.code
679
+ )
680
+ );
681
+ }
682
+ resolve(`${lPath} was successfully uploaded to ${rPath}!`);
683
+ });
684
+ });
685
+ }
686
+
687
+ async fastPut(localPath, remotePath, options) {
688
+ let listeners;
689
+ try {
690
+ listeners = addTempListeners(this, 'fastPut');
691
+ this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
692
+ haveConnection(this, 'fastPut');
645
693
  const localCheck = haveLocalAccess(localPath);
646
694
  if (!localCheck.status) {
647
- reject(
648
- fmtError(
649
- `Bad path: ${localPath}: ${localCheck.details}`,
650
- 'fastPut',
651
- localCheck.code
652
- )
695
+ throw this.fmtError(
696
+ `Bad path: ${localPath}: ${localCheck.details}`,
697
+ 'fastPut',
698
+ localCheck.code
653
699
  );
654
700
  } else if (localCheck.status && localExists(localPath) === 'd') {
655
- reject(
656
- fmtError(
657
- `Bad path: ${localPath} not a regular file`,
658
- 'fastPut',
659
- errorCode.badPath
660
- )
661
- );
662
- } else if (haveConnection(this, 'fastPut', reject)) {
663
- this.debugMsg(
664
- `fastPut -> local: ${localPath} remote: ${remotePath} opts: ${JSON.stringify(
665
- options
666
- )}`
701
+ throw this.fmtError(
702
+ `Bad path: ${localPath} not a regular file`,
703
+ 'fastgPut',
704
+ errorCode.badPath
667
705
  );
668
- this.sftp.fastPut(localPath, remotePath, options, (err) => {
669
- if (err) {
670
- this.debugMsg(`fastPut error ${err.message} ${err.code}`);
671
- reject(
672
- fmtError(
673
- `${err.message} Local: ${localPath} Remote: ${remotePath}`,
674
- 'fastPut',
675
- err.code
676
- )
677
- );
678
- }
679
- this.debugMsg('fastPut file transferred');
680
- resolve(`${localPath} was successfully uploaded to ${remotePath}!`);
681
- });
682
706
  }
683
- }).finally(() => {
707
+ return await this._fastPut(localPath, remotePath, options);
708
+ } catch (e) {
709
+ throw e.custom ? e : this.fmtError(e.message, 'fastPut', e.code);
710
+ } finally {
684
711
  removeTempListeners(this, listeners, 'fastPut');
685
712
  this._resetEventFlags();
686
- });
713
+ }
687
714
  }
688
715
 
689
716
  /**
@@ -696,90 +723,75 @@ class SftpClient {
696
723
  * @param {Object} options - options used for read, write stream and pipe configuration
697
724
  * value supported by node. Allowed properties are readStreamOptions,
698
725
  * writeStreamOptions and pipeOptions.
726
+ *
727
+ * *Important Note*: The ability to set ''autoClose' on read/write streams and 'end' on pipe() calls
728
+ * is no longer supported. New methods 'createReadStream()' and 'createWriteStream()' have been
729
+ * added to support low-level access to stream objects.
730
+ *
699
731
  * @return {Promise<String>}
700
732
  */
701
- put(
702
- localSrc,
703
- remotePath,
704
- options = {
705
- readStreamOptions: {},
706
- writeStreamOptions: { autoClose: true },
707
- pipeOptions: {},
708
- }
709
- ) {
710
- let wtr, rdr, listeners;
733
+ _put(lPath, rPath, opts) {
734
+ let wtr, rdr;
711
735
  return new Promise((resolve, reject) => {
712
- listeners = addTempListeners(this, 'put', reject);
736
+ opts = {
737
+ ...opts,
738
+ readStreamOptions: { autoClose: true },
739
+ writeStreamOptions: { autoClose: true },
740
+ pipeOptions: { end: true },
741
+ };
742
+ wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
743
+ wtr.once('error', (err) => {
744
+ reject(this.fmtError(`${err.message} ${rPath}`, 'put', err.code));
745
+ });
746
+ wtr.once('close', () => {
747
+ resolve(`Uploaded data stream to ${rPath}`);
748
+ });
749
+ if (lPath instanceof Buffer) {
750
+ this.debugMsg('put source is a buffer');
751
+ wtr.end(lPath);
752
+ } else {
753
+ rdr =
754
+ typeof lPath === 'string'
755
+ ? fs.createReadStream(lPath, opts.readStreamOptions)
756
+ : lPath;
757
+ rdr.once('error', (err) => {
758
+ reject(
759
+ this.fmtError(
760
+ `${err.message} ${
761
+ typeof lPath === 'string' ? lPath : '<stream>'
762
+ }`,
763
+ '_put',
764
+ err.code
765
+ )
766
+ );
767
+ });
768
+ rdr.pipe(wtr, opts.pipeOptions);
769
+ }
770
+ });
771
+ }
772
+
773
+ async put(localSrc, remotePath, options) {
774
+ let listeners;
775
+ try {
776
+ listeners = addTempListeners(this, 'put');
777
+ haveConnection(this, 'put');
713
778
  if (typeof localSrc === 'string') {
714
779
  const localCheck = haveLocalAccess(localSrc);
715
780
  if (!localCheck.status) {
716
- this.debugMsg(`put: local source check error ${localCheck.details}`);
717
- return reject(
718
- fmtError(
719
- `Bad path: ${localSrc}: ${localCheck.details}`,
720
- 'put',
721
- localCheck.code
722
- )
781
+ throw this.fmtError(
782
+ `Bad path: ${localSrc} ${localCheck.details}`,
783
+ 'put',
784
+ localCheck.code
723
785
  );
724
786
  }
725
787
  }
726
- if (haveConnection(this, 'put')) {
727
- wtr = this.sftp.createWriteStream(
728
- remotePath,
729
- options.writeStreamOptions
730
- ? { ...options.writeStreamOptions, autoClose: true }
731
- : {}
732
- );
733
- wtr.once('error', (err) => {
734
- this.debugMsg(`put: write stream error ${err.message}`);
735
- reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
736
- });
737
- wtr.once('close', () => {
738
- this.debugMsg('put: promise resolved');
739
- resolve(`Uploaded data stream to ${remotePath}`);
740
- });
741
- if (localSrc instanceof Buffer) {
742
- this.debugMsg('put source is a buffer');
743
- wtr.end(localSrc);
744
- } else {
745
- if (typeof localSrc === 'string') {
746
- this.debugMsg(`put source is a file path: ${localSrc}`);
747
- rdr = fs.createReadStream(
748
- localSrc,
749
- options.readStreamOptions ? options.readStreamOptions : {}
750
- );
751
- } else {
752
- this.debugMsg('put source is a stream');
753
- rdr = localSrc;
754
- }
755
- rdr.once('error', (err) => {
756
- this.debugMsg(`put: read stream error ${err.message}`);
757
- reject(
758
- fmtError(
759
- `${err.message} ${
760
- typeof localSrc === 'string' ? localSrc : ''
761
- }`,
762
- 'put',
763
- err.code
764
- )
765
- );
766
- });
767
- rdr.pipe(wtr, options.pipeOptions ? options.pipeOptions : {});
768
- }
769
- }
770
- }).finally(() => {
788
+ return await this._put(localSrc, remotePath, options);
789
+ } catch (e) {
790
+ throw e.custom ? e : this.fmtError(e.message, 'put', e.code);
791
+ } finally {
771
792
  removeTempListeners(this, listeners, 'put');
772
793
  this._resetEventFlags();
773
- if (
774
- rdr &&
775
- Object.hasOwnProperty.call(options, 'readStreamOptions') &&
776
- Object.hasOwnProperty.call(options.readStreamOptions, 'autoClose') &&
777
- options.readStreamOptions.autoClose === false &&
778
- typeof localSrc === 'string'
779
- ) {
780
- rdr.destroy();
781
- }
782
- });
794
+ }
783
795
  }
784
796
 
785
797
  /**
@@ -790,45 +802,53 @@ class SftpClient {
790
802
  * @param {Object} options
791
803
  * @return {Promise<String>}
792
804
  */
805
+ _append(input, rPath, opts) {
806
+ return new Promise((resolve, reject) => {
807
+ this.debugMsg(`append -> remote: ${rPath} `, opts);
808
+ opts.flags = 'a';
809
+ const stream = this.sftp.createWriteStream(rPath, opts);
810
+ stream.on('error', (err) => {
811
+ reject(this.fmtError(`${err.message} ${rPath}`, 'append', err.code));
812
+ });
813
+ stream.on('close', () => {
814
+ resolve(`Appended data to ${rPath}`);
815
+ });
816
+ if (input instanceof Buffer) {
817
+ stream.write(input);
818
+ stream.end();
819
+ } else {
820
+ input.pipe(stream);
821
+ }
822
+ });
823
+ }
824
+
793
825
  async append(input, remotePath, options = {}) {
794
- const fileType = await this.exists(remotePath);
795
- if (fileType && fileType === 'd') {
796
- throw fmtError(
797
- `Bad path: ${remotePath}: cannot append to a directory`,
798
- 'append',
799
- errorCode.badPath
800
- );
801
- }
802
826
  let listeners;
803
- return await new Promise((resolve, reject) => {
804
- listeners = addTempListeners(this, 'append', reject);
805
- if (haveConnection(this, 'append', reject)) {
806
- if (typeof input === 'string') {
807
- reject(fmtError('Cannot append one file to another', 'append'));
808
- } else {
809
- this.debugMsg(`append -> remote: ${remotePath} `, options);
810
- options.flags = 'a';
811
- let stream = this.sftp.createWriteStream(remotePath, options);
812
- stream.on('error', (err_1) => {
813
- reject(
814
- fmtError(`${err_1.message} ${remotePath}`, 'append', err_1.code)
815
- );
816
- });
817
- stream.on('finish', () => {
818
- resolve(`Appended data to ${remotePath}`);
819
- });
820
- if (input instanceof Buffer) {
821
- stream.write(input);
822
- stream.end();
823
- } else {
824
- input.pipe(stream);
825
- }
826
- }
827
+ try {
828
+ listeners = addTempListeners(this, 'append');
829
+ if (typeof input === 'string') {
830
+ throw this.fmtError(
831
+ 'Cannot append one file to another',
832
+ 'append',
833
+ errorCode.badPath
834
+ );
827
835
  }
828
- }).finally(() => {
836
+ haveConnection(this, 'append');
837
+ const fileType = await this.exists(remotePath);
838
+ if (fileType && fileType === 'd') {
839
+ throw this.fmtError(
840
+ `Bad path: ${remotePath}: cannot append to a directory`,
841
+ 'append',
842
+ errorCode.badPath
843
+ );
844
+ }
845
+ await this._append(input, remotePath, options);
846
+ } catch (e) {
847
+ throw e.custom ? e : this.fmtError(e.message, 'append', e.code);
848
+ } finally {
829
849
  removeTempListeners(this, listeners, 'append');
830
850
  this._resetEventFlags();
831
- });
851
+ }
832
852
  }
833
853
 
834
854
  /**
@@ -840,68 +860,85 @@ class SftpClient {
840
860
  * @param {boolean} recursive - if true, recursively create directories
841
861
  * @return {Promise<String>}
842
862
  */
843
- async mkdir(remotePath, recursive = false) {
844
- const _mkdir = (p) => {
845
- let listeners;
846
- return new Promise((resolve, reject) => {
847
- listeners = addTempListeners(this, '_mkdir', reject);
848
- this.debugMsg(`_mkdir: create ${p}`);
849
- this.sftp.mkdir(p, (err) => {
850
- if (err) {
851
- this.debugMsg(`_mkdir: Error ${err.message} code: ${err.code}`);
852
- if (err.code === 4) {
853
- //fix for windows dodgy error messages
854
- let error = new Error(`Bad path: ${p} permission denied`);
855
- error.code = errorCode.badPath;
856
- reject(error);
857
- } else if (err.code === 2) {
858
- let error = new Error(
859
- `Bad path: ${p} parent not a directory or not exist`
860
- );
861
- error.code = errorCode.badPath;
862
- reject(error);
863
- } else {
864
- reject(err);
865
- }
863
+ _doMkdir(p) {
864
+ return new Promise((resolve, reject) => {
865
+ this.sftp.mkdir(p, (err) => {
866
+ if (err) {
867
+ if (err.code === 4) {
868
+ //fix for windows dodgy error messages
869
+ reject(
870
+ this.fmtError(
871
+ `Bad path: ${p} permission denied`,
872
+ '_doMkdir',
873
+ errorCode.badPath
874
+ )
875
+ );
876
+ } else if (err.code === 2) {
877
+ reject(
878
+ this.fmtError(
879
+ `Bad path: ${p} parent not a directory or not exist`,
880
+ '_doMkdir',
881
+ errorCode.badPath
882
+ )
883
+ );
866
884
  } else {
867
- this.debugMsg('_mkdir: directory created');
868
- resolve(`${p} directory created`);
885
+ reject(this.fmtError(`${err.message} ${p}`, '_doMkdir', err.code));
869
886
  }
870
- });
871
- }).finally(() => {
872
- removeTempListeners(this, listeners, '_mkdir');
873
- this._resetEventFlags();
887
+ } else {
888
+ resolve(`${p} directory created`);
889
+ }
874
890
  });
875
- };
891
+ });
892
+ }
876
893
 
894
+ async _mkdir(remotePath, recursive) {
877
895
  try {
878
- haveConnection(this, 'mkdir');
879
- let rPath = await normalizeRemotePath(this, remotePath);
880
- let targetExists = await this.exists(rPath);
896
+ const rPath = await normalizeRemotePath(this, remotePath);
897
+ const targetExists = await this.exists(rPath);
881
898
  if (targetExists && targetExists !== 'd') {
882
- let error = new Error(`Bad path: ${rPath} already exists as a file`);
883
- error.code = errorCode.badPath;
884
- throw error;
899
+ throw this.fmtError(
900
+ `Bad path: ${rPath} already exists as a file`,
901
+ '_mkdir',
902
+ errorCode.badPath
903
+ );
885
904
  } else if (targetExists) {
886
905
  return `${rPath} already exists`;
887
906
  }
888
907
  if (!recursive) {
889
- return await _mkdir(rPath);
908
+ return await this._doMkdir(rPath);
890
909
  }
891
- let dir = parse(rPath).dir;
910
+ const dir = parse(rPath).dir;
892
911
  if (dir) {
893
- let dirExists = await this.exists(dir);
912
+ const dirExists = await this.exists(dir);
894
913
  if (!dirExists) {
895
- await this.mkdir(dir, true);
914
+ await this._mkdir(dir, true);
896
915
  } else if (dirExists !== 'd') {
897
- let error = new Error(`Bad path: ${dir} not a directory`);
898
- error.code = errorCode.badPath;
899
- throw error;
916
+ throw this.fmtError(
917
+ `Bad path: ${dir} not a directory`,
918
+ '_mkdir',
919
+ errorCode.badPath
920
+ );
900
921
  }
901
922
  }
902
- return await _mkdir(rPath);
923
+ return await this._doMkdir(rPath);
903
924
  } catch (err) {
904
- throw fmtError(`${err.message}`, 'mkdir', err.code);
925
+ throw err.custom
926
+ ? err
927
+ : this.fmtError(`${err.message} ${remotePath}`, '_mkdir', err.code);
928
+ }
929
+ }
930
+
931
+ async mkdir(remotePath, recursive = false) {
932
+ let listeners;
933
+ try {
934
+ listeners = addTempListeners(this, '_mkdir');
935
+ haveConnection(this, 'mkdir');
936
+ return await this._mkdir(remotePath, recursive);
937
+ } catch (err) {
938
+ throw this.fmtError(`${err.message}`, 'mkdir', err.code);
939
+ } finally {
940
+ removeTempListeners(this, listeners, 'append');
941
+ this._resetEventFlags();
905
942
  }
906
943
  }
907
944
 
@@ -916,48 +953,34 @@ class SftpClient {
916
953
  * @return {Promise<String>}
917
954
  */
918
955
  async rmdir(remotePath, recursive = false) {
919
- const _delete = (remotePath) => {
920
- return new Promise((resolve, reject) => {
921
- this.sftp.unlink(remotePath, (err) => {
922
- if (err && err.code !== 2) {
923
- reject(fmtError(`${err.message} ${remotePath}`, 'rmdir', err.code));
924
- }
925
- resolve(true);
926
- });
927
- });
928
- };
929
-
930
956
  const _rmdir = (p) => {
931
957
  return new Promise((resolve, reject) => {
932
958
  this.debugMsg(`rmdir -> ${p}`);
933
959
  this.sftp.rmdir(p, (err) => {
934
960
  if (err) {
935
- this.debugMsg(`rmdir error ${err.message} code: ${err.code}`);
936
- reject(fmtError(`${err.message} ${p}`, 'rmdir', err.code));
961
+ reject(this.fmtError(`${err.message} ${p}`, 'rmdir', err.code));
937
962
  }
938
963
  resolve('Successfully removed directory');
939
964
  });
940
- }).finally(() => {
941
- removeTempListeners(this, listeners, '_rmdir');
942
965
  });
943
966
  };
944
967
 
945
968
  const _dormdir = async (p, recur) => {
946
969
  try {
947
970
  if (recur) {
948
- let list = await this.list(p);
971
+ const list = await this.list(p);
949
972
  if (list.length) {
950
- let files = list.filter((item) => item.type !== 'd');
951
- let dirs = list.filter((item) => item.type === 'd');
973
+ const files = list.filter((item) => item.type !== 'd');
974
+ const dirs = list.filter((item) => item.type === 'd');
952
975
  this.debugMsg('rmdir contents (files): ', files);
953
976
  this.debugMsg('rmdir contents (dirs): ', dirs);
954
- let promiseList = [];
955
- for (let f of files) {
956
- promiseList.push(_delete(`${p}${this.remotePathSep}${f.name}`));
977
+ for (const d of dirs) {
978
+ await _dormdir(`${p}${this.remotePathSep}${d.name}`, true);
957
979
  }
958
- for (let d of dirs) {
980
+ const promiseList = [];
981
+ for (const f of files) {
959
982
  promiseList.push(
960
- _dormdir(`${p}${this.remotePathSep}${d.name}`, true)
983
+ this._delete(`${p}${this.remotePathSep}${f.name}`)
961
984
  );
962
985
  }
963
986
  await Promise.all(promiseList);
@@ -965,7 +988,7 @@ class SftpClient {
965
988
  }
966
989
  return await _rmdir(p);
967
990
  } catch (err) {
968
- throw err.custom ? err : fmtError(err, '_dormdir', err.code);
991
+ throw err.custom ? err : this.fmtError(err, '_dormdir', err.code);
969
992
  }
970
993
  };
971
994
 
@@ -973,16 +996,16 @@ class SftpClient {
973
996
  try {
974
997
  listeners = addTempListeners(this, 'rmdir');
975
998
  haveConnection(this, 'rmdir');
976
- let absPath = await normalizeRemotePath(this, remotePath);
977
- let dirStatus = await this.exists(absPath);
999
+ const absPath = await normalizeRemotePath(this, remotePath);
1000
+ const dirStatus = await this.exists(absPath);
978
1001
  if (dirStatus && dirStatus !== 'd') {
979
- throw fmtError(
1002
+ throw this.fmtError(
980
1003
  `Bad path: ${absPath} not a directory`,
981
1004
  'rmdir',
982
1005
  errorCode.badPath
983
1006
  );
984
1007
  } else if (!dirStatus) {
985
- throw fmtError(
1008
+ throw this.fmtError(
986
1009
  `Bad path: ${absPath} No such file`,
987
1010
  'rmdir',
988
1011
  errorCode.badPath
@@ -991,10 +1014,10 @@ class SftpClient {
991
1014
  return await _dormdir(absPath, recursive);
992
1015
  }
993
1016
  } catch (err) {
994
- this._resetEventFlags();
995
- throw err.custom ? err : fmtError(err, 'rmdir', err.code);
1017
+ throw err.custom ? err : this.fmtError(err.message, 'rmdir', err.code);
996
1018
  } finally {
997
1019
  removeTempListeners(this, listeners, 'rmdir');
1020
+ this._resetEventFlags();
998
1021
  }
999
1022
  }
1000
1023
 
@@ -1009,31 +1032,35 @@ class SftpClient {
1009
1032
  * @return {Promise<String>} with string 'Successfully deleted file' once resolved
1010
1033
  *
1011
1034
  */
1012
- delete(remotePath, notFoundOK = false) {
1013
- let listeners;
1035
+ _delete(rPath, notFoundOK) {
1014
1036
  return new Promise((resolve, reject) => {
1015
- listeners = addTempListeners(this, 'delete', reject);
1016
- if (haveConnection(this, 'delete', reject)) {
1017
- this.debugMsg(`delete -> ${remotePath}`);
1018
- this.sftp.unlink(remotePath, (err) => {
1019
- if (err) {
1020
- this.debugMsg(`delete error ${err.message} code: ${err.code}`);
1021
- if (notFoundOK && err.code === 2) {
1022
- this.debugMsg('delete ignore missing target error');
1023
- resolve(`Successfully deleted ${remotePath}`);
1024
- } else {
1025
- reject(
1026
- fmtError(`${err.message} ${remotePath}`, 'delete', err.code)
1027
- );
1028
- }
1037
+ this.sftp.unlink(rPath, (err) => {
1038
+ if (err) {
1039
+ if (notFoundOK && err.code === 2) {
1040
+ resolve(`Successfully deleted ${rPath}`);
1041
+ } else {
1042
+ reject(
1043
+ this.fmtError(`${err.message} ${rPath}`, 'delete', err.code)
1044
+ );
1029
1045
  }
1030
- resolve(`Successfully deleted ${remotePath}`);
1031
- });
1032
- }
1033
- }).finally(() => {
1046
+ }
1047
+ resolve(`Successfully deleted ${rPath}`);
1048
+ });
1049
+ });
1050
+ }
1051
+
1052
+ async delete(remotePath, notFoundOK = false) {
1053
+ let listeners;
1054
+ try {
1055
+ listeners = addTempListeners(this, 'delete');
1056
+ haveConnection(this, 'delete');
1057
+ return await this._delete(remotePath, notFoundOK);
1058
+ } catch (err) {
1059
+ throw err.custom ? err : this.fmtError(err.message, 'delete', err.code);
1060
+ } finally {
1034
1061
  removeTempListeners(this, listeners, 'delete');
1035
1062
  this._resetEventFlags();
1036
- });
1063
+ }
1037
1064
  }
1038
1065
 
1039
1066
  /**
@@ -1047,30 +1074,41 @@ class SftpClient {
1047
1074
  * @return {Promise<String>}
1048
1075
  *
1049
1076
  */
1050
- rename(fromPath, toPath) {
1051
- let listeners;
1077
+ _rename(fPath, tPath) {
1052
1078
  return new Promise((resolve, reject) => {
1053
- listeners = addTempListeners(this, 'rename', reject);
1054
- if (haveConnection(this, 'rename', reject)) {
1055
- this.debugMsg(`rename -> ${fromPath} ${toPath}`);
1056
- this.sftp.rename(fromPath, toPath, (err) => {
1057
- if (err) {
1058
- this.debugMsg(`rename error ${err.message} code: ${err.code}`);
1059
- reject(
1060
- fmtError(
1061
- `${err.message} From: ${fromPath} To: ${toPath}`,
1062
- 'rename',
1063
- err.code
1064
- )
1065
- );
1066
- }
1067
- resolve(`Successfully renamed ${fromPath} to ${toPath}`);
1068
- });
1069
- }
1070
- }).finally(() => {
1079
+ this.sftp.rename(fPath, tPath, (err) => {
1080
+ if (err) {
1081
+ reject(
1082
+ this.fmtError(
1083
+ `${err.message} From: ${fPath} To: ${tPath}`,
1084
+ '_rename',
1085
+ err.code
1086
+ )
1087
+ );
1088
+ }
1089
+ resolve(`Successfully renamed ${fPath} to ${tPath}`);
1090
+ });
1091
+ });
1092
+ }
1093
+
1094
+ async rename(fromPath, toPath) {
1095
+ let listeners;
1096
+ try {
1097
+ listeners = addTempListeners(this, 'rename');
1098
+ haveConnection(this, 'rename');
1099
+ return await this._rename(fromPath, toPath);
1100
+ } catch (err) {
1101
+ throw err.custom
1102
+ ? err
1103
+ : this.fmtError(
1104
+ `${err.message} ${fromPath} ${toPath}`,
1105
+ 'rename',
1106
+ err.code
1107
+ );
1108
+ } finally {
1071
1109
  removeTempListeners(this, listeners, 'rename');
1072
1110
  this._resetEventFlags();
1073
- });
1111
+ }
1074
1112
  }
1075
1113
 
1076
1114
  /**
@@ -1085,30 +1123,41 @@ class SftpClient {
1085
1123
  * @return {Promise<String>}
1086
1124
  *
1087
1125
  */
1088
- posixRename(fromPath, toPath) {
1089
- let listeners;
1126
+ _posixRename(fPath, tPath) {
1090
1127
  return new Promise((resolve, reject) => {
1091
- listeners = addTempListeners(this, 'posixRename', reject);
1092
- if (haveConnection(this, 'posixRename', reject)) {
1093
- this.debugMsg(`posixRename -> ${fromPath} ${toPath}`);
1094
- this.sftp.ext_openssh_rename(fromPath, toPath, (err) => {
1095
- if (err) {
1096
- this.debugMsg(`posixRename error ${err.message} code: ${err.code}`);
1097
- reject(
1098
- fmtError(
1099
- `${err.message} From: ${fromPath} To: ${toPath}`,
1100
- 'posixRename',
1101
- err.code
1102
- )
1103
- );
1104
- }
1105
- resolve(`Successful POSIX rename ${fromPath} to ${toPath}`);
1106
- });
1107
- }
1108
- }).finally(() => {
1128
+ this.sftp.ext_openssh_rename(fPath, tPath, (err) => {
1129
+ if (err) {
1130
+ reject(
1131
+ this.fmtError(
1132
+ `${err.message} From: ${fPath} To: ${tPath}`,
1133
+ '_posixRename',
1134
+ err.code
1135
+ )
1136
+ );
1137
+ }
1138
+ resolve(`Successful POSIX rename ${fPath} to ${tPath}`);
1139
+ });
1140
+ });
1141
+ }
1142
+
1143
+ async posixRename(fromPath, toPath) {
1144
+ let listeners;
1145
+ try {
1146
+ listeners = addTempListeners(this, 'posixRename');
1147
+ haveConnection(this, 'posixRename');
1148
+ return await this._posixRename(fromPath, toPath);
1149
+ } catch (err) {
1150
+ throw err.custom
1151
+ ? err
1152
+ : this.fmtError(
1153
+ `${err.message} ${fromPath} ${toPath}`,
1154
+ 'posixRename',
1155
+ err.code
1156
+ );
1157
+ } finally {
1109
1158
  removeTempListeners(this, listeners, 'posixRename');
1110
1159
  this._resetEventFlags();
1111
- });
1160
+ }
1112
1161
  }
1113
1162
 
1114
1163
  /**
@@ -1121,21 +1170,31 @@ class SftpClient {
1121
1170
  *
1122
1171
  * @return {Promise<String>}
1123
1172
  */
1124
- chmod(remotePath, mode) {
1125
- let listeners;
1173
+ _chmod(rPath, mode) {
1126
1174
  return new Promise((resolve, reject) => {
1127
- listeners = addTempListeners(this, 'chmod', reject);
1128
- this.debugMsg(`chmod -> ${remotePath} ${mode}`);
1129
- this.sftp.chmod(remotePath, mode, (err) => {
1175
+ this.sftp.chmod(rPath, mode, (err) => {
1130
1176
  if (err) {
1131
- reject(fmtError(`${err.message} ${remotePath}`, 'chmod', err.code));
1177
+ reject(this.fmtError(`${err.message} ${rPath}`, '_chmod', err.code));
1132
1178
  }
1133
1179
  resolve('Successfully change file mode');
1134
1180
  });
1135
- }).finally(() => {
1181
+ });
1182
+ }
1183
+
1184
+ async chmod(remotePath, mode) {
1185
+ let listeners;
1186
+ try {
1187
+ listeners = addTempListeners(this, 'chmod');
1188
+ haveConnection(this, 'chmod');
1189
+ return await this._chmod(remotePath, mode);
1190
+ } catch (err) {
1191
+ throw err.custom
1192
+ ? err
1193
+ : this.fmtError(`${err.message} ${remotePath}`, 'chmod', err.code);
1194
+ } finally {
1136
1195
  removeTempListeners(this, listeners, 'chmod');
1137
1196
  this._resetEventFlags();
1138
- });
1197
+ }
1139
1198
  }
1140
1199
 
1141
1200
  /**
@@ -1146,73 +1205,95 @@ class SftpClient {
1146
1205
  * server.
1147
1206
  * @param {String} srcDir - local source directory
1148
1207
  * @param {String} dstDir - remote destination directory
1149
- * @param {RegExp} filter - (Optional) a regular expression used to select
1150
- * files and directories to upload
1208
+ * @param {Object} options - (Optional) An object with 2 supported properties,
1209
+ * 'filter' and 'useFastput'. The first argument is the full path of the item
1210
+ * to be uploaded and the second argument is a boolean, which will be true if
1211
+ * the target path is for a directory. If the function returns true, the item
1212
+ * will be uploaded and excluded when it returns false. The 'useFastput' property is a
1213
+ * boolean value. When true, the 'fastPut()' method will be used to upload files. Default
1214
+ * is to use the slower, but more supported 'put()' method.
1215
+ *
1151
1216
  * @returns {Promise<String>}
1152
1217
  */
1153
- async uploadDir(srcDir, dstDir, filter) {
1218
+ async _uploadDir(srcDir, dstDir, options) {
1154
1219
  try {
1155
- this.debugMsg(`uploadDir -> SRC = ${srcDir} DST = ${dstDir}`);
1156
- haveConnection(this, 'uploadDir');
1157
- //let absSrcDir = fs.realpathSync(srcDir);
1158
- let absDstDir = await normalizeRemotePath(this, dstDir);
1159
-
1220
+ const absDstDir = await normalizeRemotePath(this, dstDir);
1160
1221
  this.debugMsg(`uploadDir <- SRC = ${srcDir} DST = ${absDstDir}`);
1161
1222
  const srcType = localExists(srcDir);
1162
1223
  if (!srcType) {
1163
- throw fmtError(
1224
+ throw this.fmtError(
1164
1225
  `Bad path: ${srcDir} not exist`,
1165
- 'uploadDir',
1226
+ '_uploadDir',
1166
1227
  errorCode.badPath
1167
1228
  );
1168
1229
  }
1169
1230
  if (srcType !== 'd') {
1170
- throw fmtError(
1231
+ throw this.fmtError(
1171
1232
  `Bad path: ${srcDir}: not a directory`,
1172
- 'uploadDir',
1233
+ '_uploadDir',
1173
1234
  errorCode.badPath
1174
1235
  );
1175
1236
  }
1176
- let dstStatus = await this.exists(absDstDir);
1237
+ const dstStatus = await this.exists(absDstDir);
1177
1238
  if (dstStatus && dstStatus !== 'd') {
1178
- this.debugMsg(`UploadDir: DST ${absDstDir} exists but not a directory`);
1179
- throw fmtError(
1239
+ throw this.fmtError(
1180
1240
  `Bad path ${absDstDir} Not a directory`,
1181
- 'uploadDir',
1241
+ '_uploadDir',
1182
1242
  errorCode.badPath
1183
1243
  );
1184
1244
  }
1185
1245
  if (!dstStatus) {
1186
- this.debugMsg(`UploadDir: Creating directory ${absDstDir}`);
1187
- await this.mkdir(absDstDir, true);
1246
+ await this._mkdir(absDstDir, true);
1188
1247
  }
1189
1248
  let dirEntries = fs.readdirSync(srcDir, {
1190
1249
  encoding: 'utf8',
1191
1250
  withFileTypes: true,
1192
1251
  });
1193
- if (filter) {
1252
+ if (options?.filter) {
1194
1253
  dirEntries = dirEntries.filter((item) =>
1195
- filter(join(srcDir, item.name), item.isDirectory())
1254
+ options.filter(join(srcDir, item.name), item.isDirectory())
1196
1255
  );
1197
1256
  }
1198
- for (let e of dirEntries) {
1199
- let newSrc = join(srcDir, e.name);
1200
- let newDst = `${absDstDir}${this.remotePathSep}${e.name}`;
1257
+ let fileUploads = [];
1258
+ for (const e of dirEntries) {
1259
+ const newSrc = join(srcDir, e.name);
1260
+ const newDst = `${absDstDir}${this.remotePathSep}${e.name}`;
1201
1261
  if (e.isDirectory()) {
1202
- await this.uploadDir(newSrc, newDst, filter);
1262
+ await this.uploadDir(newSrc, newDst, options);
1203
1263
  } else if (e.isFile()) {
1204
- await this.put(newSrc, newDst);
1264
+ if (options?.useFastput) {
1265
+ fileUploads.push(this._fastPut(newSrc, newDst));
1266
+ } else {
1267
+ fileUploads.push(this._put(newSrc, newDst));
1268
+ }
1205
1269
  this.client.emit('upload', { source: newSrc, destination: newDst });
1206
1270
  } else {
1207
1271
  this.debugMsg(
1208
1272
  `uploadDir: File ignored: ${e.name} not a regular file`
1209
1273
  );
1210
1274
  }
1275
+ await Promise.all(fileUploads);
1211
1276
  }
1212
1277
  return `${srcDir} uploaded to ${absDstDir}`;
1213
1278
  } catch (err) {
1279
+ throw err.custom
1280
+ ? err
1281
+ : this.fmtError(`${err.message} ${srcDir}`, '_uploadDir', err.code);
1282
+ }
1283
+ }
1284
+
1285
+ async uploadDir(srcDir, dstDir, options) {
1286
+ let listeners;
1287
+ try {
1288
+ listeners = addTempListeners(this, 'uploadDir');
1289
+ this.debugMsg(`uploadDir -> SRC = ${srcDir} DST = ${dstDir}`);
1290
+ haveConnection(this, 'uploadDir');
1291
+ return await this._uploadDir(srcDir, dstDir, options);
1292
+ } catch (err) {
1293
+ throw err.custom ? err : this.fmtError(err, 'uploadDir');
1294
+ } finally {
1295
+ removeTempListeners(this, listeners, 'chmod');
1214
1296
  this._resetEventFlags();
1215
- throw err.custom ? err : fmtError(err, 'uploadDir');
1216
1297
  }
1217
1298
  }
1218
1299
 
@@ -1224,18 +1305,21 @@ class SftpClient {
1224
1305
  * file system.
1225
1306
  * @param {String} srcDir - remote source directory
1226
1307
  * @param {String} dstDir - local destination directory
1227
- * @param {RegExp} filter - (Optional) a regular expression used to select
1228
- * files and directories to upload
1308
+ * @param {Object} options - (Optional) Object with 2 supported properties,
1309
+ * 'filter' and 'useFastget'. The filter property is a function of two
1310
+ * arguments. The first argument is the full path of the item to be downloaded
1311
+ * and the second argument is a boolean, which will be true if the target path
1312
+ * is for a directory. If the function returns true, the item will be
1313
+ * downloaded and excluded if teh function returns false.
1314
+ *
1229
1315
  * @returns {Promise<String>}
1230
1316
  */
1231
- async downloadDir(srcDir, dstDir, filter) {
1317
+ async _downloadDir(srcDir, dstDir, options) {
1232
1318
  try {
1233
- this.debugMsg(`downloadDir -> ${srcDir} ${dstDir}`);
1234
- haveConnection(this, 'downloadDir');
1235
- let fileList = await this.list(srcDir);
1236
- if (filter) {
1319
+ let fileList = await this._list(srcDir);
1320
+ if (options?.filter) {
1237
1321
  fileList = fileList.filter((item) =>
1238
- filter(
1322
+ options.filter(
1239
1323
  `${srcDir}${this.remotePathSep}${item.name}`,
1240
1324
  item.type === 'd' ? true : false
1241
1325
  )
@@ -1243,7 +1327,7 @@ class SftpClient {
1243
1327
  }
1244
1328
  const localCheck = haveLocalCreate(dstDir);
1245
1329
  if (!localCheck.status && localCheck.details === 'permission denied') {
1246
- throw fmtError(
1330
+ throw this.fmtError(
1247
1331
  `Bad path: ${dstDir}: ${localCheck.details}`,
1248
1332
  'downloadDir',
1249
1333
  localCheck.code
@@ -1251,19 +1335,24 @@ class SftpClient {
1251
1335
  } else if (localCheck.status && !localCheck.type) {
1252
1336
  fs.mkdirSync(dstDir, { recursive: true });
1253
1337
  } else if (localCheck.status && localCheck.type !== 'd') {
1254
- throw fmtError(
1338
+ throw this.fmtError(
1255
1339
  `Bad path: ${dstDir}: not a directory`,
1256
1340
  'downloadDir',
1257
1341
  errorCode.badPath
1258
1342
  );
1259
1343
  }
1260
- for (let f of fileList) {
1261
- let newSrc = `${srcDir}${this.remotePathSep}${f.name}`;
1262
- let newDst = join(dstDir, f.name);
1344
+ let downloadFiles = [];
1345
+ for (const f of fileList) {
1346
+ const newSrc = `${srcDir}${this.remotePathSep}${f.name}`;
1347
+ const newDst = join(dstDir, f.name);
1263
1348
  if (f.type === 'd') {
1264
- await this.downloadDir(newSrc, newDst, filter);
1349
+ await this._downloadDir(newSrc, newDst, options);
1265
1350
  } else if (f.type === '-') {
1266
- await this.get(newSrc, newDst);
1351
+ if (options?.useFasget) {
1352
+ downloadFiles.push(this._fastGet(newSrc, newDst));
1353
+ } else {
1354
+ downloadFiles.push(this._get(newSrc, newDst));
1355
+ }
1267
1356
  this.client.emit('download', { source: newSrc, destination: newDst });
1268
1357
  } else {
1269
1358
  this.debugMsg(
@@ -1271,13 +1360,157 @@ class SftpClient {
1271
1360
  );
1272
1361
  }
1273
1362
  }
1363
+ await Promise.all(downloadFiles);
1274
1364
  return `${srcDir} downloaded to ${dstDir}`;
1275
1365
  } catch (err) {
1366
+ throw err.custom
1367
+ ? err
1368
+ : this.fmtError(`${err.message} ${srcDir}`, '_downloadDir', err.code);
1369
+ }
1370
+ }
1371
+
1372
+ async downloadDir(srcDir, dstDir, options) {
1373
+ let listeners;
1374
+ try {
1375
+ listeners = addTempListeners(this, 'downloadDir');
1376
+ haveConnection(this, 'downloadDir');
1377
+ return await this._downloadDir(srcDir, dstDir, options);
1378
+ } catch (err) {
1379
+ throw err.custom ? err : this.fmtError(err, 'downloadDir', err.code);
1380
+ } finally {
1381
+ removeTempListeners(this, listeners, 'downloadDir');
1382
+ this._resetEventFlags();
1383
+ }
1384
+ }
1385
+
1386
+ /**
1387
+ *
1388
+ * Returns a read stream object. This is a low level method which will return a read stream
1389
+ * connected to the remote file object specified as an argument. Client code is fully responsible
1390
+ * for managing this stream object i.e. adding any necessary listeners and disposing of the object etc.
1391
+ * See the SSH2 sftp documentation for details on possible options which can be used.
1392
+ *
1393
+ * @param {String} remotePath - path to remote file to attach stream to
1394
+ * @param {Object} options - options to pass to the create stream process
1395
+ *
1396
+ * @returns {Object} a read stream object
1397
+ *
1398
+ */
1399
+ createReadStream(remotePath, options) {
1400
+ let listeners;
1401
+ try {
1402
+ listeners = addTempListeners(this, 'createReadStream');
1403
+ haveConnection(this, 'createReadStream');
1404
+ const stream = this.sftp.createReadStream(remotePath, options);
1405
+ return stream;
1406
+ } catch (err) {
1407
+ throw err.custom
1408
+ ? err
1409
+ : this.fmtError(err.message, 'createReadStream', err.code);
1410
+ } finally {
1411
+ removeTempListeners(this, listeners, 'createReadStreame');
1276
1412
  this._resetEventFlags();
1277
- throw err.custom ? err : fmtError(err, 'downloadDir', err.code);
1278
1413
  }
1279
1414
  }
1280
1415
 
1416
+ /**
1417
+ *
1418
+ * Create a write stream object connected to a file on the remote sftp server.
1419
+ * This is a low level method which will return a write stream for the remote file specified
1420
+ * in the 'remotePath' argument. Client code to responsible for managing this object once created.
1421
+ * This includes disposing of file handles, setting up any necessary event listeners etc.
1422
+ *
1423
+ * @param {String} remotePath - path to the remote file on the sftp server
1424
+ * @param (Object} options - options to pass to the create write stream process)
1425
+ *
1426
+ * @returns {Object} a stream object
1427
+ *
1428
+ */
1429
+ createWriteStream(remotePath, options) {
1430
+ let listeners;
1431
+ try {
1432
+ listeners = addTempListeners(this, 'createWriteStream');
1433
+ haveConnection(this, 'createWriteStream');
1434
+ const stream = this.sftp.createWriteStream(remotePath, options);
1435
+ return stream;
1436
+ } catch (err) {
1437
+ throw err.custom
1438
+ ? err
1439
+ : this.fmtError(err.message, 'createWriteStream', err.code);
1440
+ } finally {
1441
+ removeTempListeners(this, listeners, 'createWriteStream');
1442
+ this._resetEventFlags();
1443
+ }
1444
+ }
1445
+
1446
+ /**
1447
+ * @async
1448
+ *
1449
+ * Make a remote copy of a remote file. Create a copy of a remote file on the remote
1450
+ * server. It is assumed the directory where the copy will be placed already exists.
1451
+ * The destination file must not already exist.
1452
+ *
1453
+ * @param {String} srcPath - path to the remote file to be copied
1454
+ * @param {String} dstPath - destination path for the copy.
1455
+ *
1456
+ * @returns {String}.
1457
+ *
1458
+ */
1459
+ _rcopy(srcPath, dstPath) {
1460
+ return new Promise((resolve, reject) => {
1461
+ const ws = this.sftp.createWriteStream(dstPath);
1462
+ const rs = this.sftp.createReadStream(srcPath);
1463
+ ws.on('error', (err) => {
1464
+ reject(this.fmtError(`${err.message} ${dstPath}`, '_rcopy'));
1465
+ });
1466
+ rs.on('error', (err) => {
1467
+ reject(this.fmtError(`${err.message} ${srcPath}`, '_rcopy'));
1468
+ });
1469
+ ws.on('close', () => {
1470
+ resolve(`${srcPath} copied to ${dstPath}`);
1471
+ });
1472
+ rs.pipe(ws);
1473
+ });
1474
+ }
1475
+
1476
+ async rcopy(src, dst) {
1477
+ let listeners;
1478
+ try {
1479
+ listeners = addTempListeners(this, 'rcopy');
1480
+ haveConnection(this, 'rcopy');
1481
+ const srcPath = await normalizeRemotePath(this, src);
1482
+ const srcExists = await this.exists(srcPath);
1483
+ if (!srcExists) {
1484
+ throw this.fmtError(
1485
+ `Source does not exist ${srcPath}`,
1486
+ 'rcopy',
1487
+ errorCode.badPath
1488
+ );
1489
+ }
1490
+ if (srcExists !== '-') {
1491
+ throw this.fmtError(
1492
+ `Source not a file ${srcPath}`,
1493
+ 'rcopy',
1494
+ errorCode.badPath
1495
+ );
1496
+ }
1497
+ const dstPath = await normalizeRemotePath(this, dst);
1498
+ const dstExists = await this.exists(dstPath);
1499
+ if (dstExists) {
1500
+ throw this.fmtError(
1501
+ `Destination already exists ${dstPath}`,
1502
+ 'rcopy',
1503
+ errorCode.badPath
1504
+ );
1505
+ }
1506
+ return await this._rcopy(srcPath, dstPath);
1507
+ } catch (err) {
1508
+ throw err.custom ? err : this.fmtError(err, 'rcopy');
1509
+ } finally {
1510
+ removeTempListeners(this, listeners, 'rcopy');
1511
+ this._resetEventFlags();
1512
+ }
1513
+ }
1281
1514
  /**
1282
1515
  * @async
1283
1516
  *
@@ -1297,11 +1530,9 @@ class SftpClient {
1297
1530
  };
1298
1531
  this.on('close', endCloseHandler);
1299
1532
  if (haveConnection(this, 'end', reject)) {
1300
- this.debugMsg('end: Have connection - calling end()');
1301
1533
  this.client.end();
1302
1534
  }
1303
1535
  }).finally(() => {
1304
- this.debugMsg('end: finally clause fired');
1305
1536
  removeTempListeners(this, listeners, 'end');
1306
1537
  this.removeListener('close', endCloseHandler);
1307
1538
  this.endCalled = false;