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/README.md +348 -125
- package/README.org +138 -52
- package/package.json +9 -5
- package/src/constants.js +1 -0
- package/src/index.js +888 -657
- package/src/utils.js +43 -106
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(
|
|
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.
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
257
|
+
});
|
|
258
|
+
let sftp = await this.getSftpChannel();
|
|
259
|
+
return sftp;
|
|
214
260
|
} catch (err) {
|
|
215
|
-
this.
|
|
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
|
-
|
|
233
|
-
let listeners;
|
|
280
|
+
_realPath(rPath) {
|
|
234
281
|
return new Promise((resolve, reject) => {
|
|
235
|
-
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
})
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
|
398
|
+
async _exists(rPath) {
|
|
345
399
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
403
|
-
let listeners;
|
|
457
|
+
_list(remotePath, filter) {
|
|
404
458
|
return new Promise((resolve, reject) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
+
} else {
|
|
564
|
+
this.debugMsg('get: returning data into supplied stream');
|
|
565
|
+
wtr = dst;
|
|
549
566
|
}
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
642
|
+
throw this.fmtError(
|
|
643
|
+
`Bad path: ${localPath}: ${localCheck.details}`,
|
|
644
|
+
'fastGet',
|
|
645
|
+
errorCode.badPath
|
|
646
|
+
);
|
|
600
647
|
}
|
|
601
|
-
|
|
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
|
-
|
|
641
|
-
let listeners;
|
|
642
|
-
this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
|
|
670
|
+
_fastPut(lPath, rPath, opts) {
|
|
643
671
|
return new Promise((resolve, reject) => {
|
|
644
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
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
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
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
|
-
|
|
804
|
-
listeners = addTempListeners(this, 'append'
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
`Bad path: ${p} parent not a directory or not exist
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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.
|
|
868
|
-
resolve(`${p} directory created`);
|
|
885
|
+
reject(this.fmtError(`${err.message} ${p}`, '_doMkdir', err.code));
|
|
869
886
|
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
879
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
|
908
|
+
return await this._doMkdir(rPath);
|
|
890
909
|
}
|
|
891
|
-
|
|
910
|
+
const dir = parse(rPath).dir;
|
|
892
911
|
if (dir) {
|
|
893
|
-
|
|
912
|
+
const dirExists = await this.exists(dir);
|
|
894
913
|
if (!dirExists) {
|
|
895
|
-
await this.
|
|
914
|
+
await this._mkdir(dir, true);
|
|
896
915
|
} else if (dirExists !== 'd') {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
916
|
+
throw this.fmtError(
|
|
917
|
+
`Bad path: ${dir} not a directory`,
|
|
918
|
+
'_mkdir',
|
|
919
|
+
errorCode.badPath
|
|
920
|
+
);
|
|
900
921
|
}
|
|
901
922
|
}
|
|
902
|
-
return await
|
|
923
|
+
return await this._doMkdir(rPath);
|
|
903
924
|
} catch (err) {
|
|
904
|
-
throw
|
|
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.
|
|
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
|
-
|
|
971
|
+
const list = await this.list(p);
|
|
949
972
|
if (list.length) {
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
955
|
-
|
|
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
|
-
|
|
980
|
+
const promiseList = [];
|
|
981
|
+
for (const f of files) {
|
|
959
982
|
promiseList.push(
|
|
960
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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.
|
|
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
|
-
|
|
1013
|
-
let listeners;
|
|
1035
|
+
_delete(rPath, notFoundOK) {
|
|
1014
1036
|
return new Promise((resolve, reject) => {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
-
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
})
|
|
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
|
-
|
|
1051
|
-
let listeners;
|
|
1077
|
+
_rename(fPath, tPath) {
|
|
1052
1078
|
return new Promise((resolve, reject) => {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1089
|
-
let listeners;
|
|
1126
|
+
_posixRename(fPath, tPath) {
|
|
1090
1127
|
return new Promise((resolve, reject) => {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1125
|
-
let listeners;
|
|
1173
|
+
_chmod(rPath, mode) {
|
|
1126
1174
|
return new Promise((resolve, reject) => {
|
|
1127
|
-
|
|
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} ${
|
|
1177
|
+
reject(this.fmtError(`${err.message} ${rPath}`, '_chmod', err.code));
|
|
1132
1178
|
}
|
|
1133
1179
|
resolve('Successfully change file mode');
|
|
1134
1180
|
});
|
|
1135
|
-
})
|
|
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 {
|
|
1150
|
-
*
|
|
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
|
|
1218
|
+
async _uploadDir(srcDir, dstDir, options) {
|
|
1154
1219
|
try {
|
|
1155
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
1233
|
+
'_uploadDir',
|
|
1173
1234
|
errorCode.badPath
|
|
1174
1235
|
);
|
|
1175
1236
|
}
|
|
1176
|
-
|
|
1237
|
+
const dstStatus = await this.exists(absDstDir);
|
|
1177
1238
|
if (dstStatus && dstStatus !== 'd') {
|
|
1178
|
-
this.
|
|
1179
|
-
throw fmtError(
|
|
1239
|
+
throw this.fmtError(
|
|
1180
1240
|
`Bad path ${absDstDir} Not a directory`,
|
|
1181
|
-
'
|
|
1241
|
+
'_uploadDir',
|
|
1182
1242
|
errorCode.badPath
|
|
1183
1243
|
);
|
|
1184
1244
|
}
|
|
1185
1245
|
if (!dstStatus) {
|
|
1186
|
-
this.
|
|
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
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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,
|
|
1262
|
+
await this.uploadDir(newSrc, newDst, options);
|
|
1203
1263
|
} else if (e.isFile()) {
|
|
1204
|
-
|
|
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 {
|
|
1228
|
-
*
|
|
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
|
|
1317
|
+
async _downloadDir(srcDir, dstDir, options) {
|
|
1232
1318
|
try {
|
|
1233
|
-
this.
|
|
1234
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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.
|
|
1349
|
+
await this._downloadDir(newSrc, newDst, options);
|
|
1265
1350
|
} else if (f.type === '-') {
|
|
1266
|
-
|
|
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;
|