Initial for Backup function

This commit is contained in:
Michel Roegl-Brunner
2025-11-14 08:44:33 +01:00
parent dab2da4b70
commit 4ea49be97d
10 changed files with 1224 additions and 39 deletions

196
server.js
View File

@@ -276,13 +276,15 @@ class ScriptExecutionHandler {
* @param {WebSocketMessage} message
*/
async handleMessage(ws, message) {
const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, containerId } = message;
const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, isBackup, containerId, storage, backupStorage } = message;
switch (action) {
case 'start':
if (scriptPath && executionId) {
if (isUpdate && containerId) {
await this.startUpdateExecution(ws, containerId, executionId, mode, server);
if (isBackup && containerId && storage) {
await this.startBackupExecution(ws, containerId, executionId, storage, mode, server);
} else if (isUpdate && containerId) {
await this.startUpdateExecution(ws, containerId, executionId, mode, server, backupStorage);
} else if (isShell && containerId) {
await this.startShellExecution(ws, containerId, executionId, mode, server);
} else {
@@ -660,6 +662,115 @@ class ScriptExecutionHandler {
}
}
/**
* Start backup execution
* @param {ExtendedWebSocket} ws
* @param {string} containerId
* @param {string} executionId
* @param {string} storage
* @param {string} mode
* @param {ServerInfo|null} server
*/
async startBackupExecution(ws, containerId, executionId, storage, mode = 'local', server = null) {
try {
// Send start message
this.sendMessage(ws, {
type: 'start',
data: `Starting backup for container ${containerId} to storage ${storage}...`,
timestamp: Date.now()
});
if (mode === 'ssh' && server) {
await this.startSSHBackupExecution(ws, containerId, executionId, storage, server);
} else {
this.sendMessage(ws, {
type: 'error',
data: 'Backup is only supported via SSH',
timestamp: Date.now()
});
}
} catch (error) {
this.sendMessage(ws, {
type: 'error',
data: `Failed to start backup: ${error instanceof Error ? error.message : String(error)}`,
timestamp: Date.now()
});
}
}
/**
* Start SSH backup execution
* @param {ExtendedWebSocket} ws
* @param {string} containerId
* @param {string} executionId
* @param {string} storage
* @param {ServerInfo} server
*/
async startSSHBackupExecution(ws, containerId, executionId, storage, server) {
const sshService = getSSHExecutionService();
try {
const backupCommand = `vzdump ${containerId} --storage ${storage} --mode snapshot`;
const execution = await sshService.executeCommand(
server,
backupCommand,
/** @param {string} data */
(data) => {
this.sendMessage(ws, {
type: 'output',
data: data,
timestamp: Date.now()
});
},
/** @param {string} error */
(error) => {
this.sendMessage(ws, {
type: 'error',
data: error,
timestamp: Date.now()
});
},
/** @param {number} code */
(code) => {
if (code === 0) {
this.sendMessage(ws, {
type: 'end',
data: `Backup completed successfully with exit code: ${code}`,
timestamp: Date.now()
});
} else {
this.sendMessage(ws, {
type: 'error',
data: `Backup failed with exit code: ${code}`,
timestamp: Date.now()
});
this.sendMessage(ws, {
type: 'end',
data: `Backup execution ended with exit code: ${code}`,
timestamp: Date.now()
});
}
this.activeExecutions.delete(executionId);
}
);
// Store the execution
this.activeExecutions.set(executionId, {
process: /** @type {any} */ (execution).process,
ws
});
} catch (error) {
this.sendMessage(ws, {
type: 'error',
data: `SSH backup execution failed: ${error instanceof Error ? error.message : String(error)}`,
timestamp: Date.now()
});
}
}
/**
* Start update execution (pct enter + update command)
* @param {ExtendedWebSocket} ws
@@ -667,11 +778,86 @@ class ScriptExecutionHandler {
* @param {string} executionId
* @param {string} mode
* @param {ServerInfo|null} server
* @param {string} [backupStorage] - Optional storage to backup to before update
*/
async startUpdateExecution(ws, containerId, executionId, mode = 'local', server = null) {
async startUpdateExecution(ws, containerId, executionId, mode = 'local', server = null, backupStorage = null) {
try {
// If backup storage is provided, run backup first
if (backupStorage && mode === 'ssh' && server) {
this.sendMessage(ws, {
type: 'start',
data: `Starting backup before update for container ${containerId}...`,
timestamp: Date.now()
});
// Create a separate execution ID for backup
const backupExecutionId = `backup_${executionId}`;
let backupCompleted = false;
let backupSucceeded = false;
// Run backup and wait for it to complete
await new Promise<void>((resolve) => {
// Create a wrapper websocket that forwards messages and tracks completion
const backupWs = {
send: (data) => {
try {
const message = typeof data === 'string' ? JSON.parse(data) : data;
// Forward all messages to the main websocket
ws.send(JSON.stringify(message));
// Check for completion
if (message.type === 'end') {
backupCompleted = true;
backupSucceeded = !message.data.includes('failed') && !message.data.includes('exit code:');
if (!backupSucceeded) {
// Backup failed, but we'll still allow update (per requirement 1b)
ws.send(JSON.stringify({
type: 'output',
data: '\n⚠ Backup failed, but proceeding with update as requested...\n',
timestamp: Date.now()
}));
}
resolve();
} else if (message.type === 'error' && message.data.includes('Backup failed')) {
backupCompleted = true;
backupSucceeded = false;
ws.send(JSON.stringify({
type: 'output',
data: '\n⚠ Backup failed, but proceeding with update as requested...\n',
timestamp: Date.now()
}));
resolve();
}
} catch (e) {
// If parsing fails, just forward the raw data
ws.send(data);
}
}
};
// Start backup execution
this.startSSHBackupExecution(backupWs, containerId, backupExecutionId, backupStorage, server)
.catch((error) => {
// Backup failed to start, but allow update to proceed
if (!backupCompleted) {
backupCompleted = true;
backupSucceeded = false;
ws.send(JSON.stringify({
type: 'output',
data: `\n⚠️ Backup error: ${error.message}. Proceeding with update...\n`,
timestamp: Date.now()
}));
resolve();
}
});
});
// Small delay before starting update
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Send start message
// Send start message for update
this.sendMessage(ws, {
type: 'start',
data: `Starting update for container ${containerId}...`,