Add LXC container backup functionality

- Add backup capability before updates or as standalone action
- Implement storage service to fetch and parse backup-capable storages from PVE nodes
- Add backup storage selection modal for user choice
- Support backup+update flow with sequential execution
- Add standalone backup option in Actions menu
- Add storage viewer in server section to show available storages
- Parse /etc/pve/storage.cfg to identify backup-capable storages
- Cache storage data for performance
- Handle backup failures gracefully (warn but allow update to proceed)
This commit is contained in:
Michel Roegl-Brunner
2025-11-14 10:30:27 +01:00
parent 4ea49be97d
commit d50ea55e6d
4 changed files with 173 additions and 136 deletions

View File

@@ -4,7 +4,6 @@ import { getDatabase } from "~/server/database-prisma";
import { createHash } from "crypto";
import type { Server } from "~/types/server";
import { getStorageService } from "~/server/services/storageService";
import { SSHService } from "~/server/ssh-service";
// Helper function to parse raw LXC config into structured data
function parseRawConfig(rawConfig: string): any {
@@ -2063,6 +2062,7 @@ EOFCONFIG`;
}
const storageService = getStorageService();
const { default: SSHService } = await import('~/server/ssh-service');
const sshService = new SSHService();
// Test SSH connection first
@@ -2118,6 +2118,7 @@ EOFCONFIG`;
};
}
const { default: SSHService } = await import('~/server/ssh-service');
const sshService = new SSHService();
// Test SSH connection first

View File

@@ -29,7 +29,12 @@ class StorageService {
let currentStorage: Partial<Storage> | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const rawLine = lines[i];
if (!rawLine) continue;
// Check if line is indented (has leading whitespace/tabs) BEFORE trimming
const isIndented = /^[\s\t]/.test(rawLine);
const line = rawLine.trim();
// Skip empty lines and comments
if (!line || line.startsWith('#')) {
@@ -37,29 +42,34 @@ class StorageService {
}
// Check if this is a storage definition line (format: "type: name")
const storageMatch = line.match(/^(\w+):\s*(.+)$/);
if (storageMatch) {
// Save previous storage if exists
if (currentStorage && currentStorage.name) {
storages.push(this.finalizeStorage(currentStorage));
// Storage definitions are NOT indented
if (!isIndented) {
const storageMatch = line.match(/^(\w+):\s*(.+)$/);
if (storageMatch && storageMatch[1] && storageMatch[2]) {
// Save previous storage if exists
if (currentStorage && currentStorage.name) {
storages.push(this.finalizeStorage(currentStorage));
}
// Start new storage
currentStorage = {
type: storageMatch[1],
name: storageMatch[2],
content: [],
supportsBackup: false,
};
continue;
}
// Start new storage
currentStorage = {
type: storageMatch[1],
name: storageMatch[2],
content: [],
supportsBackup: false,
};
continue;
}
// Parse storage properties (indented lines)
if (currentStorage && /^\s/.test(line)) {
const propertyMatch = line.match(/^\s+(\w+)\s+(.+)$/);
if (propertyMatch) {
const key = propertyMatch[1];
const value = propertyMatch[2];
// Parse storage properties (indented lines - can be tabs or spaces)
if (currentStorage && isIndented) {
// Split on first whitespace (space or tab) to separate key and value
const match = line.match(/^(\S+)\s+(.+)$/);
if (match && match[1] && match[2]) {
const key = match[1];
const value = match[2].trim();
switch (key) {
case 'content':
@@ -73,7 +83,9 @@ class StorageService {
break;
default:
// Store other properties
(currentStorage as any)[key] = value;
if (key) {
(currentStorage as any)[key] = value;
}
}
}
}