Add backup restore feature

- Add restoreBackup() method to UpdateExecutor with full restore workflow
- Add getBackupDetails() to retrieve backup metadata and contents info
- Add restore controller routes (backup details API, restore action)
- Add restore button to backups table in UI
- Create backup_restore_controller.js Stimulus controller for confirmation
- Add translation strings for restore feature

The restore process:
1. Acquires lock and enables maintenance mode
2. Extracts backup to temp directory
3. Restores database (MySQL/PostgreSQL SQL or SQLite file)
4. Optionally restores config files and attachments
5. Clears and warms cache
6. Disables maintenance mode

Fix backup restore database import

The restore feature was using a non-existent doctrine:database:import
command. Now properly uses mysql/psql commands directly to import
database dumps.

Changes:
- Add EntityManagerInterface dependency to UpdateExecutor
- Use mysql command with shell input redirection for MySQL restore
- Use psql -f command for PostgreSQL restore
- Properly handle database connection parameters
- Add error handling for failed imports
This commit is contained in:
Sebastian Almberg 2026-01-30 22:51:25 +01:00
parent 0bfbbc961d
commit 1637fd63f4
5 changed files with 499 additions and 2 deletions

View file

@ -265,4 +265,75 @@ class UpdateManagerController extends AbstractController
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
]);
}
/**
* Get backup details for restore confirmation.
*/
#[Route('/backup/{filename}', name: 'admin_update_manager_backup_details', methods: ['GET'])]
public function backupDetails(string $filename): JsonResponse
{
$this->denyAccessUnlessGranted('@system.manage_updates');
$details = $this->updateExecutor->getBackupDetails($filename);
if (!$details) {
return $this->json(['error' => 'Backup not found'], 404);
}
return $this->json($details);
}
/**
* Restore from a backup.
*/
#[Route('/restore', name: 'admin_update_manager_restore', methods: ['POST'])]
public function restore(Request $request): Response
{
$this->denyAccessUnlessGranted('@system.manage_updates');
// Validate CSRF token
if (!$this->isCsrfTokenValid('update_manager_restore', $request->request->get('_token'))) {
$this->addFlash('error', 'Invalid CSRF token.');
return $this->redirectToRoute('admin_update_manager');
}
// Check if already locked
if ($this->updateExecutor->isLocked()) {
$this->addFlash('error', 'An update or restore is already in progress.');
return $this->redirectToRoute('admin_update_manager');
}
$filename = $request->request->get('filename');
$restoreDatabase = $request->request->getBoolean('restore_database', true);
$restoreConfig = $request->request->getBoolean('restore_config', false);
$restoreAttachments = $request->request->getBoolean('restore_attachments', false);
if (!$filename) {
$this->addFlash('error', 'No backup file specified.');
return $this->redirectToRoute('admin_update_manager');
}
// Verify the backup exists
$backupDetails = $this->updateExecutor->getBackupDetails($filename);
if (!$backupDetails) {
$this->addFlash('error', 'Backup file not found.');
return $this->redirectToRoute('admin_update_manager');
}
// Execute restore (this is a synchronous operation for now - could be made async later)
$result = $this->updateExecutor->restoreBackup(
$filename,
$restoreDatabase,
$restoreConfig,
$restoreAttachments
);
if ($result['success']) {
$this->addFlash('success', 'Backup restored successfully.');
} else {
$this->addFlash('error', 'Restore failed: ' . ($result['error'] ?? 'Unknown error'));
}
return $this->redirectToRoute('admin_update_manager');
}
}