ssh2-sftp-client 10.0.3 → 12.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.
Files changed (5) hide show
  1. package/README.md +177 -142
  2. package/README.org +152 -50
  3. package/package.json +9 -10
  4. package/src/index.js +76 -147
  5. package/src/utils.js +83 -38
package/src/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  'use strict';
2
-
3
2
  const { Client } = require('ssh2');
4
3
  const fs = require('node:fs');
5
4
  const concat = require('concat-stream');
6
- const promiseRetry = require('promise-retry');
7
5
  const { join, parse } = require('node:path');
8
6
  const {
9
7
  globalListener,
@@ -19,11 +17,18 @@ const {
19
17
  const { errorCode } = require('./constants');
20
18
 
21
19
  class SftpClient {
22
- constructor(clientName) {
23
- this.version = '10.0.3';
20
+ constructor(
21
+ clientName = 'sftp',
22
+ callbacks = {
23
+ error: (err) => console.error(`Global error listener: ${err.message}`),
24
+ end: () => console.log('Global end listener: end event raised'),
25
+ close: () => console.log('Global close listener: close event raised'),
26
+ },
27
+ ) {
28
+ this.version = '12.0.0';
24
29
  this.client = new Client();
25
30
  this.sftp = undefined;
26
- this.clientName = clientName || 'sftp';
31
+ this.clientName = clientName;
27
32
  this.endCalled = false;
28
33
  this.errorHandled = false;
29
34
  this.closeHandled = false;
@@ -31,9 +36,10 @@ class SftpClient {
31
36
  this.remotePlatform = 'unix';
32
37
  this.debug = undefined;
33
38
  this.promiseLimit = 10;
34
- this.client.on('close', globalListener(this, 'close'));
35
- this.client.on('end', globalListener(this, 'end'));
36
- this.client.on('error', globalListener(this, 'error'));
39
+ this.eventCallbacks = callbacks;
40
+ this.client.on('close', globalListener(this, 'close', this.eventCallbacks));
41
+ this.client.on('end', globalListener(this, 'end', this.eventCallbacks));
42
+ this.client.on('error', globalListener(this, 'error', this.eventCallbacks));
37
43
  }
38
44
 
39
45
  debugMsg(msg, obj) {
@@ -51,35 +57,32 @@ class SftpClient {
51
57
  fmtError(err, name = 'sftp', eCode, retryCount) {
52
58
  let msg = '';
53
59
  let code = '';
54
- const retry = retryCount
55
- ? ` after ${retryCount} ${retryCount > 1 ? 'attempts' : 'attempt'}`
56
- : '';
57
60
 
58
61
  if (err === undefined) {
59
62
  msg = `${name}: Undefined error - probably a bug!`;
60
63
  code = errorCode.generic;
61
64
  } else if (typeof err === 'string') {
62
- msg = `${name}: ${err}${retry}`;
65
+ msg = `${name}: ${err}`;
63
66
  code = eCode || errorCode.generic;
64
67
  } else if (err.custom) {
65
- msg = `${name}->${err.message}${retry}`;
68
+ msg = `${name}->${err.message}`;
66
69
  code = err.code;
67
70
  } else {
68
71
  switch (err.code) {
69
72
  case 'ENOTFOUND': {
70
- msg = `${name}: Address lookup failed for host${retry}`;
73
+ msg = `${name}: Address lookup failed for host`;
71
74
  break;
72
75
  }
73
76
  case 'ECONNREFUSED': {
74
- msg = `${name}: Remote host refused connection${retry}`;
77
+ msg = `${name}: Remote host refused connection`;
75
78
  break;
76
79
  }
77
80
  case 'ECONNRESET': {
78
- msg = `${name}: Remote host has reset the connection: ${err.message}${retry}`;
81
+ msg = `${name}: Remote host has reset the connection: ${err.message}`;
79
82
  break;
80
83
  }
81
84
  default: {
82
- msg = `${name}: ${err.message}${retry}`;
85
+ msg = `${name}: ${err.message}`;
83
86
  }
84
87
  }
85
88
  code = err.code || errorCode.generic;
@@ -115,27 +118,52 @@ class SftpClient {
115
118
  }
116
119
 
117
120
  /**
118
- * @async
119
121
  *
120
- * Create a new SFTP connection to a remote SFTP server
122
+ * Create a new SFTP connection to a remote SFTP server.
123
+ * The connection options are the same as those offered
124
+ * by the underlying SSH2 module.
121
125
  *
122
126
  * @param {Object} config - an SFTP configuration object
123
127
  *
124
128
  * @return {Promise<Object>} which will resolve to an sftp client object
125
129
  */
126
- getConnection(config) {
130
+ connect(config) {
127
131
  let doReady, listeners;
128
132
  return new Promise((resolve, reject) => {
129
133
  listeners = addTempListeners(this, 'getConnection', reject);
134
+ if (config.debug) {
135
+ this.debug = config.debug;
136
+ this.debugMsg('connect: Debugging turned on');
137
+ this.debugMsg(`ssh2-sftp-client Version: ${this.version} `, process.versions);
138
+ }
139
+ this.promiseLimit = config.promiseLimit ?? 10;
140
+
130
141
  doReady = () => {
131
- this.debugMsg('getConnection ready listener: got connection - promise resolved');
132
- resolve(true);
142
+ this.client.sftp((err, sftp) => {
143
+ if (err) {
144
+ reject(this.fmtError(err));
145
+ } else {
146
+ this.sftp = sftp;
147
+ resolve(sftp);
148
+ }
149
+ });
133
150
  };
134
151
  this.on('ready', doReady);
152
+
135
153
  try {
136
- this.client.connect(config);
154
+ if (this.sftp) {
155
+ reject(
156
+ this.fmtError(
157
+ 'An existing SFTP connection is already defined',
158
+ 'connect',
159
+ errorCode.connect,
160
+ ),
161
+ );
162
+ } else {
163
+ this.client.connect(config);
164
+ }
137
165
  } catch (err) {
138
- this.debugMsg(`getConnection: ${err.message}`);
166
+ this.end();
139
167
  reject(err);
140
168
  }
141
169
  }).finally(() => {
@@ -144,90 +172,6 @@ class SftpClient {
144
172
  });
145
173
  }
146
174
 
147
- getSftpChannel() {
148
- return new Promise((resolve, reject) => {
149
- this.client.sftp((err, sftp) => {
150
- if (err) {
151
- reject(this.fmtError(err, 'getSftpChannel', err.code));
152
- } else {
153
- this.sftp = sftp;
154
- resolve(sftp);
155
- }
156
- });
157
- });
158
- }
159
-
160
- /**
161
- * @async
162
- *
163
- * Create a new SFTP connection to a remote SFTP server.
164
- * The connection options are the same as those offered
165
- * by the underlying SSH2 module.
166
- *
167
- * @param {Object} config - an SFTP configuration object
168
- *
169
- * @return {Promise<Object>} which will resolve to an sftp client object
170
- */
171
- async connect(config) {
172
- let listeners;
173
-
174
- try {
175
- listeners = addTempListeners(this, 'connect');
176
- if (config.debug) {
177
- this.debug = config.debug;
178
- this.debugMsg('connect: Debugging turned on');
179
- this.debugMsg(`ssh2-sftp-client Version: ${this.version} `, process.versions);
180
- }
181
- this.promiseLimit = config.promiseLimit ?? 10;
182
- if (this.sftp) {
183
- throw this.fmtError(
184
- 'An existing SFTP connection is already defined',
185
- 'connect',
186
- errorCode.connect,
187
- );
188
- }
189
- const retryOpts = {
190
- retries: config.retries ?? 1,
191
- factor: config.retry_factor ?? 2,
192
- minTimeout: config.retry_minTimeout ?? 25000,
193
- };
194
- await promiseRetry(retryOpts, async (retry, attempt) => {
195
- try {
196
- this.debugMsg(`connect: Connect attempt ${attempt}`);
197
- await this.getConnection(config);
198
- } catch (err) {
199
- switch (err.code) {
200
- case 'ENOTFOUND':
201
- case 'ECONNREFUSED':
202
- case 'ERR_SOCKET_BAD_PORT': {
203
- throw err;
204
- }
205
- case undefined: {
206
- if (
207
- err.message.endsWith('All configured authentication methods failed') ||
208
- err.message.startsWith('Cannot parse privateKey')
209
- ) {
210
- throw err;
211
- }
212
- retry(err);
213
- break;
214
- }
215
- default: {
216
- retry(err);
217
- }
218
- }
219
- }
220
- });
221
- const sftp = await this.getSftpChannel();
222
- return sftp;
223
- } catch (err) {
224
- this.end();
225
- throw err.custom ? err : this.fmtError(err, 'connect');
226
- } finally {
227
- removeTempListeners(this, listeners, 'connect');
228
- }
229
- }
230
-
231
175
  /**
232
176
  * @async
233
177
  *
@@ -246,18 +190,14 @@ class SftpClient {
246
190
  if (addListeners) {
247
191
  listeners = addTempListeners(this, 'realPath', reject);
248
192
  }
249
- this.debugMsg(`realPath -> ${remotePath}`);
250
193
  this.sftp.realpath(remotePath, (err, absPath) => {
251
194
  if (err) {
252
195
  if (err.code === 2) {
253
- this.debugMsg('realPath <- ""');
254
196
  resolve('');
255
197
  } else {
256
- this.debugMsg(`${err.message} ${remotePath}`, 'realPath');
257
198
  reject(this.fmtError(`${err.message} ${remotePath}`, 'realPath', err.code));
258
199
  }
259
200
  }
260
- this.debugMsg(`realPath <- ${absPath}`);
261
201
  resolve(absPath);
262
202
  });
263
203
  }).finally(() => {
@@ -313,7 +253,6 @@ class SftpClient {
313
253
  isFIFO: stats.isFIFO(),
314
254
  isSocket: stats.isSocket(),
315
255
  };
316
- this.debugMsg('_xstat: result = ', result);
317
256
  resolve(result);
318
257
  }
319
258
  };
@@ -382,13 +321,11 @@ class SftpClient {
382
321
  * object if it does
383
322
  */
384
323
  async exists(remotePath) {
385
- this.debugMsg(`exists: remotePath = ${remotePath}`);
386
324
  try {
387
325
  if (remotePath === '.') {
388
326
  return 'd';
389
327
  }
390
328
  const info = await this.lstat(remotePath);
391
- this.debugMsg('exists: <- ', info);
392
329
  if (info.isDirectory) {
393
330
  return 'd';
394
331
  } else if (info.isSymbolicLink) {
@@ -501,20 +438,20 @@ class SftpClient {
501
438
  };
502
439
  rdr = this.sftp.createReadStream(remotePath, options.readStreamOptions);
503
440
  rdr.once('error', (err) => {
504
- if (dst && typeof dst !== 'string' && !dst.destroyed) {
505
- dst.destroy();
441
+ if (dst && typeof dst === 'string' && wtr && !wtr.destroyed) {
442
+ wtr.destroy();
506
443
  }
507
444
  reject(this.fmtError(`${err.message} ${remotePath}`, 'get', err.code));
508
445
  });
509
446
  if (dst === undefined) {
510
447
  // no dst specified, return buffer of data
511
- this.debugMsg('get resolving buffer of data');
448
+ this.debugMsg('get resolving with buffer of data');
512
449
  wtr = concat((buff) => {
513
450
  resolve(buff);
514
451
  });
515
452
  } else if (typeof dst === 'string') {
516
453
  // dst local file path
517
- this.debugMsg('get returning local file');
454
+ this.debugMsg(`get called with file path destination ${dst}`);
518
455
  const localCheck = haveLocalCreate(dst);
519
456
  if (localCheck.status) {
520
457
  wtr = fs.createWriteStream(dst, options.writeStreamOptions);
@@ -528,7 +465,7 @@ class SftpClient {
528
465
  );
529
466
  }
530
467
  } else {
531
- this.debugMsg('get: returning data into supplied stream');
468
+ this.debugMsg('get called with stream destination');
532
469
  wtr = dst;
533
470
  }
534
471
  wtr.once('error', (err) => {
@@ -542,16 +479,17 @@ class SftpClient {
542
479
  });
543
480
  rdr.once('end', () => {
544
481
  if (typeof dst === 'string') {
545
- this.debugMsg('get: resolving with dst filename');
546
482
  resolve(dst);
547
483
  } else if (dst !== undefined) {
548
- this.debugMsg('get: resolving with writer stream object');
549
484
  resolve(wtr);
550
485
  }
551
486
  });
552
487
  rdr.pipe(wtr, options.pipeOptions);
553
488
  }
554
489
  }).finally(() => {
490
+ if (rdr && !rdr.destroyed) {
491
+ rdr.destroy();
492
+ }
555
493
  if (addListeners) {
556
494
  removeTempListeners(this, listeners, 'get');
557
495
  }
@@ -661,7 +599,6 @@ class SftpClient {
661
599
 
662
600
  async fastPut(localPath, remotePath, options) {
663
601
  try {
664
- this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
665
602
  const localCheck = haveLocalAccess(localPath);
666
603
  if (!localCheck.status) {
667
604
  throw this.fmtError(
@@ -713,6 +650,9 @@ class SftpClient {
713
650
  if (haveConnection(this, '_put', reject)) {
714
651
  wtr = this.sftp.createWriteStream(rPath, opts.writeStreamOptions);
715
652
  wtr.once('error', (err) => {
653
+ if (typeof lPath === 'string' && rdr && !rdr.destroyed) {
654
+ rdr.destroy();
655
+ }
716
656
  reject(
717
657
  this.fmtError(
718
658
  `Write stream error: ${err.message} ${rPath}`,
@@ -728,10 +668,13 @@ class SftpClient {
728
668
  this.debugMsg('put source is a buffer');
729
669
  wtr.end(lPath);
730
670
  } else {
731
- rdr =
732
- typeof lPath === 'string'
733
- ? fs.createReadStream(lPath, opts.readStreamOptions)
734
- : lPath;
671
+ if (typeof lPath === 'string') {
672
+ this.debugMsg('put source is string path');
673
+ rdr = fs.createReadStream(lPath, opts.readStreamOptions);
674
+ } else {
675
+ this.debugMsg('put source is a stream');
676
+ rdr = lPath;
677
+ }
735
678
  rdr.once('error', (err) => {
736
679
  reject(
737
680
  this.fmtError(
@@ -747,6 +690,9 @@ class SftpClient {
747
690
  }
748
691
  }
749
692
  }).finally(() => {
693
+ if (wtr && !wtr.destroyed) {
694
+ wtr.destroy();
695
+ }
750
696
  if (addListeners) {
751
697
  removeTempListeners(this, listeners, '_put');
752
698
  }
@@ -786,7 +732,6 @@ class SftpClient {
786
732
  listeners = addTempListeners(this, '_append', reject);
787
733
  }
788
734
  if (haveConnection(this, '_append', reject)) {
789
- this.debugMsg(`append -> remote: ${rPath} `, opts);
790
735
  opts.flags = 'a';
791
736
  const stream = this.sftp.createWriteStream(rPath, opts);
792
737
  stream.on('error', (err) => {
@@ -826,7 +771,7 @@ class SftpClient {
826
771
  errorCode.badPath,
827
772
  );
828
773
  }
829
- await this._append(input, remotePath, options);
774
+ return await this._append(input, remotePath, options);
830
775
  } catch (e) {
831
776
  throw e.custom ? e : this.fmtError(e.message, 'append', e.code);
832
777
  }
@@ -941,7 +886,6 @@ class SftpClient {
941
886
  let listeners;
942
887
  return new Promise((resolve, reject) => {
943
888
  listeners = addTempListeners(this, '_rmdir', reject);
944
- this.debugMsg(`_rmdir: dir = ${dir}`);
945
889
  this.sftp.rmdir(dir, (err) => {
946
890
  if (err) {
947
891
  reject(this.fmtError(`${err.message} ${dir}`, 'rmdir', err.code));
@@ -957,7 +901,6 @@ class SftpClient {
957
901
  let listeners;
958
902
  return new Promise((resolve, reject) => {
959
903
  listeners = addTempListeners(this, '_delFiles', reject);
960
- this.debugMsg(`_delFiles: path = ${path} fileList = ${fileList}`);
961
904
  const pList = [];
962
905
  for (const f of fileList) {
963
906
  pList.push(this.delete(`${path}/${f.name}`, true, false));
@@ -973,10 +916,8 @@ class SftpClient {
973
916
  };
974
917
 
975
918
  try {
976
- this.debugMsg(`rmdir: dir = ${remoteDir} recursive = ${recursive}`);
977
919
  const absPath = await normalizeRemotePath(this, remoteDir);
978
920
  const existStatus = await this.exists(absPath);
979
- this.debugMsg(`rmdir: ${absPath} existStatus = ${existStatus}`);
980
921
  if (!existStatus) {
981
922
  throw this.fmtError(
982
923
  `Bad Path: ${remoteDir}: No such directory`,
@@ -992,19 +933,14 @@ class SftpClient {
992
933
  );
993
934
  }
994
935
  if (!recursive) {
995
- this.debugMsg('rmdir: non-recursive - just try to remove it');
996
936
  return await _rmdir(absPath);
997
937
  }
998
938
  const listing = await this.list(absPath);
999
- this.debugMsg(`rmdir: listing count = ${listing.length}`);
1000
939
  if (!listing.length) {
1001
- this.debugMsg('rmdir: No sub dir or files, just rmdir');
1002
940
  return await _rmdir(absPath);
1003
941
  }
1004
942
  const fileList = listing.filter((i) => i.type !== 'd');
1005
- this.debugMsg(`rmdir: dir content files to remove = ${fileList.length}`);
1006
943
  const dirList = listing.filter((i) => i.type === 'd');
1007
- this.debugMsg(`rmdir: sub-directories to remove = ${dirList.length}`);
1008
944
  await _delFiles(absPath, fileList);
1009
945
  for (const d of dirList) {
1010
946
  await this.rmdir(`${absPath}/${d.name}`, true);
@@ -1244,11 +1180,7 @@ class SftpClient {
1244
1180
 
1245
1181
  try {
1246
1182
  haveConnection(this, 'uploadDir');
1247
- this.debugMsg(
1248
- `uploadDir: srcDir = ${srcDir} dstDir = ${dstDir} options = ${options}`,
1249
- );
1250
1183
  const { remoteDir, remoteStatus } = await getRemoteStatus(dstDir);
1251
- this.debugMsg(`uploadDir: remoteDir = ${remoteDir} remoteStatus = ${remoteStatus}`);
1252
1184
  checkLocalStatus(srcDir);
1253
1185
  if (!remoteStatus) {
1254
1186
  await this._mkdir(remoteDir, true);
@@ -1257,7 +1189,6 @@ class SftpClient {
1257
1189
  encoding: 'utf8',
1258
1190
  withFileTypes: true,
1259
1191
  });
1260
- this.debugMsg(`uploadDir: dirEntries = ${dirEntries}`);
1261
1192
  if (options?.filter) {
1262
1193
  dirEntries = dirEntries.filter((item) =>
1263
1194
  options.filter(join(srcDir, item.name), item.isDirectory()),
@@ -1265,8 +1196,6 @@ class SftpClient {
1265
1196
  }
1266
1197
  const dirUploads = dirEntries.filter((item) => item.isDirectory());
1267
1198
  const fileUploads = dirEntries.filter((item) => !item.isDirectory());
1268
- this.debugMsg(`uploadDir: dirUploads = ${dirUploads}`);
1269
- this.debugMsg(`uploadDir: fileUploads = ${fileUploads}`);
1270
1199
  await uploadFiles(srcDir, remoteDir, fileUploads, options?.useFastput);
1271
1200
  for (const d of dirUploads) {
1272
1201
  const src = join(srcDir, d.name);
@@ -1417,7 +1346,7 @@ class SftpClient {
1417
1346
  } catch (err) {
1418
1347
  throw err.custom ? err : this.fmtError(err.message, 'createReadStream', err.code);
1419
1348
  } finally {
1420
- removeTempListeners(this, listeners, 'createReadStreame');
1349
+ removeTempListeners(this, listeners, 'createReadStream');
1421
1350
  }
1422
1351
  }
1423
1352
 
@@ -1527,6 +1456,7 @@ class SftpClient {
1527
1456
  };
1528
1457
  this.on('close', endCloseHandler);
1529
1458
  if (this.sftp) {
1459
+ this.debugMsg('end: Ending SFTP connection');
1530
1460
  this.client.end();
1531
1461
  } else {
1532
1462
  // no actual connection exists - just resolve
@@ -1536,7 +1466,6 @@ class SftpClient {
1536
1466
  }).finally(() => {
1537
1467
  removeTempListeners(this, listeners, 'end');
1538
1468
  this.removeListener('close', endCloseHandler);
1539
- this.endCalled = false;
1540
1469
  });
1541
1470
  }
1542
1471
  }
package/src/utils.js CHANGED
@@ -1,6 +1,54 @@
1
- const fs = require('node:fs');
2
- const path = require('node:path');
3
- const { errorCode } = require('./constants');
1
+ const { statSync, constants, accessSync } = require('node:fs');
2
+ const { dirname } = require('node:path');
3
+ const { errorCode } = require('./constants.js');
4
+
5
+ function eventHandled(client) {
6
+ if (client.errorHandled || client.endHandled || client.closeHandled) {
7
+ return true;
8
+ }
9
+ return false;
10
+ }
11
+
12
+ function globalListener(client, evt, eventCallbacks) {
13
+ if (evt === 'error') {
14
+ return (err) => {
15
+ if (client.errorHandled) {
16
+ client.debugMsg(`Global error event: Ignoring handled error ${err.message}`);
17
+ return;
18
+ }
19
+ client.debugMsg(`Global error event: ${err.message}`);
20
+ client.errorHandled = true;
21
+ if (eventCallbacks?.error) {
22
+ eventCallbacks.error(err);
23
+ }
24
+ };
25
+ }
26
+ if (evt === 'end') {
27
+ return () => {
28
+ if (client.endCalled || client.endHandled) {
29
+ client.debugMsg('Global end event: Ignoring handled end event');
30
+ return;
31
+ }
32
+ client.debugMsg('Global end event: Handling end event');
33
+ client.endHandled = true;
34
+ if (eventCallbacks?.end) {
35
+ eventCallbacks.end();
36
+ }
37
+ };
38
+ }
39
+ return () => {
40
+ if (client.endCalled || client.closeHandled) {
41
+ client.debugMsg('Global close event: Ignoring handled close event');
42
+ } else {
43
+ client.debugMsg('Global close event: Handling close event');
44
+ client.closeHandled = true;
45
+ client.sftp = undefined;
46
+ if (eventCallbacks?.close) {
47
+ eventCallbacks.close();
48
+ }
49
+ }
50
+ };
51
+ }
4
52
 
5
53
  /**
6
54
  * Simple default error listener. Will reformat the error message and
@@ -10,12 +58,13 @@ const { errorCode } = require('./constants');
10
58
  * @throws {Error} Throws new error
11
59
  */
12
60
  function errorListener(client, name, reject) {
13
- const fn = (err) => {
14
- if (client.endCalled || client.errorHandled) {
61
+ const fn = function (err) {
62
+ if (eventHandled(client)) {
15
63
  // error already handled or expected - ignore
16
- client.debugMsg(`${name} errorListener - ignoring handled error`);
64
+ client.debugMsg(`${name} errorListener - ignoring handled error ${err.message}`);
17
65
  return;
18
66
  }
67
+ client.debugMsg(`${name} errorListener - handling error ${err.message}`);
19
68
  client.errorHandled = true;
20
69
  const newError = new Error(`${name}: ${err.message}`);
21
70
  newError.code = err.code;
@@ -28,55 +77,52 @@ function errorListener(client, name, reject) {
28
77
  return fn;
29
78
  }
30
79
 
31
- function globalListener(client, evt) {
32
- return () => {
33
- if (client.endCalled || client.errorHandled || client.closeHandled) {
34
- // we are processing an expected event handled elsewhere
35
- client.debugMsg(`Global ${evt} event: Ignoring expected and handled event`);
36
- } else {
37
- client.debugMsg(`Global ${evt} event: Handling unexpected event`);
38
- client.sftp = undefined;
39
- }
40
- };
41
- }
42
-
43
- function endListener(client, name) {
80
+ function endListener(client, name, reject) {
44
81
  const fn = function () {
45
82
  client.sftp = undefined;
46
- if (client.endCalled || client.endHandled || client.errorHandled) {
83
+ if (client.endCalled || eventHandled(client)) {
47
84
  // end event already handled - ignore
48
- client.debugMsg(`${name} endListener - handled end event`);
85
+ client.debugMsg(`${name} endListener - ignoring handled end event`);
49
86
  return;
50
87
  }
51
88
  client.endHandled = true;
52
- client.debugMsg(`${name} Unexpected end event`);
89
+ client.debugMsg(`${name} endListener - handling unexpected end event`);
90
+ const newError = new Error(`${name}: Unexpected end event`);
91
+ newError.code = errorCode.ERR_GENERIC_CLIENT;
92
+ if (reject) {
93
+ reject(newError);
94
+ } else {
95
+ throw newError;
96
+ }
53
97
  };
54
98
  return fn;
55
99
  }
56
100
 
57
- function closeListener(client, name) {
101
+ function closeListener(client, name, reject) {
58
102
  const fn = function () {
59
103
  client.sftp = undefined;
60
- if (
61
- client.endCalled ||
62
- client.closeHandled ||
63
- client.errorHandled ||
64
- client.endHandled
65
- ) {
104
+ if (client.endCalled || eventHandled(client)) {
66
105
  // handled or expected close event - ignore
67
- client.debugMsg(`${name} closeListener - handled close event`);
106
+ client.debugMsg(`${name} closeListener - ignoring handled close event`);
68
107
  return;
69
108
  }
70
109
  client.closeHandled = true;
71
- client.debugMsg(`${name} Unexpected close event`);
110
+ client.debugMsg(`${name} closeListener - handling unexpected close event`);
111
+ const newError = new Error(`${name}: Unexpected close event`);
112
+ newError.code = errorCode.ERR_GENERIC_CLIENT;
113
+ if (reject) {
114
+ reject(newError);
115
+ } else {
116
+ throw newError;
117
+ }
72
118
  };
73
119
  return fn;
74
120
  }
75
121
 
76
122
  function addTempListeners(client, name, reject) {
77
123
  const listeners = {
78
- end: endListener(client, name),
79
- close: closeListener(client, name),
124
+ end: endListener(client, name, reject),
125
+ close: closeListener(client, name, reject),
80
126
  error: errorListener(client, name, reject),
81
127
  };
82
128
  client.on('end', listeners.end);
@@ -109,7 +155,7 @@ function removeTempListeners(client, listeners, name) {
109
155
  * @returns {string | boolean} returns a string for object type if it exists, false otherwise
110
156
  */
111
157
  function localExists(filePath) {
112
- const stats = fs.statSync(filePath, { throwIfNoEntry: false });
158
+ const stats = statSync(filePath, { throwIfNoEntry: false });
113
159
  if (!stats) {
114
160
  return false;
115
161
  } else if (stats.isDirectory()) {
@@ -140,11 +186,10 @@ function localExists(filePath) {
140
186
  * @returns {Object} with properties status, type, details and code
141
187
  */
142
188
  function haveLocalAccess(filePath, mode = 'r') {
143
- const accessMode =
144
- fs.constants.F_OK | (mode === 'w') ? fs.constants.W_OK : fs.constants.R_OK;
189
+ const accessMode = constants.F_OK | (mode === 'w') ? constants.W_OK : constants.R_OK;
145
190
 
146
191
  try {
147
- fs.accessSync(filePath, accessMode);
192
+ accessSync(filePath, accessMode);
148
193
  const type = localExists(filePath);
149
194
  return {
150
195
  status: true,
@@ -210,7 +255,7 @@ function haveLocalCreate(filePath) {
210
255
  };
211
256
  }
212
257
  // to create it, parent must be directory and writeable
213
- const dirPath = path.dirname(filePath);
258
+ const dirPath = dirname(filePath);
214
259
  const localCheck = haveLocalAccess(dirPath, 'w');
215
260
  if (!localCheck.status) {
216
261
  // no access to parent directory