ezpm2gui 1.1.0 → 1.2.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/dist/server/config/project-configs.json +236 -0
- package/dist/server/index.js +30 -5
- package/dist/server/logs/deployment.log +12 -0
- package/dist/server/routes/deployApplication.js +174 -27
- package/dist/server/routes/logStreaming.js +174 -0
- package/dist/server/routes/remoteConnections.d.ts +3 -0
- package/dist/server/routes/remoteConnections.js +634 -0
- package/dist/server/services/ProjectSetupService.d.ts +72 -0
- package/dist/server/services/ProjectSetupService.js +327 -0
- package/dist/server/utils/dialog.d.ts +1 -0
- package/dist/server/utils/dialog.js +16 -0
- package/dist/server/utils/encryption.d.ts +12 -0
- package/dist/server/utils/encryption.js +72 -0
- package/dist/server/utils/remote-connection.d.ts +152 -0
- package/dist/server/utils/remote-connection.js +590 -0
- package/dist/server/utils/upload.d.ts +3 -0
- package/dist/server/utils/upload.js +39 -0
- package/package.json +65 -63
- package/src/client/build/asset-manifest.json +3 -3
- package/src/client/build/favicon.ico +2 -0
- package/src/client/build/index.html +1 -1
- package/src/client/build/logo192.svg +7 -0
- package/src/client/build/logo512.svg +7 -0
- package/src/client/build/manifest.json +5 -6
- package/src/client/build/static/js/{main.1d7f99ff.js → main.31323a04.js} +13 -13
- package/src/client/build/static/js/main.31323a04.js.map +1 -0
- package/src/client/build/static/js/main.1d7f99ff.js.map +0 -1
- /package/src/client/build/static/js/{main.1d7f99ff.js.LICENSE.txt → main.31323a04.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.remoteConnectionManager = exports.RemoteConnectionManager = exports.RemoteConnection = void 0;
|
|
4
|
+
const ssh2_1 = require("ssh2");
|
|
5
|
+
const events_1 = require("events");
|
|
6
|
+
/**
|
|
7
|
+
* Class to manage SSH connections to remote servers
|
|
8
|
+
*/
|
|
9
|
+
class RemoteConnection extends events_1.EventEmitter {
|
|
10
|
+
// Getters for secure access to authentication details
|
|
11
|
+
get hasPassword() {
|
|
12
|
+
return !!this.config.password;
|
|
13
|
+
}
|
|
14
|
+
get hasPrivateKey() {
|
|
15
|
+
return !!this.config.privateKey;
|
|
16
|
+
}
|
|
17
|
+
get hasPassphrase() {
|
|
18
|
+
return !!this.config.passphrase;
|
|
19
|
+
}
|
|
20
|
+
// Getter for secure config (without sensitive data)
|
|
21
|
+
get secureConfig() {
|
|
22
|
+
return {
|
|
23
|
+
name: this.name,
|
|
24
|
+
host: this.host,
|
|
25
|
+
port: this.port,
|
|
26
|
+
username: this.username
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Method to get the full config for use by connection manager
|
|
30
|
+
getFullConfig() {
|
|
31
|
+
return this.config;
|
|
32
|
+
}
|
|
33
|
+
constructor(config) {
|
|
34
|
+
super();
|
|
35
|
+
this._isConnected = false;
|
|
36
|
+
this.isPM2Installed = false;
|
|
37
|
+
this.client = new ssh2_1.Client();
|
|
38
|
+
this.config = config;
|
|
39
|
+
// Initialize public properties
|
|
40
|
+
this.name = config.name || config.host;
|
|
41
|
+
this.host = config.host;
|
|
42
|
+
this.port = config.port || 22;
|
|
43
|
+
this.username = config.username;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if the connection is active
|
|
47
|
+
*/
|
|
48
|
+
isConnected() {
|
|
49
|
+
return this._isConnected;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Connect to the remote server
|
|
53
|
+
*/ async connect() {
|
|
54
|
+
if (this._isConnected) {
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
this.client.on('ready', () => {
|
|
59
|
+
this._isConnected = true;
|
|
60
|
+
console.log(`Successfully connected to ${this.config.host}`);
|
|
61
|
+
this.emit('connected');
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
this.client.on('error', (err) => {
|
|
65
|
+
this._isConnected = false;
|
|
66
|
+
console.error(`SSH connection error for ${this.config.host}:`, err.message);
|
|
67
|
+
reject(err);
|
|
68
|
+
});
|
|
69
|
+
this.client.on('end', () => {
|
|
70
|
+
this._isConnected = false;
|
|
71
|
+
console.log(`Disconnected from ${this.config.host}`);
|
|
72
|
+
this.emit('disconnected');
|
|
73
|
+
});
|
|
74
|
+
// Attempt connection with the provided configuration
|
|
75
|
+
const connectionConfig = {
|
|
76
|
+
host: this.config.host,
|
|
77
|
+
port: this.config.port || 22,
|
|
78
|
+
username: this.config.username,
|
|
79
|
+
// Add reasonable timeouts
|
|
80
|
+
readyTimeout: 10000,
|
|
81
|
+
keepaliveInterval: 30000
|
|
82
|
+
};
|
|
83
|
+
// Debug output
|
|
84
|
+
console.log(`Connecting to ${this.config.host}:${this.config.port} as ${this.config.username}`);
|
|
85
|
+
console.log(`Authentication methods available: ${this.config.password ? 'Password' : 'No password'}, ${this.config.privateKey ? 'Private key' : 'No private key'}`);
|
|
86
|
+
// Add authentication method
|
|
87
|
+
if (this.config.privateKey) {
|
|
88
|
+
connectionConfig.privateKey = this.config.privateKey;
|
|
89
|
+
if (this.config.passphrase) {
|
|
90
|
+
connectionConfig.passphrase = this.config.passphrase;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (this.config.password) {
|
|
94
|
+
connectionConfig.password = this.config.password;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.error('No authentication method provided for', this.config.host);
|
|
98
|
+
return reject(new Error('No authentication method provided'));
|
|
99
|
+
}
|
|
100
|
+
this.client.connect(connectionConfig);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Disconnect from the remote server
|
|
105
|
+
*/
|
|
106
|
+
disconnect() {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
if (!this._isConnected) {
|
|
109
|
+
resolve();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.client.once('end', () => {
|
|
113
|
+
this._isConnected = false;
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
this.client.end();
|
|
117
|
+
});
|
|
118
|
+
} /**
|
|
119
|
+
* Execute a command on the remote server
|
|
120
|
+
* @param command The command to execute
|
|
121
|
+
* @param forceSudo Whether to force using sudo for this specific command
|
|
122
|
+
*/
|
|
123
|
+
async executeCommand(command, forceSudo) {
|
|
124
|
+
// Connect if not already connected
|
|
125
|
+
if (!this._isConnected) {
|
|
126
|
+
await this.connect();
|
|
127
|
+
}
|
|
128
|
+
// Apply sudo if configured or explicitly requested for this command
|
|
129
|
+
const useElevatedPrivileges = forceSudo || this.config.useSudo;
|
|
130
|
+
let finalCommand = command;
|
|
131
|
+
// Only apply sudo if it's requested and we have a password
|
|
132
|
+
if (useElevatedPrivileges && this.config.password) {
|
|
133
|
+
finalCommand = `echo '${this.config.password}' | sudo -S ${command}`;
|
|
134
|
+
}
|
|
135
|
+
console.log(`Executing command: ${useElevatedPrivileges ? '[sudo] ' : ''}${command}`);
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
this.client.exec(finalCommand, (err, channel) => {
|
|
138
|
+
if (err) {
|
|
139
|
+
console.error('Error executing command:', err);
|
|
140
|
+
return reject(err);
|
|
141
|
+
}
|
|
142
|
+
let stdout = '';
|
|
143
|
+
let stderr = '';
|
|
144
|
+
let exitCode = null;
|
|
145
|
+
channel.on('data', (data) => {
|
|
146
|
+
stdout += data.toString();
|
|
147
|
+
});
|
|
148
|
+
channel.stderr.on('data', (data) => {
|
|
149
|
+
stderr += data.toString();
|
|
150
|
+
});
|
|
151
|
+
channel.on('exit', (code) => {
|
|
152
|
+
exitCode = code;
|
|
153
|
+
});
|
|
154
|
+
channel.on('close', () => {
|
|
155
|
+
// Remove sudo password from stdout/stderr if present
|
|
156
|
+
if (useElevatedPrivileges && this.config.password) {
|
|
157
|
+
stdout = stdout.replace(this.config.password, '[PASSWORD REDACTED]');
|
|
158
|
+
stderr = stderr.replace(this.config.password, '[PASSWORD REDACTED]');
|
|
159
|
+
}
|
|
160
|
+
resolve({
|
|
161
|
+
stdout,
|
|
162
|
+
stderr,
|
|
163
|
+
code: exitCode
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
channel.on('error', (err) => {
|
|
167
|
+
console.error('Channel error:', err);
|
|
168
|
+
reject(err);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if PM2 is installed on the remote server
|
|
175
|
+
*/
|
|
176
|
+
async checkPM2Installation() {
|
|
177
|
+
try {
|
|
178
|
+
const result = await this.executeCommand('which pm2 || echo "not installed"');
|
|
179
|
+
this.isPM2Installed = !result.stdout.includes('not installed');
|
|
180
|
+
return this.isPM2Installed;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.isPM2Installed = false;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get PM2 processes from the remote server
|
|
189
|
+
*/
|
|
190
|
+
async getPM2Processes() {
|
|
191
|
+
try {
|
|
192
|
+
// Connect if not already connected
|
|
193
|
+
if (!this._isConnected) {
|
|
194
|
+
await this.connect();
|
|
195
|
+
}
|
|
196
|
+
// Check if PM2 is installed
|
|
197
|
+
if (!this.isPM2Installed) {
|
|
198
|
+
const isPM2Installed = await this.checkPM2Installation();
|
|
199
|
+
if (!isPM2Installed) {
|
|
200
|
+
throw new Error('PM2 is not installed on the remote server');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const result = await this.executeCommand('pm2 jlist');
|
|
204
|
+
// Clean the output to ensure it's valid JSON
|
|
205
|
+
// Sometimes pm2 jlist can include non-JSON data at the beginning or end
|
|
206
|
+
let cleanedOutput = result.stdout.trim();
|
|
207
|
+
// Find the beginning of the JSON array
|
|
208
|
+
const startIndex = cleanedOutput.indexOf('[');
|
|
209
|
+
// Find the end of the JSON array
|
|
210
|
+
const endIndex = cleanedOutput.lastIndexOf(']') + 1;
|
|
211
|
+
if (startIndex === -1 || endIndex === 0) {
|
|
212
|
+
console.log('Invalid PM2 output format, trying alternative approach');
|
|
213
|
+
// Try using pm2 list --format=json instead
|
|
214
|
+
const listResult = await this.executeCommand('pm2 list --format=json');
|
|
215
|
+
cleanedOutput = listResult.stdout.trim();
|
|
216
|
+
}
|
|
217
|
+
else if (startIndex > 0 || endIndex < cleanedOutput.length) {
|
|
218
|
+
// Extract just the JSON array part
|
|
219
|
+
cleanedOutput = cleanedOutput.substring(startIndex, endIndex);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const processList = JSON.parse(cleanedOutput);
|
|
223
|
+
// Format process data similar to local PM2 format
|
|
224
|
+
return processList.map((proc) => ({
|
|
225
|
+
name: proc.name,
|
|
226
|
+
pm_id: proc.pm_id,
|
|
227
|
+
status: proc.pm2_env ? proc.pm2_env.status : 'unknown',
|
|
228
|
+
cpu: proc.monit ? (proc.monit.cpu || 0).toFixed(1) : '0.0',
|
|
229
|
+
memory: proc.monit ? this.formatMemory(proc.monit.memory) : '0 B',
|
|
230
|
+
uptime: proc.pm2_env ? this.formatUptime(proc.pm2_env.pm_uptime) : 'N/A',
|
|
231
|
+
restarts: proc.pm2_env ? (proc.pm2_env.restart_time || 0) : 0
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error('Error parsing PM2 process list:', error);
|
|
236
|
+
console.log('Raw output:', result.stdout);
|
|
237
|
+
// Return an empty array instead of throwing
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error('Error getting PM2 processes:', error);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Format memory size to human readable format
|
|
248
|
+
*/
|
|
249
|
+
formatMemory(bytes) {
|
|
250
|
+
if (bytes === 0)
|
|
251
|
+
return '0 B';
|
|
252
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
253
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
254
|
+
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Format uptime to human readable format
|
|
258
|
+
*/ formatUptime(timestamp) {
|
|
259
|
+
if (!timestamp || isNaN(timestamp))
|
|
260
|
+
return 'N/A';
|
|
261
|
+
try {
|
|
262
|
+
const uptime = Date.now() - timestamp;
|
|
263
|
+
if (uptime < 0)
|
|
264
|
+
return 'N/A'; // Invalid timestamp
|
|
265
|
+
const seconds = Math.floor(uptime / 1000);
|
|
266
|
+
if (seconds < 60) {
|
|
267
|
+
return `${seconds}s`;
|
|
268
|
+
}
|
|
269
|
+
else if (seconds < 3600) {
|
|
270
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
271
|
+
}
|
|
272
|
+
else if (seconds < 86400) {
|
|
273
|
+
const hours = Math.floor(seconds / 3600);
|
|
274
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
275
|
+
return `${hours}h ${mins}m`;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const days = Math.floor(seconds / 86400);
|
|
279
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
280
|
+
return `${days}d ${hours}h`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.error('Error formatting uptime:', error);
|
|
285
|
+
return 'N/A';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Start a PM2 process
|
|
290
|
+
*/
|
|
291
|
+
async startPM2Process(processName) {
|
|
292
|
+
return await this.executeCommand(`pm2 start ${processName}`);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Stop a PM2 process
|
|
296
|
+
*/
|
|
297
|
+
async stopPM2Process(processName) {
|
|
298
|
+
return await this.executeCommand(`pm2 stop ${processName}`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Restart a PM2 process
|
|
302
|
+
*/
|
|
303
|
+
async restartPM2Process(processName) {
|
|
304
|
+
return await this.executeCommand(`pm2 restart ${processName}`);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Delete a PM2 process
|
|
308
|
+
*/
|
|
309
|
+
async deletePM2Process(processName) {
|
|
310
|
+
return await this.executeCommand(`pm2 delete ${processName}`);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get system information from the remote server
|
|
314
|
+
*/
|
|
315
|
+
async getSystemInfo() {
|
|
316
|
+
try {
|
|
317
|
+
// Connect if not already connected
|
|
318
|
+
if (!this._isConnected) {
|
|
319
|
+
await this.connect();
|
|
320
|
+
}
|
|
321
|
+
// Run a series of commands to get system information
|
|
322
|
+
const [hostname, platform, arch, nodeVersion, memInfo, cpuInfo] = await Promise.all([
|
|
323
|
+
this.executeCommand('hostname'),
|
|
324
|
+
this.executeCommand('uname -s'),
|
|
325
|
+
this.executeCommand('uname -m'),
|
|
326
|
+
this.executeCommand('node -v'),
|
|
327
|
+
this.executeCommand('free -b'),
|
|
328
|
+
this.executeCommand('cat /proc/cpuinfo | grep processor | wc -l')
|
|
329
|
+
]);
|
|
330
|
+
// Parse memory information
|
|
331
|
+
const memLines = memInfo.stdout.split('\n');
|
|
332
|
+
let totalMemory = 0;
|
|
333
|
+
let freeMemory = 0;
|
|
334
|
+
if (memLines.length > 1) {
|
|
335
|
+
const memValues = memLines[1].split(/\s+/);
|
|
336
|
+
if (memValues.length > 6) {
|
|
337
|
+
totalMemory = parseInt(memValues[1], 10);
|
|
338
|
+
freeMemory = parseInt(memValues[3], 10);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Get load average
|
|
342
|
+
const loadAvgResult = await this.executeCommand('cat /proc/loadavg');
|
|
343
|
+
const loadAvg = loadAvgResult.stdout.trim().split(' ').slice(0, 3).map(parseFloat);
|
|
344
|
+
return {
|
|
345
|
+
hostname: hostname.stdout.trim(),
|
|
346
|
+
platform: platform.stdout.trim(),
|
|
347
|
+
arch: arch.stdout.trim(),
|
|
348
|
+
nodeVersion: nodeVersion.stdout.trim(),
|
|
349
|
+
totalMemory: this.formatMemory(totalMemory),
|
|
350
|
+
freeMemory: this.formatMemory(freeMemory),
|
|
351
|
+
cpuCount: parseInt(cpuInfo.stdout.trim(), 10),
|
|
352
|
+
loadAverage: loadAvg
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.error('Error getting system info:', error);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get logs for a PM2 process
|
|
362
|
+
*/
|
|
363
|
+
async getPM2Logs(processName, lines = 100) {
|
|
364
|
+
return await this.executeCommand(`pm2 logs ${processName} --lines ${lines} --nostream`);
|
|
365
|
+
} /**
|
|
366
|
+
* Create a streaming log connection for a command
|
|
367
|
+
*/
|
|
368
|
+
async createLogStream(command, useSudo = false) {
|
|
369
|
+
if (!this._isConnected) {
|
|
370
|
+
throw new Error('Connection not established');
|
|
371
|
+
}
|
|
372
|
+
let finalCommand = command;
|
|
373
|
+
let isInitialized = false;
|
|
374
|
+
// Apply sudo if requested and we have a password
|
|
375
|
+
if (useSudo && this.config.useSudo && this.config.password) {
|
|
376
|
+
// Use echo to pipe the password to sudo for streaming commands
|
|
377
|
+
finalCommand = `echo '${this.config.password}' | sudo -S ${command}`;
|
|
378
|
+
}
|
|
379
|
+
console.log(`[createLogStream] Executing command: ${finalCommand.replace(this.config.password || '', '[PASSWORD]')}`);
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
this.client.exec(finalCommand, (err, stream) => {
|
|
382
|
+
var _a;
|
|
383
|
+
if (err) {
|
|
384
|
+
console.error(`[createLogStream] Exec error:`, err);
|
|
385
|
+
reject(err);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const logEmitter = new events_1.EventEmitter();
|
|
389
|
+
stream.on('data', (data) => {
|
|
390
|
+
const dataStr = data.toString();
|
|
391
|
+
console.log(`[createLogStream] Raw data:`, dataStr);
|
|
392
|
+
// Skip the sudo password prompt and initial setup messages
|
|
393
|
+
if (!isInitialized) {
|
|
394
|
+
if (dataStr.includes('[sudo] password') ||
|
|
395
|
+
dataStr.includes('Password:') ||
|
|
396
|
+
dataStr.trim() === '') {
|
|
397
|
+
return; // Skip initialization messages
|
|
398
|
+
}
|
|
399
|
+
isInitialized = true;
|
|
400
|
+
}
|
|
401
|
+
logEmitter.emit('data', dataStr);
|
|
402
|
+
});
|
|
403
|
+
(_a = stream.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
404
|
+
const dataStr = data.toString();
|
|
405
|
+
console.log(`[createLogStream] Stderr data:`, dataStr);
|
|
406
|
+
// Don't emit stderr data for sudo prompts or permission messages
|
|
407
|
+
if (!dataStr.includes('[sudo] password') &&
|
|
408
|
+
!dataStr.includes('Password:') &&
|
|
409
|
+
!dataStr.includes('Sorry, try again')) {
|
|
410
|
+
logEmitter.emit('data', dataStr);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
stream.on('close', (code) => {
|
|
414
|
+
console.log(`[createLogStream] Stream closed with code:`, code);
|
|
415
|
+
logEmitter.emit('close', code);
|
|
416
|
+
});
|
|
417
|
+
stream.on('error', (error) => {
|
|
418
|
+
console.error(`[createLogStream] Stream error:`, error);
|
|
419
|
+
logEmitter.emit('error', error);
|
|
420
|
+
});
|
|
421
|
+
// Add a method to kill the stream
|
|
422
|
+
logEmitter.kill = () => {
|
|
423
|
+
console.log(`[createLogStream] Killing stream`);
|
|
424
|
+
stream.close();
|
|
425
|
+
};
|
|
426
|
+
resolve(logEmitter);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
exports.RemoteConnection = RemoteConnection;
|
|
432
|
+
/**
|
|
433
|
+
* Connection manager to handle multiple remote connections
|
|
434
|
+
*/
|
|
435
|
+
class RemoteConnectionManager {
|
|
436
|
+
constructor() {
|
|
437
|
+
this.connections = new Map();
|
|
438
|
+
this.configFilePath = 'D:/Personal/ezpm2gui/src/server/config/remote-connections.json';
|
|
439
|
+
this.loadConnectionsFromDisk();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Create a new remote connection
|
|
443
|
+
* @param config Connection configuration
|
|
444
|
+
* @returns The connection ID
|
|
445
|
+
*/
|
|
446
|
+
createConnection(config) {
|
|
447
|
+
const connectionId = `${config.host}-${config.port}-${config.username}`;
|
|
448
|
+
if (this.connections.has(connectionId)) {
|
|
449
|
+
return connectionId;
|
|
450
|
+
}
|
|
451
|
+
const connection = new RemoteConnection(config);
|
|
452
|
+
this.connections.set(connectionId, connection);
|
|
453
|
+
// Save the updated connections to disk
|
|
454
|
+
this.saveConnectionsToDisk();
|
|
455
|
+
return connectionId;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Get a connection by ID
|
|
459
|
+
* @param connectionId The connection ID
|
|
460
|
+
*/
|
|
461
|
+
getConnection(connectionId) {
|
|
462
|
+
return this.connections.get(connectionId);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Close a connection by ID (disconnect but keep the configuration)
|
|
466
|
+
* @param connectionId The connection ID
|
|
467
|
+
*/
|
|
468
|
+
async closeConnection(connectionId) {
|
|
469
|
+
const connection = this.connections.get(connectionId);
|
|
470
|
+
if (connection) {
|
|
471
|
+
await connection.disconnect();
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Close all connections
|
|
478
|
+
*/ async closeAllConnections() {
|
|
479
|
+
const closePromises = Array.from(this.connections.values()).map((conn) => conn.disconnect());
|
|
480
|
+
await Promise.all(closePromises);
|
|
481
|
+
this.connections.clear();
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get all connections
|
|
485
|
+
* @returns Map of all connections
|
|
486
|
+
*/
|
|
487
|
+
getAllConnections() {
|
|
488
|
+
return this.connections;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Delete a connection by ID
|
|
492
|
+
* @param connectionId The connection ID
|
|
493
|
+
* @returns True if the connection was deleted, false if it didn't exist
|
|
494
|
+
*/
|
|
495
|
+
deleteConnection(connectionId) {
|
|
496
|
+
const deleted = this.connections.delete(connectionId);
|
|
497
|
+
if (deleted) {
|
|
498
|
+
// Save the updated connections to disk
|
|
499
|
+
this.saveConnectionsToDisk();
|
|
500
|
+
}
|
|
501
|
+
return deleted;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Load connection configurations from disk
|
|
505
|
+
*/
|
|
506
|
+
loadConnectionsFromDisk() {
|
|
507
|
+
try {
|
|
508
|
+
const fs = require('fs');
|
|
509
|
+
const path = require('path');
|
|
510
|
+
const { decrypt } = require('./encryption');
|
|
511
|
+
// Ensure the config directory exists
|
|
512
|
+
const configDir = path.dirname(this.configFilePath);
|
|
513
|
+
if (!fs.existsSync(configDir)) {
|
|
514
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
515
|
+
}
|
|
516
|
+
// Create the file with default content if it doesn't exist
|
|
517
|
+
if (!fs.existsSync(this.configFilePath)) {
|
|
518
|
+
fs.writeFileSync(this.configFilePath, JSON.stringify({ connections: [] }, null, 2));
|
|
519
|
+
}
|
|
520
|
+
// Read the configuration file
|
|
521
|
+
const data = fs.readFileSync(this.configFilePath, 'utf8');
|
|
522
|
+
const config = JSON.parse(data);
|
|
523
|
+
if (config && Array.isArray(config.connections)) {
|
|
524
|
+
// Create connections from saved configs
|
|
525
|
+
config.connections.forEach((conn) => {
|
|
526
|
+
const connectionConfig = {
|
|
527
|
+
name: conn.name,
|
|
528
|
+
host: conn.host,
|
|
529
|
+
port: conn.port,
|
|
530
|
+
username: conn.username,
|
|
531
|
+
// Decrypt sensitive data if it exists
|
|
532
|
+
password: conn.password ? decrypt(conn.password) : undefined,
|
|
533
|
+
privateKey: conn.privateKey ? decrypt(conn.privateKey) : undefined,
|
|
534
|
+
passphrase: conn.passphrase ? decrypt(conn.passphrase) : undefined,
|
|
535
|
+
useSudo: conn.useSudo
|
|
536
|
+
};
|
|
537
|
+
// Re-create the connection with the original ID
|
|
538
|
+
const connection = new RemoteConnection(connectionConfig);
|
|
539
|
+
this.connections.set(conn.id, connection);
|
|
540
|
+
});
|
|
541
|
+
console.log(`Loaded ${config.connections.length} remote connections from disk`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
console.error('Error loading remote connections from disk:', error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Save connection configurations to disk
|
|
550
|
+
*/
|
|
551
|
+
saveConnectionsToDisk() {
|
|
552
|
+
try {
|
|
553
|
+
const fs = require('fs');
|
|
554
|
+
const path = require('path');
|
|
555
|
+
const { encrypt } = require('./encryption');
|
|
556
|
+
// Ensure the config directory exists
|
|
557
|
+
const configDir = path.dirname(this.configFilePath);
|
|
558
|
+
if (!fs.existsSync(configDir)) {
|
|
559
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
560
|
+
}
|
|
561
|
+
// Convert connections map to serializable array with encrypted sensitive data
|
|
562
|
+
const savedConnections = Array.from(this.connections.entries()).map(([id, conn]) => {
|
|
563
|
+
// Get the original configuration to access sensitive data
|
|
564
|
+
const config = conn.getFullConfig();
|
|
565
|
+
return {
|
|
566
|
+
id,
|
|
567
|
+
name: conn.name,
|
|
568
|
+
host: conn.host,
|
|
569
|
+
port: conn.port,
|
|
570
|
+
username: conn.username,
|
|
571
|
+
password: config.password ? encrypt(config.password) : undefined,
|
|
572
|
+
privateKey: config.privateKey ? encrypt(config.privateKey) : undefined,
|
|
573
|
+
passphrase: config.passphrase ? encrypt(config.passphrase) : undefined,
|
|
574
|
+
useSudo: config.useSudo
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
// Write to file
|
|
578
|
+
fs.writeFileSync(this.configFilePath, JSON.stringify({
|
|
579
|
+
connections: savedConnections
|
|
580
|
+
}, null, 2));
|
|
581
|
+
console.log(`Saved ${savedConnections.length} remote connections to disk`);
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
console.error('Error saving remote connections to disk:', error);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
exports.RemoteConnectionManager = RemoteConnectionManager;
|
|
589
|
+
// Create a singleton instance of the connection manager
|
|
590
|
+
exports.remoteConnectionManager = new RemoteConnectionManager();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const multer_1 = __importDefault(require("multer"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
// Create uploads directory if it doesn't exist
|
|
10
|
+
const uploadsDir = path_1.default.join(process.cwd(), 'uploads');
|
|
11
|
+
if (!fs_1.default.existsSync(uploadsDir)) {
|
|
12
|
+
fs_1.default.mkdirSync(uploadsDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
// Configure multer for file upload
|
|
15
|
+
const storage = multer_1.default.diskStorage({
|
|
16
|
+
destination: function (req, file, cb) {
|
|
17
|
+
cb(null, uploadsDir);
|
|
18
|
+
},
|
|
19
|
+
filename: function (req, file, cb) {
|
|
20
|
+
// Use original filename to preserve extension
|
|
21
|
+
cb(null, file.originalname);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
// Create the multer instance
|
|
25
|
+
const upload = (0, multer_1.default)({
|
|
26
|
+
storage: storage,
|
|
27
|
+
fileFilter: (req, file, cb) => {
|
|
28
|
+
// Allow only JavaScript and TypeScript files
|
|
29
|
+
const allowedExtensions = ['.js', '.ts', '.mjs', '.cjs'];
|
|
30
|
+
const ext = path_1.default.extname(file.originalname).toLowerCase();
|
|
31
|
+
if (allowedExtensions.includes(ext)) {
|
|
32
|
+
cb(null, true);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
cb(new Error('Only JavaScript and TypeScript files are allowed'));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
exports.default = upload;
|