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