@milaboratories/pl-deployments 2.4.6 → 2.4.8
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/dist/common/os_and_arch.cjs +39 -0
- package/dist/common/os_and_arch.cjs.map +1 -0
- package/dist/common/os_and_arch.d.ts +0 -1
- package/dist/common/os_and_arch.js +34 -0
- package/dist/common/os_and_arch.js.map +1 -0
- package/dist/common/pl_binary.cjs +34 -0
- package/dist/common/pl_binary.cjs.map +1 -0
- package/dist/common/pl_binary.d.ts +0 -1
- package/dist/common/pl_binary.js +30 -0
- package/dist/common/pl_binary.js.map +1 -0
- package/dist/common/pl_binary_download.cjs +172 -0
- package/dist/common/pl_binary_download.cjs.map +1 -0
- package/dist/common/pl_binary_download.d.ts +0 -1
- package/dist/common/pl_binary_download.js +148 -0
- package/dist/common/pl_binary_download.js.map +1 -0
- package/dist/common/pl_version.cjs +11 -0
- package/dist/common/pl_version.cjs.map +1 -0
- package/dist/common/pl_version.d.ts +0 -1
- package/dist/common/pl_version.js +9 -0
- package/dist/common/pl_version.js.map +1 -0
- package/dist/index.cjs +26 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +5 -64
- package/dist/index.js.map +1 -1
- package/dist/local/options.d.ts +0 -1
- package/dist/local/pid.cjs +24 -0
- package/dist/local/pid.cjs.map +1 -0
- package/dist/local/pid.d.ts +0 -1
- package/dist/local/pid.js +20 -0
- package/dist/local/pid.js.map +1 -0
- package/dist/local/pl.cjs +217 -0
- package/dist/local/pl.cjs.map +1 -0
- package/dist/local/pl.d.ts +0 -1
- package/dist/local/pl.js +192 -0
- package/dist/local/pl.js.map +1 -0
- package/dist/local/process.cjs +42 -0
- package/dist/local/process.cjs.map +1 -0
- package/dist/local/process.d.ts +0 -1
- package/dist/local/process.js +37 -0
- package/dist/local/process.js.map +1 -0
- package/dist/local/trace.cjs +27 -0
- package/dist/local/trace.cjs.map +1 -0
- package/dist/local/trace.d.ts +0 -1
- package/dist/local/trace.js +23 -0
- package/dist/local/trace.js.map +1 -0
- package/dist/package.json.cjs +7 -0
- package/dist/package.json.cjs.map +1 -0
- package/dist/package.json.js +5 -0
- package/dist/package.json.js.map +1 -0
- package/dist/ssh/__tests__/common-utils.d.ts +0 -1
- package/dist/ssh/connection_info.cjs +62 -0
- package/dist/ssh/connection_info.cjs.map +1 -0
- package/dist/ssh/connection_info.d.ts +0 -1
- package/dist/ssh/connection_info.js +55 -0
- package/dist/ssh/connection_info.js.map +1 -0
- package/dist/ssh/pl.cjs +500 -0
- package/dist/ssh/pl.cjs.map +1 -0
- package/dist/ssh/pl.d.ts +0 -1
- package/dist/ssh/pl.js +497 -0
- package/dist/ssh/pl.js.map +1 -0
- package/dist/ssh/pl_paths.cjs +67 -0
- package/dist/ssh/pl_paths.cjs.map +1 -0
- package/dist/ssh/pl_paths.d.ts +0 -1
- package/dist/ssh/pl_paths.js +50 -0
- package/dist/ssh/pl_paths.js.map +1 -0
- package/dist/ssh/ssh.cjs +621 -0
- package/dist/ssh/ssh.cjs.map +1 -0
- package/dist/ssh/ssh.d.ts +0 -1
- package/dist/ssh/ssh.js +619 -0
- package/dist/ssh/ssh.js.map +1 -0
- package/dist/ssh/supervisord.cjs +149 -0
- package/dist/ssh/supervisord.cjs.map +1 -0
- package/dist/ssh/supervisord.d.ts +0 -1
- package/dist/ssh/supervisord.js +140 -0
- package/dist/ssh/supervisord.js.map +1 -0
- package/package.json +16 -14
- package/src/common/pl_version.ts +3 -2
- package/dist/common/os_and_arch.d.ts.map +0 -1
- package/dist/common/pl_binary.d.ts.map +0 -1
- package/dist/common/pl_binary_download.d.ts.map +0 -1
- package/dist/common/pl_version.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.mjs +0 -1366
- package/dist/index.mjs.map +0 -1
- package/dist/local/options.d.ts.map +0 -1
- package/dist/local/pid.d.ts.map +0 -1
- package/dist/local/pl.d.ts.map +0 -1
- package/dist/local/process.d.ts.map +0 -1
- package/dist/local/trace.d.ts.map +0 -1
- package/dist/ssh/__tests__/common-utils.d.ts.map +0 -1
- package/dist/ssh/connection_info.d.ts.map +0 -1
- package/dist/ssh/pl.d.ts.map +0 -1
- package/dist/ssh/pl_paths.d.ts.map +0 -1
- package/dist/ssh/ssh.d.ts.map +0 -1
- package/dist/ssh/supervisord.d.ts.map +0 -1
package/dist/ssh/ssh.cjs
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ssh = require('ssh2');
|
|
4
|
+
var net = require('node:net');
|
|
5
|
+
var dns = require('node:dns');
|
|
6
|
+
var fs = require('node:fs');
|
|
7
|
+
var fsp = require('node:fs/promises');
|
|
8
|
+
var upath = require('upath');
|
|
9
|
+
var tsHelpers = require('@milaboratories/ts-helpers');
|
|
10
|
+
var node_crypto = require('node:crypto');
|
|
11
|
+
|
|
12
|
+
const defaultConfig = {
|
|
13
|
+
keepaliveInterval: 60000,
|
|
14
|
+
keepaliveCountMax: 10,
|
|
15
|
+
};
|
|
16
|
+
class SshClient {
|
|
17
|
+
logger;
|
|
18
|
+
client;
|
|
19
|
+
config;
|
|
20
|
+
homeDir;
|
|
21
|
+
forwardedServers = [];
|
|
22
|
+
constructor(logger, client) {
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
this.client = client;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Initializes the SshClient and establishes a connection using the provided configuration.
|
|
28
|
+
* @param config - The connection configuration object for the SSH client.
|
|
29
|
+
* @returns A new instance of SshClient with an active connection.
|
|
30
|
+
*/
|
|
31
|
+
static async init(logger, config) {
|
|
32
|
+
const withDefaults = {
|
|
33
|
+
...defaultConfig,
|
|
34
|
+
...config,
|
|
35
|
+
};
|
|
36
|
+
const client = new SshClient(logger, new ssh.Client());
|
|
37
|
+
await client.connect(withDefaults);
|
|
38
|
+
return client;
|
|
39
|
+
}
|
|
40
|
+
getForwardedServers() {
|
|
41
|
+
return this.forwardedServers;
|
|
42
|
+
}
|
|
43
|
+
getFullHostName() {
|
|
44
|
+
return `${this.config?.host}:${this.config?.port}`;
|
|
45
|
+
}
|
|
46
|
+
getUserName() {
|
|
47
|
+
return this.config?.username;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Connects to the SSH server using the specified configuration.
|
|
51
|
+
* @param config - The connection configuration object for the SSH client.
|
|
52
|
+
* @returns A promise that resolves when the connection is established or rejects on error.
|
|
53
|
+
*/
|
|
54
|
+
async connect(config) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
return await connect(this.client, config);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Executes a command on the SSH server.
|
|
60
|
+
* @param command - The command to execute on the remote server.
|
|
61
|
+
* @returns A promise resolving with the command's stdout and stderr outputs.
|
|
62
|
+
*/
|
|
63
|
+
async exec(command) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
this.client.exec(command, (err, stream) => {
|
|
66
|
+
if (err) {
|
|
67
|
+
return reject(new Error(`ssh.exec: ${command}: ${err}`));
|
|
68
|
+
}
|
|
69
|
+
let stdout = '';
|
|
70
|
+
let stderr = '';
|
|
71
|
+
stream.on('close', (code) => {
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
resolve({ stdout, stderr });
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
reject(new Error(`Command ${command} exited with code ${code}, stdout: ${stdout}, stderr: ${stderr}`));
|
|
77
|
+
}
|
|
78
|
+
}).on('data', (data) => {
|
|
79
|
+
stdout += data.toString();
|
|
80
|
+
}).stderr.on('data', (data) => {
|
|
81
|
+
stderr += data.toString();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Retrieves the supported authentication methods for a given host and port.
|
|
88
|
+
* @param host - The hostname or IP address of the server.
|
|
89
|
+
* @param port - The port number to connect to on the server.
|
|
90
|
+
* @returns 'publickey' | 'password'[] A promise resolving with a list of supported authentication methods.
|
|
91
|
+
*/
|
|
92
|
+
static async getAuthTypes(host, port) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
let stdout = '';
|
|
95
|
+
const conn = new ssh.Client();
|
|
96
|
+
conn.on('ready', () => {
|
|
97
|
+
conn.end();
|
|
98
|
+
const types = this.extractAuthMethods(stdout);
|
|
99
|
+
resolve(types.length === 0 ? ['publickey', 'password'] : types);
|
|
100
|
+
});
|
|
101
|
+
conn.on('error', () => {
|
|
102
|
+
conn.end();
|
|
103
|
+
resolve(['publickey', 'password']);
|
|
104
|
+
});
|
|
105
|
+
conn.connect({
|
|
106
|
+
host,
|
|
107
|
+
port,
|
|
108
|
+
username: new Date().getTime().toString(),
|
|
109
|
+
debug: (err) => {
|
|
110
|
+
stdout += `${err}\n`;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extracts authentication methods from debug logs.
|
|
117
|
+
* @param log - The debug log output containing authentication information.
|
|
118
|
+
* @returns An array of extracted authentication methods.
|
|
119
|
+
*/
|
|
120
|
+
static extractAuthMethods(log) {
|
|
121
|
+
const match = log.match(/Inbound: Received USERAUTH_FAILURE \((.+)\)/);
|
|
122
|
+
return match && match[1] ? match[1].split(',').map((method) => method.trim()) : [];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Sets up port forwarding between a remote port on the SSH server and a local port.
|
|
126
|
+
* A new connection is used for this operation instead of an existing one.
|
|
127
|
+
* @param ports - An object specifying the remote and local port configuration.
|
|
128
|
+
* @param config - Optional connection configuration for the SSH client.
|
|
129
|
+
* @returns { server: net.Server } A promise resolving with the created server instance.
|
|
130
|
+
*/
|
|
131
|
+
async forwardPort(ports, config) {
|
|
132
|
+
const log = `ssh.forward:${ports.localPort}:${ports.remotePort}.id_${node_crypto.randomBytes(1).toString('hex')}`;
|
|
133
|
+
config = config ?? this.config;
|
|
134
|
+
// we make this thing persistent so that if the connection
|
|
135
|
+
// drops (it happened in the past because of lots of errors and forwardOut opened channels),
|
|
136
|
+
// we'll recreate it here.
|
|
137
|
+
const persistentClient = new tsHelpers.RetryablePromise((p) => {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const client = new ssh.Client();
|
|
140
|
+
client.on('ready', () => {
|
|
141
|
+
this.logger.info(`${log}.client.ready`);
|
|
142
|
+
resolve(client);
|
|
143
|
+
});
|
|
144
|
+
client.on('error', (err) => {
|
|
145
|
+
this.logger.info(`${log}.client.error: ${err}`);
|
|
146
|
+
p.reset();
|
|
147
|
+
reject(err);
|
|
148
|
+
});
|
|
149
|
+
client.on('close', () => {
|
|
150
|
+
this.logger.info(`${log}.client.closed`);
|
|
151
|
+
p.reset();
|
|
152
|
+
});
|
|
153
|
+
client.connect(config);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
await persistentClient.ensure(); // warm up a connection
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const server = net.createServer({ pauseOnConnect: true }, async (localSocket) => {
|
|
159
|
+
const sockLog = `${log}.sock_${node_crypto.randomBytes(1).toString('hex')}`;
|
|
160
|
+
// this.logger.info(`${sockLog}.localSocket: start connection`);
|
|
161
|
+
let conn;
|
|
162
|
+
try {
|
|
163
|
+
conn = await persistentClient.ensure();
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
this.logger.info(`${sockLog}.persistentClient.catch: ${e}`);
|
|
167
|
+
localSocket.end();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Remove TCP buffering.
|
|
171
|
+
// Although it means less throughput (bad), it also less latency (good).
|
|
172
|
+
// It could help when we have
|
|
173
|
+
// small messages like in our grpc transactions.
|
|
174
|
+
// And it also could help when we have tcp forwarding to not buffer messages in the middle.
|
|
175
|
+
conn.setNoDelay(true);
|
|
176
|
+
localSocket.setNoDelay(true);
|
|
177
|
+
let stream;
|
|
178
|
+
try {
|
|
179
|
+
stream = await forwardOut(this.logger, conn, '127.0.0.1', 0, '127.0.0.1', ports.remotePort);
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
this.logger.error(`${sockLog}.forwardOut.err: ${e}`);
|
|
183
|
+
localSocket.end();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
localSocket.pipe(stream);
|
|
187
|
+
stream.pipe(localSocket);
|
|
188
|
+
localSocket.resume();
|
|
189
|
+
// this.logger.info(`${sockLog}.forwardOut: connected`);
|
|
190
|
+
stream.on('error', (err) => {
|
|
191
|
+
this.logger.error(`${sockLog}.stream.error: ${err}`);
|
|
192
|
+
localSocket.end();
|
|
193
|
+
stream.end();
|
|
194
|
+
});
|
|
195
|
+
stream.on('close', () => {
|
|
196
|
+
// this.logger.info(`${sockLog}.stream.close: closed`);
|
|
197
|
+
localSocket.end();
|
|
198
|
+
stream.end();
|
|
199
|
+
});
|
|
200
|
+
localSocket.on('close', () => {
|
|
201
|
+
this.logger.info(`${sockLog}.localSocket: closed`);
|
|
202
|
+
localSocket.end();
|
|
203
|
+
stream.end();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
server.listen(ports.localPort, '127.0.0.1', () => {
|
|
207
|
+
this.logger.info(`${log}.server: started listening`);
|
|
208
|
+
this.forwardedServers.push(server);
|
|
209
|
+
resolve({ server });
|
|
210
|
+
});
|
|
211
|
+
server.on('error', (err) => {
|
|
212
|
+
server.close();
|
|
213
|
+
reject(new Error(`${log}.server: error: ${JSON.stringify(err)}`));
|
|
214
|
+
});
|
|
215
|
+
server.on('close', () => {
|
|
216
|
+
this.logger.info(`${log}.server: closed ${JSON.stringify(ports)}`);
|
|
217
|
+
this.forwardedServers = this.forwardedServers.filter((s) => s !== server);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
closeForwardedPorts() {
|
|
222
|
+
this.logger.info('[SSH] Closing all forwarded ports...');
|
|
223
|
+
this.forwardedServers.forEach((server) => {
|
|
224
|
+
const rawAddress = server.address();
|
|
225
|
+
if (rawAddress && typeof rawAddress !== 'string') {
|
|
226
|
+
const address = rawAddress;
|
|
227
|
+
this.logger.info(`[SSH] Closing port forward for server ${address.address}:${address.port}`);
|
|
228
|
+
}
|
|
229
|
+
server.close();
|
|
230
|
+
});
|
|
231
|
+
this.forwardedServers = [];
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Checks if a specified host is available by performing a DNS lookup.
|
|
235
|
+
* @param hostname - The hostname or IP address to check.
|
|
236
|
+
* @returns A promise resolving with `true` if the host is reachable, otherwise `false`.
|
|
237
|
+
*/
|
|
238
|
+
static async checkHostAvailability(hostname) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
dns.lookup(hostname, (err) => {
|
|
241
|
+
resolve(!err);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Determines whether a private key requires a passphrase for use.
|
|
247
|
+
* @param privateKey - The private key content to check.
|
|
248
|
+
* @returns A promise resolving with `true` if a passphrase is required, otherwise `false`.
|
|
249
|
+
*/
|
|
250
|
+
static async isPassphraseRequiredForKey(privateKey) {
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
try {
|
|
253
|
+
const keyOrError = ssh.utils.parseKey(privateKey);
|
|
254
|
+
if (keyOrError instanceof Error) {
|
|
255
|
+
resolve(true);
|
|
256
|
+
}
|
|
257
|
+
return resolve(false);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
console.log('Error parsing privateKey');
|
|
261
|
+
reject(new Error(`ssh.isPassphraseRequiredForKey: err ${err}`));
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Uploads a local file to a remote server via SFTP.
|
|
267
|
+
* This function creates new SFTP connection
|
|
268
|
+
* @param localPath - The local file path.
|
|
269
|
+
* @param remotePath - The remote file path on the server.
|
|
270
|
+
* @returns A promise resolving with `true` if the file was successfully uploaded.
|
|
271
|
+
*/
|
|
272
|
+
async uploadFile(localPath, remotePath) {
|
|
273
|
+
return await this.withSftp(async (sftp) => {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
sftp.fastPut(localPath, remotePath, (err) => {
|
|
276
|
+
if (err) {
|
|
277
|
+
const newErr = new Error(`ssh.uploadFile: err: ${err}, localPath: ${localPath}, remotePath: ${remotePath}`);
|
|
278
|
+
return reject(newErr);
|
|
279
|
+
}
|
|
280
|
+
resolve(true);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async withSftp(callback) {
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
this.client.sftp((err, sftp) => {
|
|
288
|
+
if (err) {
|
|
289
|
+
return reject(new Error(`ssh.withSftp: sftp err: ${err}`));
|
|
290
|
+
}
|
|
291
|
+
callback(sftp)
|
|
292
|
+
.then(resolve)
|
|
293
|
+
.catch((err) => {
|
|
294
|
+
reject(new Error(`ssh.withSftp.callback: err ${err}`));
|
|
295
|
+
})
|
|
296
|
+
.finally(() => {
|
|
297
|
+
sftp?.end();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async writeFileOnTheServer(remotePath, data, mode = 0o660) {
|
|
303
|
+
return this.withSftp(async (sftp) => {
|
|
304
|
+
return this.writeFile(sftp, remotePath, data, mode);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async getForderStructure(sftp, remotePath, data = { files: [], directories: [] }) {
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
sftp.readdir(remotePath, async (err, items) => {
|
|
310
|
+
if (err) {
|
|
311
|
+
return reject(err);
|
|
312
|
+
}
|
|
313
|
+
for (const item of items) {
|
|
314
|
+
const itemPath = `${remotePath}/${item.filename}`;
|
|
315
|
+
if (item.attrs.isDirectory()) {
|
|
316
|
+
data.directories.push(itemPath);
|
|
317
|
+
try {
|
|
318
|
+
await this.getForderStructure(sftp, itemPath, data);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
return reject(error instanceof Error ? error : new Error(String(error)));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
data.files.push(itemPath);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
resolve(data);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
rmdir(sftp, path) {
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
sftp.rmdir(path, (err) => err ? reject(err) : resolve(true));
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
unlink(sftp, path) {
|
|
338
|
+
return new Promise((resolve, reject) => {
|
|
339
|
+
sftp.unlink(path, (err) => err ? reject(err) : resolve(true));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async deleteFolder(path) {
|
|
343
|
+
return this.withSftp(async (sftp) => {
|
|
344
|
+
try {
|
|
345
|
+
const list = await this.getForderStructure(sftp, path);
|
|
346
|
+
this.logger.info(`ssh.deleteFolder list of files and directories`);
|
|
347
|
+
this.logger.info(`ssh.deleteFolder list of files: ${list.files}`);
|
|
348
|
+
this.logger.info(`ssh.deleteFolder list of directories: ${list.directories}`);
|
|
349
|
+
for (const filePath of list.files) {
|
|
350
|
+
this.logger.info(`ssh.deleteFolder unlink file ${filePath}`);
|
|
351
|
+
await this.unlink(sftp, filePath);
|
|
352
|
+
}
|
|
353
|
+
list.directories.sort((a, b) => b.length - a.length);
|
|
354
|
+
for (const directoryPath of list.directories) {
|
|
355
|
+
this.logger.info(`ssh.deleteFolder rmdir ${directoryPath}`);
|
|
356
|
+
await this.rmdir(sftp, directoryPath);
|
|
357
|
+
}
|
|
358
|
+
await this.rmdir(sftp, path);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
this.logger.error(e);
|
|
363
|
+
const message = e instanceof Error ? e.message : '';
|
|
364
|
+
throw new Error(`ssh.deleteFolder: path: ${path}, message: ${message}`);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async readFile(remotePath) {
|
|
369
|
+
return this.withSftp(async (sftp) => {
|
|
370
|
+
return new Promise((resolve, reject) => {
|
|
371
|
+
sftp.readFile(remotePath, (err, buffer) => {
|
|
372
|
+
if (err) {
|
|
373
|
+
return reject(new Error(`ssh.readFile: ${err}`));
|
|
374
|
+
}
|
|
375
|
+
resolve(buffer.toString());
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async chmod(path, mode) {
|
|
381
|
+
return this.withSftp(async (sftp) => {
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
sftp.chmod(path, mode, (err) => {
|
|
384
|
+
if (err) {
|
|
385
|
+
return reject(new Error(`ssh.chmod: ${err}, path: ${path}, mode: ${mode}`));
|
|
386
|
+
}
|
|
387
|
+
return resolve(undefined);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async checkFileExists(remotePath) {
|
|
393
|
+
return this.withSftp(async (sftp) => {
|
|
394
|
+
return new Promise((resolve, reject) => {
|
|
395
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
396
|
+
if (err) {
|
|
397
|
+
if (err?.code === 2) {
|
|
398
|
+
return resolve(false);
|
|
399
|
+
}
|
|
400
|
+
return reject(new Error(`ssh.checkFileExists: err ${err}`));
|
|
401
|
+
}
|
|
402
|
+
resolve(stats.isFile());
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async checkPathExists(remotePath) {
|
|
408
|
+
return this.withSftp(async (sftp) => {
|
|
409
|
+
return new Promise((resolve, reject) => {
|
|
410
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
411
|
+
if (err) {
|
|
412
|
+
if (err.code === 2) {
|
|
413
|
+
return resolve({ exists: false, isFile: false, isDirectory: false });
|
|
414
|
+
}
|
|
415
|
+
return reject(new Error(`ssh.checkPathExists: ${err}`));
|
|
416
|
+
}
|
|
417
|
+
resolve({
|
|
418
|
+
exists: true,
|
|
419
|
+
isFile: stats.isFile(),
|
|
420
|
+
isDirectory: stats.isDirectory(),
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
async writeFile(sftp, remotePath, data, mode = 0o660) {
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
sftp.writeFile(remotePath, data, { mode }, (err) => {
|
|
429
|
+
if (err) {
|
|
430
|
+
return reject(new Error(`ssh.writeFile: err ${err}, remotePath: ${remotePath}`));
|
|
431
|
+
}
|
|
432
|
+
resolve(true);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
uploadFileUsingExistingSftp(sftp, localPath, remotePath, mode = 0o660) {
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
void fsp.readFile(localPath).then(async (result) => {
|
|
439
|
+
return this.writeFile(sftp, remotePath, result, mode)
|
|
440
|
+
.then(() => {
|
|
441
|
+
resolve(undefined);
|
|
442
|
+
})
|
|
443
|
+
.catch((err) => {
|
|
444
|
+
const msg = `uploadFileUsingExistingSftp: ${err}`;
|
|
445
|
+
this.logger.error(msg);
|
|
446
|
+
reject(new Error(msg));
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
async __uploadDirectory(sftp, localDir, remoteDir, mode = 0o660) {
|
|
452
|
+
return new Promise((resolve, reject) => {
|
|
453
|
+
fs.readdir(localDir, async (err, files) => {
|
|
454
|
+
if (err) {
|
|
455
|
+
return reject(new Error(`ssh.__uploadDir: err ${err}, localDir: ${localDir}, remoteDir: ${remoteDir}`));
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
await this.__createRemoteDirectory(sftp, remoteDir);
|
|
459
|
+
for (const file of files) {
|
|
460
|
+
const localPath = upath.join(localDir, file);
|
|
461
|
+
const remotePath = `${remoteDir}/${file}`;
|
|
462
|
+
if (fs.lstatSync(localPath).isDirectory()) {
|
|
463
|
+
await this.__uploadDirectory(sftp, localPath, remotePath, mode);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
await this.uploadFileUsingExistingSftp(sftp, localPath, remotePath, mode);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
resolve();
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
const msg = `ssh.__uploadDir: catched err ${err}`;
|
|
473
|
+
this.logger.error(msg);
|
|
474
|
+
reject(new Error(msg));
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Uploads a local directory and its contents (including subdirectories) to the remote server via SFTP.
|
|
481
|
+
* @param localDir - The path to the local directory to upload.
|
|
482
|
+
* @param remoteDir - The path to the remote directory on the server.
|
|
483
|
+
* @returns A promise that resolves when the directory and its contents are uploaded.
|
|
484
|
+
*/
|
|
485
|
+
async uploadDirectory(localDir, remoteDir, mode = 0o660) {
|
|
486
|
+
return new Promise((resolve, reject) => {
|
|
487
|
+
void this.withSftp(async (sftp) => {
|
|
488
|
+
try {
|
|
489
|
+
await this.__uploadDirectory(sftp, localDir, remoteDir, mode);
|
|
490
|
+
resolve();
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
reject(new Error(`ssh.uploadDirectory: ${e}`));
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Ensures that a remote directory and all its parent directories exist.
|
|
500
|
+
* @param sftp - The SFTP wrapper.
|
|
501
|
+
* @param remotePath - The path to the remote directory.
|
|
502
|
+
* @returns A promise that resolves when the directory is created.
|
|
503
|
+
*/
|
|
504
|
+
__createRemoteDirectory(sftp, remotePath) {
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
const directories = remotePath.split('/');
|
|
507
|
+
let currentPath = '';
|
|
508
|
+
const createNext = (index) => {
|
|
509
|
+
if (index >= directories.length) {
|
|
510
|
+
return resolve();
|
|
511
|
+
}
|
|
512
|
+
currentPath += `${directories[index]}/`;
|
|
513
|
+
sftp.stat(currentPath, (err) => {
|
|
514
|
+
if (err) {
|
|
515
|
+
sftp.mkdir(currentPath, (err) => {
|
|
516
|
+
if (err) {
|
|
517
|
+
return reject(new Error(`ssh.__createRemDir: err ${err}, remotePath: ${remotePath}`));
|
|
518
|
+
}
|
|
519
|
+
createNext(index + 1);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
createNext(index + 1);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
createNext(0);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Ensures that a remote directory and all its parent directories exist.
|
|
532
|
+
* @param sftp - The SFTP wrapper.
|
|
533
|
+
* @param remotePath - The path to the remote directory.
|
|
534
|
+
* @returns A promise that resolves when the directory is created.
|
|
535
|
+
*/
|
|
536
|
+
ensureRemoteDirCreated(remotePath, mode = 0o755) {
|
|
537
|
+
return this.withSftp(async (sftp) => {
|
|
538
|
+
const directories = remotePath.split('/');
|
|
539
|
+
let currentPath = '';
|
|
540
|
+
for (const directory of directories) {
|
|
541
|
+
currentPath += `${directory}/`;
|
|
542
|
+
try {
|
|
543
|
+
await new Promise((resolve, reject) => {
|
|
544
|
+
sftp.stat(currentPath, (err) => {
|
|
545
|
+
if (!err)
|
|
546
|
+
return resolve();
|
|
547
|
+
sftp.mkdir(currentPath, { mode }, (err) => {
|
|
548
|
+
if (err) {
|
|
549
|
+
return reject(new Error(`ssh.createRemoteDir: err ${err}, remotePath: ${remotePath}`));
|
|
550
|
+
}
|
|
551
|
+
resolve();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
console.error(`Failed to create directory: ${currentPath}`, error);
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Downloads a file from the remote server to a local path via SFTP.
|
|
565
|
+
* @param remotePath - The remote file path on the server.
|
|
566
|
+
* @param localPath - The local file path to save the file.
|
|
567
|
+
* @returns A promise resolving with `true` if the file was successfully downloaded.
|
|
568
|
+
*/
|
|
569
|
+
async downloadFile(remotePath, localPath) {
|
|
570
|
+
return this.withSftp(async (sftp) => {
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
sftp.fastGet(remotePath, localPath, (err) => {
|
|
573
|
+
if (err) {
|
|
574
|
+
return reject(new Error(`ssh.downloadFile: err ${err}, remotePath: ${remotePath}, localPath: ${localPath}`));
|
|
575
|
+
}
|
|
576
|
+
resolve(true);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Closes the SSH client connection and forwarded ports.
|
|
583
|
+
*/
|
|
584
|
+
close() {
|
|
585
|
+
this.closeForwardedPorts();
|
|
586
|
+
this.client.end();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function connect(client, config, onError, onClose) {
|
|
590
|
+
return new Promise((resolve, reject) => {
|
|
591
|
+
client.on('ready', () => {
|
|
592
|
+
resolve(client);
|
|
593
|
+
});
|
|
594
|
+
client.on('error', (err) => {
|
|
595
|
+
reject(new Error(`ssh.connect: ${err}`));
|
|
596
|
+
});
|
|
597
|
+
client.on('close', () => {
|
|
598
|
+
});
|
|
599
|
+
client.connect(config);
|
|
600
|
+
// Remove TCP buffering.
|
|
601
|
+
// Although it means less throughput (bad), it also means less latency (good).
|
|
602
|
+
// It could help when we have
|
|
603
|
+
// small messages like in our grpc transactions.
|
|
604
|
+
// And it also could help when we have tcp forwarding to not buffer messages in the middle.
|
|
605
|
+
client.setNoDelay(true);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async function forwardOut(logger, conn, localHost, localPort, remoteHost, remotePort) {
|
|
609
|
+
return new Promise((resolve, reject) => {
|
|
610
|
+
conn.forwardOut(localHost, localPort, remoteHost, remotePort, (err, stream) => {
|
|
611
|
+
if (err) {
|
|
612
|
+
logger.error(`forwardOut.error: ${err}`);
|
|
613
|
+
return reject(err);
|
|
614
|
+
}
|
|
615
|
+
return resolve(stream);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
exports.SshClient = SshClient;
|
|
621
|
+
//# sourceMappingURL=ssh.cjs.map
|