Files
jabali-panel/app/Jobs/RunServerBackup.php
2026-01-24 19:36:46 +02:00

275 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Jobs\IndexRemoteBackups;
use App\Models\Backup;
use App\Models\BackupSchedule;
use App\Services\AdminNotificationService;
use App\Services\Agent\AgentClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Exception;
class RunServerBackup implements ShouldQueue
{
use Queueable;
public int $tries = 1;
public int $timeout = 3600; // 1 hour max
public function __construct(
public int $backupId
) {}
public function handle(): void
{
$backup = Backup::find($this->backupId);
if (!$backup) {
Log::warning("RunServerBackup: Backup {$this->backupId} not found");
return;
}
// Skip if already completed or failed
if (in_array($backup->status, ['completed', 'failed'])) {
Log::info("RunServerBackup: Backup {$this->backupId} already {$backup->status}");
return;
}
$backup->update(['status' => 'running', 'started_at' => now()]);
$backupType = $backup->metadata['backup_type'] ?? 'full';
$isIncrementalRemote = $backupType === 'incremental' && $backup->destination_id;
try {
$agent = new AgentClient();
if ($isIncrementalRemote) {
$destination = $backup->destination;
if (!$destination) {
throw new Exception('Backup destination not found');
}
$config = array_merge($destination->config ?? [], ['type' => $destination->type]);
$result = $agent->send('backup.incremental_direct', [
'destination' => $config,
'users' => $backup->users,
'include_files' => $backup->include_files,
'include_databases' => $backup->include_databases,
'include_mailboxes' => $backup->include_mailboxes,
'include_dns' => $backup->include_dns,
]);
if ($result['success'] ?? false) {
$backup->update([
'status' => 'completed',
'completed_at' => now(),
'size_bytes' => $result['size'] ?? 0,
'users' => $result['users'] ?? $backup->users,
'remote_path' => $result['remote_path'] ?? null,
'metadata' => array_merge($backup->metadata ?? [], [
'user_count' => $result['user_count'] ?? 0,
'previous_backup' => $result['previous_backup'] ?? null,
'is_initialization' => $result['is_initialization'] ?? false,
]),
]);
Log::info("RunServerBackup: Incremental backup {$this->backupId} completed");
// Re-index remote backups for user discovery
IndexRemoteBackups::dispatch($backup->destination_id);
// Apply retention policy if this backup is from a schedule
$this->applyRetention($backup);
// Send success notification
AdminNotificationService::backupSuccess(
$backup->name,
$result['size'] ?? 0,
$backup->destination?->name
);
} else {
throw new Exception($result['error'] ?? 'Incremental backup failed');
}
} else {
// Full backup
$outputPath = $backup->local_path;
$result = $agent->send('backup.create_server', [
'output_path' => $outputPath,
'backup_type' => $backupType,
'users' => $backup->users,
'include_files' => $backup->include_files,
'include_databases' => $backup->include_databases,
'include_mailboxes' => $backup->include_mailboxes,
'include_dns' => $backup->include_dns,
]);
if ($result['success'] ?? false) {
$backup->update([
'status' => 'completed',
'completed_at' => now(),
'size_bytes' => $result['size'] ?? 0,
'users' => $result['users'] ?? $backup->users,
'metadata' => array_merge($backup->metadata ?? [], [
'user_count' => $result['user_count'] ?? 0,
]),
]);
// Upload to remote if destination configured
if ($backup->destination_id) {
$this->uploadToRemote($backup, $agent);
}
Log::info("RunServerBackup: Full backup {$this->backupId} completed");
// Apply retention policy if this backup is from a schedule
$this->applyRetention($backup);
// Send success notification
AdminNotificationService::backupSuccess(
$backup->name,
$result['size'] ?? 0,
$backup->destination?->name
);
} else {
throw new Exception($result['error'] ?? 'Backup failed');
}
}
} catch (Exception $e) {
$backup->update([
'status' => 'failed',
'completed_at' => now(),
'error_message' => $e->getMessage(),
]);
Log::error("RunServerBackup: Backup {$this->backupId} failed: " . $e->getMessage());
// Send failure notification
AdminNotificationService::backupFailure($backup->name, $e->getMessage());
}
}
protected function uploadToRemote(Backup $backup, AgentClient $agent): void
{
if (!$backup->destination || !$backup->local_path) {
return;
}
try {
$backup->update(['status' => 'uploading']);
$config = array_merge(
$backup->destination->config ?? [],
['type' => $backup->destination->type]
);
$backupType = $backup->metadata['backup_type'] ?? 'full';
$result = $agent->send('backup.upload_remote', [
'local_path' => $backup->local_path,
'destination' => $config,
'backup_type' => $backupType,
]);
if ($result['success'] ?? false) {
$backup->update([
'status' => 'completed',
'remote_path' => $result['remote_path'] ?? null,
]);
Log::info("RunServerBackup: Uploaded backup {$this->backupId} to remote");
// Re-index remote backups for user discovery
IndexRemoteBackups::dispatch($backup->destination_id);
} else {
// Keep as completed since local exists, just log warning
$backup->update([
'status' => 'completed',
'error_message' => 'Remote upload failed: ' . ($result['error'] ?? 'Unknown error'),
]);
Log::warning("RunServerBackup: Remote upload failed for {$this->backupId}");
}
} catch (Exception $e) {
$backup->update([
'status' => 'completed',
'error_message' => 'Remote upload failed: ' . $e->getMessage(),
]);
Log::warning("RunServerBackup: Remote upload exception for {$this->backupId}: " . $e->getMessage());
}
}
protected function applyRetention(Backup $backup): void
{
Log::info("RunServerBackup: applyRetention called for backup {$backup->id}, schedule_id: " . ($backup->schedule_id ?? 'NULL'));
// Only apply retention if backup has a schedule
if (!$backup->schedule_id) {
Log::info("RunServerBackup: No schedule_id, skipping retention");
return;
}
$schedule = BackupSchedule::find($backup->schedule_id);
if (!$schedule) {
Log::info("RunServerBackup: Schedule not found for id {$backup->schedule_id}");
return;
}
$retentionCount = $schedule->retention_count ?? 7;
Log::info("RunServerBackup: Retention count is {$retentionCount}");
// Get backups from this schedule, ordered by date
$backups = Backup::where('schedule_id', $schedule->id)
->where('status', 'completed')
->orderByDesc('created_at')
->get();
if ($backups->count() <= $retentionCount) {
return;
}
// Get backups to delete (keep newest $retentionCount)
$toDelete = $backups->slice($retentionCount);
$agent = new AgentClient();
foreach ($toDelete as $oldBackup) {
Log::info("RunServerBackup: Deleting old backup per retention: {$oldBackup->name}");
// Delete local file/folder
if ($oldBackup->local_path && file_exists($oldBackup->local_path)) {
if (is_file($oldBackup->local_path)) {
unlink($oldBackup->local_path);
} else {
exec("rm -rf " . escapeshellarg($oldBackup->local_path));
}
}
// Delete from remote if exists
if ($oldBackup->remote_path && $oldBackup->destination) {
try {
$config = array_merge(
$oldBackup->destination->config ?? [],
['type' => $oldBackup->destination->type]
);
$agent->send('backup.delete_remote', [
'remote_path' => $oldBackup->remote_path,
'destination' => $config,
]);
} catch (Exception $e) {
Log::warning("RunServerBackup: Failed to delete remote backup: " . $e->getMessage());
}
}
$oldBackup->delete();
}
Log::info("RunServerBackup: Deleted " . $toDelete->count() . " old backup(s) per retention policy");
}
}