Implement real-time restore progress updates with polling

- Add restore.log file writing in restoreService.ts for progress tracking
- Create getRestoreProgress query endpoint for polling restore logs
- Implement polling-based progress updates in BackupsTab (1 second interval)
- Update LoadingModal to display all progress logs with auto-scroll
- Remove console.log debug output from restoreService
- Add static 'Restore in progress' text under spinner
- Show success checkmark when restore completes
- Prevent modal dismissal during restore, allow ESC/X button when complete
- Remove step prefixes from log messages for cleaner output
- Keep success/error modals open until user dismisses manually
This commit is contained in:
Michel Roegl-Brunner
2025-11-14 15:43:33 +01:00
parent 33a5b8e4d0
commit 570eea41b9
5 changed files with 276 additions and 132 deletions

10
restore.log Normal file
View File

@@ -0,0 +1,10 @@
Starting restore...
Reading container configuration...
Stopping container...
Destroying container...
Logging into PBS...
Downloading backup from PBS...
Packing backup folder...
Restoring container...
Cleaning up temporary files...
Restore completed successfully

View File

@@ -42,6 +42,7 @@ export function BackupsTab() {
const [restoreProgress, setRestoreProgress] = useState<string[]>([]); const [restoreProgress, setRestoreProgress] = useState<string[]>([]);
const [restoreSuccess, setRestoreSuccess] = useState(false); const [restoreSuccess, setRestoreSuccess] = useState(false);
const [restoreError, setRestoreError] = useState<string | null>(null); const [restoreError, setRestoreError] = useState<string | null>(null);
const [shouldPollRestore, setShouldPollRestore] = useState(false);
const { data: backupsData, refetch: refetchBackups, isLoading } = api.backups.getAllBackupsGrouped.useQuery(); const { data: backupsData, refetch: refetchBackups, isLoading } = api.backups.getAllBackupsGrouped.useQuery();
const discoverMutation = api.backups.discoverBackups.useMutation({ const discoverMutation = api.backups.discoverBackups.useMutation({
@@ -50,32 +51,67 @@ export function BackupsTab() {
}, },
}); });
// Poll for restore progress
const { data: restoreLogsData } = api.backups.getRestoreProgress.useQuery(undefined, {
enabled: shouldPollRestore,
refetchInterval: 1000, // Poll every second
refetchIntervalInBackground: true,
});
// Update restore progress when log data changes
useEffect(() => {
if (restoreLogsData?.success && restoreLogsData.logs) {
setRestoreProgress(restoreLogsData.logs);
// Stop polling when restore is complete
if (restoreLogsData.isComplete) {
setShouldPollRestore(false);
// Check if restore was successful or failed
const lastLog = restoreLogsData.logs[restoreLogsData.logs.length - 1] || '';
if (lastLog.includes('Restore completed successfully')) {
setRestoreSuccess(true);
setRestoreError(null);
} else if (lastLog.includes('Error:') || lastLog.includes('failed')) {
setRestoreError(lastLog);
setRestoreSuccess(false);
}
}
}
}, [restoreLogsData]);
const restoreMutation = api.backups.restoreBackup.useMutation({ const restoreMutation = api.backups.restoreBackup.useMutation({
onMutate: () => { onMutate: () => {
// Show progress immediately when mutation starts // Start polling for progress
setShouldPollRestore(true);
setRestoreProgress(['Starting restore...']); setRestoreProgress(['Starting restore...']);
setRestoreError(null); setRestoreError(null);
setRestoreSuccess(false); setRestoreSuccess(false);
}, },
onSuccess: (result) => { onSuccess: (result) => {
// Stop polling - progress will be updated from logs
setShouldPollRestore(false);
if (result.success) { if (result.success) {
setRestoreSuccess(true); // Update progress with all messages from backend (fallback if polling didn't work)
// Update progress with all messages from backend const progressMessages = restoreProgress.length > 0 ? restoreProgress : (result.progress?.map(p => p.message) || ['Restore completed successfully']);
const progressMessages = result.progress?.map(p => p.message) || ['Restore completed successfully'];
setRestoreProgress(progressMessages); setRestoreProgress(progressMessages);
setRestoreSuccess(true);
setRestoreError(null);
setRestoreConfirmOpen(false); setRestoreConfirmOpen(false);
setSelectedBackup(null); setSelectedBackup(null);
// Clear success message after 5 seconds // Keep success message visible - user can dismiss manually
setTimeout(() => {
setRestoreSuccess(false);
setRestoreProgress([]);
}, 5000);
} else { } else {
setRestoreError(result.error || 'Restore failed'); setRestoreError(result.error || 'Restore failed');
setRestoreProgress(result.progress?.map(p => p.message) || []); setRestoreProgress(result.progress?.map(p => p.message) || restoreProgress);
setRestoreSuccess(false);
setRestoreConfirmOpen(false);
setSelectedBackup(null);
// Keep error message visible - user can dismiss manually
} }
}, },
onError: (error) => { onError: (error) => {
// Stop polling on error
setShouldPollRestore(false);
setRestoreError(error.message || 'Restore failed'); setRestoreError(error.message || 'Restore failed');
setRestoreConfirmOpen(false); setRestoreConfirmOpen(false);
setSelectedBackup(null); setSelectedBackup(null);
@@ -376,59 +412,69 @@ export function BackupsTab() {
)} )}
{/* Restore Progress Modal */} {/* Restore Progress Modal */}
{restoreMutation.isPending && ( {(restoreMutation.isPending || (restoreSuccess && restoreProgress.length > 0)) && (
<LoadingModal <LoadingModal
isOpen={true} isOpen={true}
action={currentProgressText} action={currentProgressText}
logs={restoreProgress}
isComplete={restoreSuccess}
title="Restore in progress"
onClose={() => {
setRestoreSuccess(false);
setRestoreProgress([]);
}}
/> />
)} )}
{/* Restore Progress Details - Show during restore */}
{restoreMutation.isPending && (
<div className="bg-card rounded-lg border border-border p-4">
<div className="flex items-center gap-2 mb-2">
<RefreshCw className="h-4 w-4 animate-spin text-primary" />
<span className="font-medium text-foreground">Restoring backup...</span>
</div>
{restoreProgress.length > 0 && (
<div className="space-y-1">
{restoreProgress.map((message, index) => (
<p key={index} className="text-sm text-muted-foreground">
{message}
</p>
))}
</div>
)}
</div>
)}
{/* Restore Success */} {/* Restore Success */}
{restoreSuccess && ( {restoreSuccess && (
<div className="bg-success/10 border border-success/20 rounded-lg p-4"> <div className="bg-success/10 border border-success/20 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center justify-between mb-2">
<CheckCircle className="h-5 w-5 text-success" /> <div className="flex items-center gap-2">
<span className="font-medium text-success">Restore completed successfully!</span> <CheckCircle className="h-5 w-5 text-success" />
</div> <span className="font-medium text-success">Restore Completed Successfully</span>
{restoreProgress.length > 0 && (
<div className="space-y-1 mt-2">
{restoreProgress.map((message, index) => (
<p key={index} className="text-sm text-muted-foreground">
{message}
</p>
))}
</div> </div>
)} <Button
variant="ghost"
size="sm"
onClick={() => {
setRestoreSuccess(false);
setRestoreProgress([]);
}}
className="h-6 w-6 p-0"
>
×
</Button>
</div>
<p className="text-sm text-muted-foreground">
The container has been restored from backup.
</p>
</div> </div>
)} )}
{/* Restore Error */} {/* Restore Error */}
{restoreError && ( {restoreError && (
<div className="bg-destructive/10 border border-destructive rounded-lg p-4"> <div className="bg-error/10 border border-error/20 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center justify-between mb-2">
<AlertCircle className="h-5 w-5 text-destructive" /> <div className="flex items-center gap-2">
<span className="font-medium text-destructive">Restore failed</span> <AlertCircle className="h-5 w-5 text-error" />
<span className="font-medium text-error">Restore Failed</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
setRestoreError(null);
setRestoreProgress([]);
}}
className="h-6 w-6 p-0"
>
×
</Button>
</div> </div>
<p className="text-sm text-destructive">{restoreError}</p> <p className="text-sm text-muted-foreground">
{restoreError}
</p>
{restoreProgress.length > 0 && ( {restoreProgress.length > 0 && (
<div className="space-y-1 mt-2"> <div className="space-y-1 mt-2">
{restoreProgress.map((message, index) => ( {restoreProgress.map((message, index) => (

View File

@@ -1,36 +1,84 @@
'use client'; 'use client';
import { Loader2 } from 'lucide-react'; import { Loader2, CheckCircle, X } from 'lucide-react';
import { useRegisterModal } from './modal/ModalStackProvider'; import { useRegisterModal } from './modal/ModalStackProvider';
import { useEffect, useRef } from 'react';
import { Button } from './ui/button';
interface LoadingModalProps { interface LoadingModalProps {
isOpen: boolean; isOpen: boolean;
action: string; action: string;
logs?: string[];
isComplete?: boolean;
title?: string;
onClose?: () => void;
} }
export function LoadingModal({ isOpen, action }: LoadingModalProps) { export function LoadingModal({ isOpen, action, logs = [], isComplete = false, title, onClose }: LoadingModalProps) {
useRegisterModal(isOpen, { id: 'loading-modal', allowEscape: false, onClose: () => null }); // Allow dismissing with ESC only when complete, prevent during running
useRegisterModal(isOpen, { id: 'loading-modal', allowEscape: isComplete, onClose: onClose || (() => null) });
const logsEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom when new logs arrive
useEffect(() => {
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [logs]);
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-card rounded-lg shadow-xl max-w-md w-full border border-border p-8"> <div className="bg-card rounded-lg shadow-xl max-w-2xl w-full border border-border p-8 max-h-[80vh] flex flex-col relative">
{/* Close button - only show when complete */}
{isComplete && onClose && (
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="absolute top-4 right-4 h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
)}
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<div className="relative"> <div className="relative">
<Loader2 className="h-12 w-12 animate-spin text-primary" /> {isComplete ? (
<div className="absolute inset-0 rounded-full border-2 border-primary/20 animate-pulse"></div> <CheckCircle className="h-12 w-12 text-success" />
) : (
<>
<Loader2 className="h-12 w-12 animate-spin text-primary" />
<div className="absolute inset-0 rounded-full border-2 border-primary/20 animate-pulse"></div>
</>
)}
</div> </div>
<div className="text-center">
<h3 className="text-lg font-semibold text-card-foreground mb-2"> {/* Static title text */}
Processing {title && (
</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{action} {title}
</p> </p>
<p className="text-xs text-muted-foreground mt-2"> )}
Please wait...
</p> {/* Log output */}
</div> {logs.length > 0 && (
<div className="w-full bg-card border border-border rounded-lg p-4 font-mono text-xs text-chart-2 max-h-[60vh] overflow-y-auto terminal-output">
{logs.map((log, index) => (
<div key={index} className="mb-1 whitespace-pre-wrap break-words">
{log}
</div>
))}
<div ref={logsEndRef} />
</div>
)}
{!isComplete && (
<div className="flex space-x-1">
<div className="w-2 h-2 bg-primary rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,10 @@ import { createTRPCRouter, publicProcedure } from '~/server/api/trpc';
import { getDatabase } from '~/server/database-prisma'; import { getDatabase } from '~/server/database-prisma';
import { getBackupService } from '~/server/services/backupService'; import { getBackupService } from '~/server/services/backupService';
import { getRestoreService } from '~/server/services/restoreService'; import { getRestoreService } from '~/server/services/restoreService';
import { readFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import stripAnsi from 'strip-ansi';
export const backupsRouter = createTRPCRouter({ export const backupsRouter = createTRPCRouter({
// Get all backups grouped by container ID // Get all backups grouped by container ID
@@ -89,6 +93,49 @@ export const backupsRouter = createTRPCRouter({
} }
}), }),
// Get restore progress from log file
getRestoreProgress: publicProcedure
.query(async () => {
try {
const logPath = join(process.cwd(), 'restore.log');
if (!existsSync(logPath)) {
return {
success: true,
logs: [],
isComplete: false
};
}
const logs = await readFile(logPath, 'utf-8');
const logLines = logs.split('\n')
.filter(line => line.trim())
.map(line => stripAnsi(line)); // Strip ANSI color codes
// Check if restore is complete by looking for completion indicators
const isComplete = logLines.some(line =>
line.includes('complete: Restore completed successfully') ||
line.includes('error: Error:') ||
line.includes('Restore completed successfully') ||
line.includes('Restore failed')
);
return {
success: true,
logs: logLines,
isComplete
};
} catch (error) {
console.error('Error reading restore logs:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to read restore logs',
logs: [],
isComplete: false
};
}
}),
// Restore backup // Restore backup
restoreBackup: publicProcedure restoreBackup: publicProcedure
.input(z.object({ .input(z.object({

View File

@@ -4,6 +4,9 @@ import { getStorageService } from './storageService';
import { getDatabase } from '../database-prisma'; import { getDatabase } from '../database-prisma';
import type { Server } from '~/types/server'; import type { Server } from '~/types/server';
import type { Storage } from './storageService'; import type { Storage } from './storageService';
import { writeFile, readFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
export interface RestoreProgress { export interface RestoreProgress {
step: string; step: string;
@@ -73,26 +76,25 @@ class RestoreService {
} }
return null; return null;
} catch (error) { } catch (error) {
console.error(`Error reading LXC config for CT ${ctId}:`, error); // Try fallback to database
// Try fallback to database try {
try { const installedScripts = await db.getAllInstalledScripts();
const installedScripts = await db.getAllInstalledScripts(); const script = installedScripts.find((s: any) => s.container_id === ctId && s.server_id === server.id);
const script = installedScripts.find((s: any) => s.container_id === ctId && s.server_id === server.id); if (script) {
if (script) { const lxcConfig = await db.getLXCConfigByScriptId(script.id);
const lxcConfig = await db.getLXCConfigByScriptId(script.id); if (lxcConfig?.rootfs_storage) {
if (lxcConfig?.rootfs_storage) { const match = lxcConfig.rootfs_storage.match(/^([^:]+):/);
const match = lxcConfig.rootfs_storage.match(/^([^:]+):/); if (match && match[1]) {
if (match && match[1]) { return match[1].trim();
return match[1].trim(); }
} }
} }
} catch (dbError) {
// Ignore database error
} }
} catch (dbError) { return null;
console.error(`Error getting storage from database:`, dbError);
} }
return null;
}
} }
/** /**
@@ -132,7 +134,6 @@ class RestoreService {
(error: string) => { (error: string) => {
// Check if error is about container not existing // Check if error is about container not existing
if (error.includes('does not exist') || error.includes('not found')) { if (error.includes('does not exist') || error.includes('not found')) {
console.log(`[RestoreService] Container ${ctId} does not exist`);
resolve(); // Container doesn't exist, that's fine resolve(); // Container doesn't exist, that's fine
} else { } else {
reject(new Error(`Destroy failed: ${error}`)); reject(new Error(`Destroy failed: ${error}`));
@@ -145,7 +146,6 @@ class RestoreService {
} else { } else {
// Check if error is about container not existing // Check if error is about container not existing
if (output.includes('does not exist') || output.includes('not found') || output.includes('No such file')) { if (output.includes('does not exist') || output.includes('not found') || output.includes('No such file')) {
console.log(`[RestoreService] Container ${ctId} does not exist`);
resolve(); // Container doesn't exist, that's fine resolve(); // Container doesn't exist, that's fine
} else { } else {
reject(new Error(`Destroy failed with exit code ${exitCode}: ${output}`)); reject(new Error(`Destroy failed with exit code ${exitCode}: ${output}`));
@@ -201,7 +201,7 @@ class RestoreService {
ctId: string, ctId: string,
snapshotPath: string, snapshotPath: string,
storageName: string, storageName: string,
onProgress?: (step: string, message: string) => void onProgress?: (step: string, message: string) => Promise<void>
): Promise<void> { ): Promise<void> {
const backupService = getBackupService(); const backupService = getBackupService();
const sshService = getSSHExecutionService(); const sshService = getSSHExecutionService();
@@ -236,18 +236,15 @@ class RestoreService {
let downloadSuccess = false; let downloadSuccess = false;
// Login to PBS first // Login to PBS first
if (onProgress) onProgress('pbs_login', 'Logging into PBS...'); if (onProgress) await onProgress('pbs_login', 'Logging into PBS...');
console.log(`[RestoreService] Logging into PBS for storage ${storage.name}`);
const loggedIn = await backupService.loginToPBS(server, storage); const loggedIn = await backupService.loginToPBS(server, storage);
if (!loggedIn) { if (!loggedIn) {
throw new Error(`Failed to login to PBS for storage ${storage.name}`); throw new Error(`Failed to login to PBS for storage ${storage.name}`);
} }
console.log(`[RestoreService] PBS login successful`);
// Download backup from PBS // Download backup from PBS
// proxmox-backup-client restore outputs a folder, not a file // proxmox-backup-client restore outputs a folder, not a file
if (onProgress) onProgress('pbs_download', 'Downloading backup from PBS...'); if (onProgress) await onProgress('pbs_download', 'Downloading backup from PBS...');
console.log(`[RestoreService] Starting download of snapshot ${snapshotPath}`);
// Target folder for PBS restore (without extension) // Target folder for PBS restore (without extension)
// Use sanitized snapshot name (colons replaced with underscores) for file paths // Use sanitized snapshot name (colons replaced with underscores) for file paths
@@ -270,19 +267,15 @@ class RestoreService {
restoreCommand, restoreCommand,
(data: string) => { (data: string) => {
output += data; output += data;
console.log(`[RestoreService] Download output: ${data}`);
}, },
(error: string) => { (error: string) => {
console.error(`[RestoreService] Download error: ${error}`);
reject(new Error(`Download failed: ${error}`)); reject(new Error(`Download failed: ${error}`));
}, },
(code: number) => { (code: number) => {
exitCode = code; exitCode = code;
console.log(`[RestoreService] Download command exited with code ${exitCode}`);
if (exitCode === 0) { if (exitCode === 0) {
resolve(); resolve();
} else { } else {
console.error(`[RestoreService] Download failed: ${output}`);
reject(new Error(`Download failed with exit code ${exitCode}: ${output}`)); reject(new Error(`Download failed with exit code ${exitCode}: ${output}`));
} }
} }
@@ -311,15 +304,12 @@ class RestoreService {
); );
}); });
console.log(`[RestoreService] Folder check result: ${checkOutput}`);
if (!checkOutput.includes('exists')) { if (!checkOutput.includes('exists')) {
throw new Error(`Downloaded folder ${targetFolder} does not exist`); throw new Error(`Downloaded folder ${targetFolder} does not exist`);
} }
// Pack the folder into a tar file // Pack the folder into a tar file
if (onProgress) onProgress('pbs_pack', 'Packing backup folder...'); if (onProgress) await onProgress('pbs_pack', 'Packing backup folder...');
console.log(`[RestoreService] Packing folder ${targetFolder} into ${targetTar}`);
// Use -C to change to the folder directory, then pack all contents (.) into the tar file // Use -C to change to the folder directory, then pack all contents (.) into the tar file
const packCommand = `tar -cf "${targetTar}" -C "${targetFolder}" . 2>&1`; const packCommand = `tar -cf "${targetTar}" -C "${targetFolder}" . 2>&1`;
@@ -333,19 +323,15 @@ class RestoreService {
packCommand, packCommand,
(data: string) => { (data: string) => {
packOutput += data; packOutput += data;
console.log(`[RestoreService] Pack output: ${data}`);
}, },
(error: string) => { (error: string) => {
console.error(`[RestoreService] Pack error: ${error}`);
reject(new Error(`Pack failed: ${error}`)); reject(new Error(`Pack failed: ${error}`));
}, },
(code: number) => { (code: number) => {
packExitCode = code; packExitCode = code;
console.log(`[RestoreService] Pack command exited with code ${packExitCode}`);
if (packExitCode === 0) { if (packExitCode === 0) {
resolve(); resolve();
} else { } else {
console.error(`[RestoreService] Pack failed: ${packOutput}`);
reject(new Error(`Pack failed with exit code ${packExitCode}: ${packOutput}`)); reject(new Error(`Pack failed with exit code ${packExitCode}: ${packOutput}`));
} }
} }
@@ -374,18 +360,13 @@ class RestoreService {
); );
}); });
console.log(`[RestoreService] Tar file check result: ${checkTarOutput}`);
if (!checkTarOutput.includes('exists')) { if (!checkTarOutput.includes('exists')) {
throw new Error(`Packed tar file ${targetTar} does not exist`); throw new Error(`Packed tar file ${targetTar} does not exist`);
} }
downloadedPath = targetTar; downloadedPath = targetTar;
downloadSuccess = true; downloadSuccess = true;
console.log(`[RestoreService] Successfully downloaded and packed backup to ${targetTar}`);
} catch (error) { } catch (error) {
console.error(`[RestoreService] Failed to download/pack backup:`, error);
throw error; throw error;
} }
@@ -394,13 +375,12 @@ class RestoreService {
} }
// Restore from packed tar file // Restore from packed tar file
if (onProgress) onProgress('restoring', 'Restoring container...'); if (onProgress) await onProgress('restoring', 'Restoring container...');
try { try {
console.log(`[RestoreService] Starting Restore from ${targetTar}`);
await this.restoreLocalBackup(server, ctId, downloadedPath, storageName); await this.restoreLocalBackup(server, ctId, downloadedPath, storageName);
} finally { } finally {
// Cleanup: delete downloaded folder and tar file // Cleanup: delete downloaded folder and tar file
if (onProgress) onProgress('cleanup', 'Cleaning up temporary files...'); if (onProgress) await onProgress('cleanup', 'Cleaning up temporary files...');
const cleanupCommand = `rm -rf "${targetFolder}" "${targetTar}" 2>&1 || true`; const cleanupCommand = `rm -rf "${targetFolder}" "${targetTar}" 2>&1 || true`;
sshService.executeCommand( sshService.executeCommand(
server, server,
@@ -422,20 +402,48 @@ class RestoreService {
onProgress?: (progress: RestoreProgress) => void onProgress?: (progress: RestoreProgress) => void
): Promise<RestoreResult> { ): Promise<RestoreResult> {
const progress: RestoreProgress[] = []; const progress: RestoreProgress[] = [];
const logPath = join(process.cwd(), 'restore.log');
const addProgress = (step: string, message: string) => { // Clear log file at start of restore
const clearLogFile = async () => {
try {
await writeFile(logPath, '', 'utf-8');
} catch (error) {
// Ignore log file errors
}
};
// Write progress to log file
const writeProgressToLog = async (message: string) => {
try {
const logLine = `${message}\n`;
await writeFile(logPath, logLine, { flag: 'a', encoding: 'utf-8' });
} catch (error) {
// Ignore log file errors
}
};
const addProgress = async (step: string, message: string) => {
const p = { step, message }; const p = { step, message };
progress.push(p); progress.push(p);
// Write to log file (just the message, without step prefix)
await writeProgressToLog(message);
// Call callback if provided
if (onProgress) { if (onProgress) {
onProgress(p); onProgress(p);
} }
}; };
try { try {
// Clear log file at start
await clearLogFile();
const db = getDatabase(); const db = getDatabase();
const sshService = getSSHExecutionService(); const sshService = getSSHExecutionService();
console.log(`[RestoreService] Starting restore for backup ${backupId}, CT ${containerId}, server ${serverId}`); await addProgress('starting', 'Starting restore...');
// Get backup details // Get backup details
const backup = await db.getBackupById(backupId); const backup = await db.getBackupById(backupId);
@@ -443,8 +451,6 @@ class RestoreService {
throw new Error(`Backup with ID ${backupId} not found`); throw new Error(`Backup with ID ${backupId} not found`);
} }
console.log(`[RestoreService] Backup found: ${backup.backup_name}, type: ${backup.storage_type}, path: ${backup.backup_path}`);
// Get server details // Get server details
const server = await db.getServerById(serverId); const server = await db.getServerById(serverId);
if (!server) { if (!server) {
@@ -452,10 +458,8 @@ class RestoreService {
} }
// Get rootfs storage // Get rootfs storage
addProgress('reading_config', 'Reading container configuration...'); await addProgress('reading_config', 'Reading container configuration...');
console.log(`[RestoreService] Getting rootfs storage for CT ${containerId}`);
const rootfsStorage = await this.getRootfsStorage(server, containerId); const rootfsStorage = await this.getRootfsStorage(server, containerId);
console.log(`[RestoreService] Rootfs storage: ${rootfsStorage || 'not found'}`);
if (!rootfsStorage) { if (!rootfsStorage) {
// Try to check if container exists, if not we can proceed without stopping/destroying // Try to check if container exists, if not we can proceed without stopping/destroying
@@ -482,29 +486,24 @@ class RestoreService {
} }
// Try to stop and destroy container - if it doesn't exist, continue anyway // Try to stop and destroy container - if it doesn't exist, continue anyway
addProgress('stopping', 'Stopping container...'); await addProgress('stopping', 'Stopping container...');
try { try {
await this.stopContainer(server, containerId); await this.stopContainer(server, containerId);
console.log(`[RestoreService] Container ${containerId} stopped`);
} catch (error) { } catch (error) {
console.warn(`[RestoreService] Failed to stop container (may not exist or already stopped):`, error);
// Continue even if stop fails // Continue even if stop fails
} }
// Try to destroy container - if it doesn't exist, continue anyway // Try to destroy container - if it doesn't exist, continue anyway
addProgress('destroying', 'Destroying container...'); await addProgress('destroying', 'Destroying container...');
try { try {
await this.destroyContainer(server, containerId); await this.destroyContainer(server, containerId);
console.log(`[RestoreService] Container ${containerId} destroyed successfully`);
} catch (error) { } catch (error) {
// Container might not exist, which is fine - continue with restore // Container might not exist, which is fine - continue with restore
console.log(`[RestoreService] Container ${containerId} does not exist or destroy failed (continuing anyway):`, error); await addProgress('skipping', 'Container does not exist or already destroyed, continuing...');
addProgress('skipping', 'Container does not exist or already destroyed, continuing...');
} }
// Restore based on backup type // Restore based on backup type
if (backup.storage_type === 'pbs') { if (backup.storage_type === 'pbs') {
console.log(`[RestoreService] Restoring from PBS backup`);
// Get storage info for PBS // Get storage info for PBS
const storageService = getStorageService(); const storageService = getStorageService();
const storages = await storageService.getStorages(server, false); const storages = await storageService.getStorages(server, false);
@@ -521,22 +520,17 @@ class RestoreService {
} }
const snapshotPath = snapshotPathMatch[1]; const snapshotPath = snapshotPathMatch[1];
console.log(`[RestoreService] Snapshot path: ${snapshotPath}, Storage: ${rootfsStorage}`);
await this.restorePBSBackup(server, storage, containerId, snapshotPath, rootfsStorage, (step, message) => { await this.restorePBSBackup(server, storage, containerId, snapshotPath, rootfsStorage, async (step, message) => {
addProgress(step, message); await addProgress(step, message);
}); });
} else { } else {
// Local or storage backup // Local or storage backup
console.log(`[RestoreService] Restoring from ${backup.storage_type} backup: ${backup.backup_path}`); await addProgress('restoring', 'Restoring container...');
addProgress('restoring', 'Restoring container...');
await this.restoreLocalBackup(server, containerId, backup.backup_path, rootfsStorage); await this.restoreLocalBackup(server, containerId, backup.backup_path, rootfsStorage);
console.log(`[RestoreService] Local restore completed`);
} }
addProgress('complete', 'Restore completed successfully'); await addProgress('complete', 'Restore completed successfully');
console.log(`[RestoreService] Restore completed successfully for CT ${containerId}`);
return { return {
success: true, success: true,
@@ -544,8 +538,7 @@ class RestoreService {
}; };
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
console.error(`[RestoreService] Restore failed for CT ${containerId}:`, error); await addProgress('error', `Error: ${errorMessage}`);
addProgress('error', `Error: ${errorMessage}`);
return { return {
success: false, success: false,