131 lines
4.1 KiB
PHP
131 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Ui\System;
|
|
|
|
use App\Models\BackupJob;
|
|
use App\Models\BackupPolicy;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
|
|
#[Layout('layouts.dvx')]
|
|
#[Title('Backups · Mailwolt')]
|
|
class BackupJobList extends Component
|
|
{
|
|
public function runNow(): void
|
|
{
|
|
$policy = BackupPolicy::first();
|
|
if (!$policy) {
|
|
$this->dispatch('toast', type: 'warn', badge: 'Backup',
|
|
title: 'Kein Zeitplan', text: 'Bitte zuerst einen Backup-Zeitplan konfigurieren.', duration: 4000);
|
|
return;
|
|
}
|
|
|
|
// Reset stale jobs (stuck > 30 min with no running process)
|
|
BackupJob::whereIn('status', ['queued', 'running'])
|
|
->where('started_at', '<', now()->subMinutes(30))
|
|
->update(['status' => 'failed', 'finished_at' => now(), 'error' => 'Timeout — Prozess nicht mehr aktiv.']);
|
|
|
|
$running = BackupJob::whereIn('status', ['queued', 'running'])->exists();
|
|
if ($running) {
|
|
$this->dispatch('toast', type: 'warn', badge: 'Backup',
|
|
title: 'Läuft bereits', text: 'Ein Backup-Job ist bereits aktiv.', duration: 3000);
|
|
return;
|
|
}
|
|
|
|
$job = BackupJob::create([
|
|
'policy_id' => $policy->id,
|
|
'status' => 'queued',
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$artisan = base_path('artisan');
|
|
exec("nohup php {$artisan} backup:run {$job->id} > /dev/null 2>&1 &");
|
|
|
|
$this->dispatch('openModal',
|
|
component: 'ui.system.modal.backup-progress-modal',
|
|
arguments: ['jobId' => $job->id]
|
|
);
|
|
}
|
|
|
|
public function openProgress(int $id): void
|
|
{
|
|
$this->dispatch('openModal',
|
|
component: 'ui.system.modal.backup-progress-modal',
|
|
arguments: ['jobId' => $id]
|
|
);
|
|
}
|
|
|
|
public function openDeleteConfirm(int $id): void
|
|
{
|
|
$this->dispatch('openModal',
|
|
component: 'ui.system.modal.backup-delete-modal',
|
|
arguments: ['jobId' => $id]
|
|
);
|
|
}
|
|
|
|
public function openRestoreConfirm(int $id): void
|
|
{
|
|
$this->dispatch('openModal',
|
|
component: 'ui.system.modal.backup-restore-confirm-modal',
|
|
arguments: ['jobId' => $id]
|
|
);
|
|
}
|
|
|
|
#[On('backup-list-refresh')]
|
|
public function refresh(): void {}
|
|
|
|
#[On('backup:do-restore')]
|
|
public function onRestoreConfirmed(int $jobId): void
|
|
{
|
|
$this->restore($jobId);
|
|
}
|
|
|
|
public function restore(int $id): void
|
|
{
|
|
$sourceJob = BackupJob::findOrFail($id);
|
|
|
|
if (!$sourceJob->artifact_path || !file_exists($sourceJob->artifact_path)) {
|
|
$this->dispatch('toast', type: 'warn', badge: 'Backup',
|
|
title: 'Datei nicht gefunden', text: 'Das Backup-Archiv wurde nicht gefunden.', duration: 4000);
|
|
return;
|
|
}
|
|
|
|
$token = 'mailwolt_restore_' . uniqid();
|
|
$artisan = base_path('artisan');
|
|
exec("nohup php {$artisan} restore:run {$sourceJob->id} {$token} > /dev/null 2>&1 &");
|
|
|
|
$this->dispatch('openModal',
|
|
component: 'ui.system.modal.backup-progress-modal',
|
|
arguments: ['jobId' => $sourceJob->id, 'restoreToken' => $token]
|
|
);
|
|
}
|
|
|
|
public function delete(int $id): void
|
|
{
|
|
$job = BackupJob::findOrFail($id);
|
|
|
|
if ($job->artifact_path && file_exists($job->artifact_path)) {
|
|
@unlink($job->artifact_path);
|
|
}
|
|
|
|
$job->delete();
|
|
|
|
$this->dispatch('toast', type: 'done', badge: 'Backup',
|
|
title: 'Gelöscht', text: 'Backup-Eintrag wurde entfernt.', duration: 3000);
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$jobs = BackupJob::where('checksum', '!=', 'restore')->orWhereNull('checksum')->latest('started_at')->paginate(20);
|
|
$policy = BackupPolicy::first();
|
|
|
|
$hasRunning = BackupJob::whereIn('status', ['queued', 'running'])
|
|
->where('started_at', '>=', now()->subMinutes(30))
|
|
->exists();
|
|
|
|
return view('livewire.ui.system.backup-job-list', compact('jobs', 'policy', 'hasRunning'));
|
|
}
|
|
}
|