mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-28 20:39:35 +00:00
Add Docker backup support: download button, SQLite restore fix, decouple from auto-update
- Decouple backup creation/restore UI from can_auto_update so Docker and other non-git installations can use backup features - Add backup download endpoint for saving backups externally - Fix SQLite restore to use configured DATABASE_URL path instead of hardcoded var/app.db (affects Docker and custom SQLite paths) - Show Docker-specific warning about var/backups/ not being persisted - Pass is_docker flag to template via InstallationTypeDetector
This commit is contained in:
parent
31380fdcc4
commit
3c41597262
4 changed files with 59 additions and 9 deletions
|
|
@ -24,14 +24,17 @@ declare(strict_types=1);
|
|||
namespace App\Controller;
|
||||
|
||||
use App\Services\System\BackupManager;
|
||||
use App\Services\System\InstallationTypeDetector;
|
||||
use App\Services\System\UpdateChecker;
|
||||
use App\Services\System\UpdateExecutor;
|
||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
|
|
@ -49,6 +52,7 @@ class UpdateManagerController extends AbstractController
|
|||
private readonly UpdateExecutor $updateExecutor,
|
||||
private readonly VersionManagerInterface $versionManager,
|
||||
private readonly BackupManager $backupManager,
|
||||
private readonly InstallationTypeDetector $installationTypeDetector,
|
||||
#[Autowire(env: 'bool:DISABLE_WEB_UPDATES')]
|
||||
private readonly bool $webUpdatesDisabled = false,
|
||||
#[Autowire(env: 'bool:DISABLE_BACKUP_RESTORE')]
|
||||
|
|
@ -101,6 +105,7 @@ class UpdateManagerController extends AbstractController
|
|||
'backups' => $this->backupManager->getBackups(),
|
||||
'web_updates_disabled' => $this->webUpdatesDisabled,
|
||||
'backup_restore_disabled' => $this->backupRestoreDisabled,
|
||||
'is_docker' => $this->installationTypeDetector->isDocker(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -388,6 +393,25 @@ class UpdateManagerController extends AbstractController
|
|||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a backup file.
|
||||
*/
|
||||
#[Route('/backup/download/{filename}', name: 'admin_update_manager_backup_download', methods: ['GET'])]
|
||||
public function downloadBackup(string $filename): BinaryFileResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
$details = $this->backupManager->getBackupDetails($filename);
|
||||
if (!$details) {
|
||||
throw $this->createNotFoundException('Backup not found');
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($details['path']);
|
||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $details['file']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from a backup.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -327,14 +327,14 @@ readonly class BackupManager
|
|||
*/
|
||||
private function restoreDatabaseFromBackup(string $tempDir): void
|
||||
{
|
||||
// Get database connection params from Doctrine
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$params = $connection->getParams();
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
// Check for SQL dump (MySQL/PostgreSQL)
|
||||
$sqlFile = $tempDir . '/database.sql';
|
||||
if (file_exists($sqlFile)) {
|
||||
// Import SQL using mysql/psql command directly
|
||||
// First, get database connection params from Doctrine
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$params = $connection->getParams();
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
// Use mysql command to import - need to use shell to handle input redirection
|
||||
|
|
@ -403,7 +403,8 @@ readonly class BackupManager
|
|||
// Check for SQLite database file
|
||||
$sqliteFile = $tempDir . '/var/app.db';
|
||||
if (file_exists($sqliteFile)) {
|
||||
$targetDb = $this->projectDir . '/var/app.db';
|
||||
// Use the actual configured SQLite path from Doctrine, not a hardcoded path
|
||||
$targetDb = $params['path'] ?? $this->projectDir . '/var/app.db';
|
||||
$this->filesystem->copy($sqliteFile, $targetDb, true);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,17 +377,23 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="backups-tab">
|
||||
{% if is_granted('@system.manage_updates') and status.can_auto_update and not is_locked %}
|
||||
{% if is_granted('@system.manage_updates') and not is_locked %}
|
||||
<div class="p-2 border-bottom">
|
||||
<form action="{{ path('admin_update_manager_backup') }}" method="post" class="d-inline"
|
||||
data-turbo-confirm="{% trans %}update_manager.backup.create.confirm{% endtrans %}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_backup') }}">
|
||||
<button type="submit" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-download me-1"></i>{% trans %}update_manager.backup.create{% endtrans %}
|
||||
<i class="fas fa-plus me-1"></i>{% trans %}update_manager.backup.create{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_docker %}
|
||||
<div class="alert alert-info alert-sm m-2 mb-0 py-2 small">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans %}update_manager.backup.docker_warning{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
|
|
@ -410,7 +416,14 @@
|
|||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if status.can_auto_update and validation.valid and not backup_restore_disabled %}
|
||||
{% if is_granted('@system.manage_updates') %}
|
||||
<a href="{{ path('admin_update_manager_backup_download', {filename: backup.file}) }}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans %}update_manager.backup.download{% endtrans %}">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not backup_restore_disabled and is_granted('@system.manage_updates') %}
|
||||
<form action="{{ path('admin_update_manager_restore') }}" method="post" class="d-inline"
|
||||
data-controller="backup-restore"
|
||||
data-backup-restore-filename-value="{{ backup.file }}"
|
||||
|
|
|
|||
|
|
@ -12377,6 +12377,18 @@ Buerklin-API Authentication server:
|
|||
<target>Delete</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="um_bk_download" name="update_manager.backup.download">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download</source>
|
||||
<target>Download backup</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="um_bk_docker_warning" name="update_manager.backup.docker_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.docker_warning</source>
|
||||
<target>Docker installation detected. Backups are stored in var/backups/ which is not a persistent volume. Use the download button to save backups externally, or mount var/backups/ as a volume in your docker-compose.yml.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kHKChQB" name="settings.ips.conrad">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.conrad</source>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue