mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-12 04:29:36 +00:00
Add Update Manager for automated Part-DB updates
This feature adds a comprehensive Update Manager similar to Mainsail's update system, allowing administrators to update Part-DB directly from the web interface. Features: - Web UI at /admin/update-manager showing current and available versions - Support for Git-based installations with automatic update execution - Maintenance mode during updates to prevent user access - Automatic database backup before updates - Git rollback points for recovery (tags created before each update) - Progress tracking with real-time status updates - Update history and log viewing - Downgrade support with appropriate UI messaging - CLI command `php bin/console partdb:update` for server-side updates New files: - UpdateManagerController: Handles all web UI routes - UpdateCommand: CLI command for running updates - UpdateExecutor: Core update execution logic with safety mechanisms - UpdateChecker: GitHub API integration for version checking - InstallationTypeDetector: Detects installation type (Git/Docker/ZIP) - MaintenanceModeSubscriber: Blocks user access during maintenance - UpdateExtension: Twig functions for update notifications UI improvements: - Update notification in navbar for admins when update available - Confirmation dialogs for update/downgrade actions - Downgrade-specific text throughout the interface - Progress page with auto-refresh
This commit is contained in:
parent
ae4c0786b2
commit
42fe781ef8
16 changed files with 4126 additions and 0 deletions
268
src/Controller/UpdateManagerController.php
Normal file
268
src/Controller/UpdateManagerController.php
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Services\System\UpdateChecker;
|
||||
use App\Services\System\UpdateExecutor;
|
||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* Controller for the Update Manager web interface.
|
||||
*
|
||||
* This provides a read-only view of update status and instructions.
|
||||
* Actual updates should be performed via the CLI command for safety.
|
||||
*/
|
||||
#[Route('/admin/update-manager')]
|
||||
class UpdateManagerController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly UpdateChecker $updateChecker,
|
||||
private readonly UpdateExecutor $updateExecutor,
|
||||
private readonly VersionManagerInterface $versionManager)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Main update manager page.
|
||||
*/
|
||||
#[Route('', name: 'admin_update_manager', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
||||
|
||||
$status = $this->updateChecker->getUpdateStatus();
|
||||
$availableUpdates = $this->updateChecker->getAvailableUpdates();
|
||||
$validation = $this->updateExecutor->validateUpdatePreconditions();
|
||||
|
||||
return $this->render('admin/update_manager/index.html.twig', [
|
||||
'status' => $status,
|
||||
'available_updates' => $availableUpdates,
|
||||
'all_releases' => $this->updateChecker->getAvailableReleases(10),
|
||||
'validation' => $validation,
|
||||
'is_locked' => $this->updateExecutor->isLocked(),
|
||||
'lock_info' => $this->updateExecutor->getLockInfo(),
|
||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
||||
'maintenance_info' => $this->updateExecutor->getMaintenanceInfo(),
|
||||
'update_logs' => $this->updateExecutor->getUpdateLogs(),
|
||||
'backups' => $this->updateExecutor->getBackups(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX endpoint to check update status.
|
||||
*/
|
||||
#[Route('/status', name: 'admin_update_manager_status', methods: ['GET'])]
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
||||
|
||||
return $this->json([
|
||||
'status' => $this->updateChecker->getUpdateStatus(),
|
||||
'is_locked' => $this->updateExecutor->isLocked(),
|
||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
||||
'lock_info' => $this->updateExecutor->getLockInfo(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX endpoint to refresh version information.
|
||||
*/
|
||||
#[Route('/refresh', name: 'admin_update_manager_refresh', methods: ['POST'])]
|
||||
public function refresh(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
||||
|
||||
// Validate CSRF token
|
||||
if (!$this->isCsrfTokenValid('update_manager_refresh', $request->request->get('_token'))) {
|
||||
return $this->json(['error' => 'Invalid CSRF token'], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$this->updateChecker->refreshGitInfo();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'status' => $this->updateChecker->getUpdateStatus(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View release notes for a specific version.
|
||||
*/
|
||||
#[Route('/release/{tag}', name: 'admin_update_manager_release', methods: ['GET'])]
|
||||
public function releaseNotes(string $tag): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
||||
|
||||
$releases = $this->updateChecker->getAvailableReleases(20);
|
||||
$release = null;
|
||||
|
||||
foreach ($releases as $r) {
|
||||
if ($r['tag'] === $tag) {
|
||||
$release = $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$release) {
|
||||
throw $this->createNotFoundException('Release not found');
|
||||
}
|
||||
|
||||
return $this->render('admin/update_manager/release_notes.html.twig', [
|
||||
'release' => $release,
|
||||
'current_version' => $this->updateChecker->getCurrentVersionString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View an update log file.
|
||||
*/
|
||||
#[Route('/log/{filename}', name: 'admin_update_manager_log', methods: ['GET'])]
|
||||
public function viewLog(string $filename): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
||||
|
||||
// Security: Only allow viewing files from the update logs directory
|
||||
$logs = $this->updateExecutor->getUpdateLogs();
|
||||
$logPath = null;
|
||||
|
||||
foreach ($logs as $log) {
|
||||
if ($log['file'] === $filename) {
|
||||
$logPath = $log['path'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$logPath || !file_exists($logPath)) {
|
||||
throw $this->createNotFoundException('Log file not found');
|
||||
}
|
||||
|
||||
$content = file_get_contents($logPath);
|
||||
|
||||
return $this->render('admin/update_manager/log_viewer.html.twig', [
|
||||
'filename' => $filename,
|
||||
'content' => $content,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an update process.
|
||||
*/
|
||||
#[Route('/start', name: 'admin_update_manager_start', methods: ['POST'])]
|
||||
public function startUpdate(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
// Validate CSRF token
|
||||
if (!$this->isCsrfTokenValid('update_manager_start', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
// Check if update is already running
|
||||
if ($this->updateExecutor->isLocked() || $this->updateExecutor->isUpdateRunning()) {
|
||||
$this->addFlash('error', 'An update is already in progress.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
$targetVersion = $request->request->get('version');
|
||||
$createBackup = $request->request->getBoolean('backup', true);
|
||||
|
||||
if (!$targetVersion) {
|
||||
// Get latest version if not specified
|
||||
$latest = $this->updateChecker->getLatestRelease();
|
||||
if (!$latest) {
|
||||
$this->addFlash('error', 'Could not determine target version.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
$targetVersion = $latest['tag'];
|
||||
}
|
||||
|
||||
// Validate preconditions
|
||||
$validation = $this->updateExecutor->validateUpdatePreconditions();
|
||||
if (!$validation['valid']) {
|
||||
$this->addFlash('error', implode(' ', $validation['errors']));
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
// Start the background update
|
||||
$pid = $this->updateExecutor->startBackgroundUpdate($targetVersion, $createBackup);
|
||||
|
||||
if (!$pid) {
|
||||
$this->addFlash('error', 'Failed to start update process.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
// Redirect to progress page
|
||||
return $this->redirectToRoute('admin_update_manager_progress');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress page.
|
||||
*/
|
||||
#[Route('/progress', name: 'admin_update_manager_progress', methods: ['GET'])]
|
||||
public function progress(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
||||
|
||||
$progress = $this->updateExecutor->getProgress();
|
||||
$currentVersion = $this->versionManager->getVersion()->toString();
|
||||
|
||||
// Determine if this is a downgrade
|
||||
$isDowngrade = false;
|
||||
if ($progress && isset($progress['target_version'])) {
|
||||
$targetVersion = ltrim($progress['target_version'], 'v');
|
||||
$isDowngrade = version_compare($targetVersion, $currentVersion, '<');
|
||||
}
|
||||
|
||||
return $this->render('admin/update_manager/progress.html.twig', [
|
||||
'progress' => $progress,
|
||||
'is_locked' => $this->updateExecutor->isLocked(),
|
||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
||||
'is_downgrade' => $isDowngrade,
|
||||
'current_version' => $currentVersion,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX endpoint to get update progress.
|
||||
*/
|
||||
#[Route('/progress/status', name: 'admin_update_manager_progress_status', methods: ['GET'])]
|
||||
public function progressStatus(): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.show_updates');
|
||||
|
||||
$progress = $this->updateExecutor->getProgress();
|
||||
|
||||
return $this->json([
|
||||
'progress' => $progress,
|
||||
'is_locked' => $this->updateExecutor->isLocked(),
|
||||
'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue