mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-28 02:06:37 -04:00
feat(backups): unify service backup management (#11574)
This commit is contained in:
@@ -322,6 +322,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
$this->backup_standalone_postgresql($database);
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
if ($database === '*') {
|
||||
@@ -343,6 +344,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
$this->backup_standalone_mongodb($database);
|
||||
} elseif (str($databaseType)->contains('mysql')) {
|
||||
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
|
||||
@@ -357,6 +359,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
$this->backup_standalone_mysql($database);
|
||||
} elseif (str($databaseType)->contains('mariadb')) {
|
||||
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
|
||||
@@ -371,6 +374,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
$this->backup_standalone_mariadb($database);
|
||||
} elseif ($this->database instanceof StandaloneClickhouse) {
|
||||
$this->backup_file = '/clickhouse-backup-'.Carbon::now()->timestamp."-{$this->backup_log_uuid}.zip";
|
||||
@@ -382,6 +386,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
'local_storage_deleted' => false,
|
||||
]);
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
$this->backup_standalone_clickhouse($database);
|
||||
} else {
|
||||
throw new \Exception('Unsupported database type');
|
||||
@@ -480,14 +485,14 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} catch (Throwable $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
if ($this->team) {
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
}
|
||||
if ($this->backup_log) {
|
||||
$this->backup_log->update([
|
||||
'finished_at' => Carbon::now()->toImmutable(),
|
||||
]);
|
||||
}
|
||||
if ($this->team) {
|
||||
BackupCreated::dispatch($this->team->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
@@ -37,12 +36,12 @@ class BackupExecutions extends Component
|
||||
|
||||
public $delete_backup_sftp = false;
|
||||
|
||||
public function getListeners()
|
||||
public function getListeners(): array
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
|
||||
"echo-private:team.{$teamId},BackupCreated" => 'refreshBackupExecutions',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledDatabaseBackupExecution;
|
||||
use App\Models\ScheduledVolumeBackup;
|
||||
use App\Models\ScheduledVolumeBackupExecution;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Service $service;
|
||||
|
||||
public bool $executionModalOpen = false;
|
||||
|
||||
public ?array $selectedExecution = null;
|
||||
|
||||
public function getListeners(): array
|
||||
{
|
||||
$teamId = currentTeam()->id;
|
||||
|
||||
return [
|
||||
'modalClosed' => 'closeExecutionModal',
|
||||
"echo-private:team.{$teamId},BackupCreated" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(Service $service): void
|
||||
{
|
||||
abort_unless($service->environment?->project?->team_id === currentTeam()->id, 404);
|
||||
$this->service = $service;
|
||||
$this->authorize('view', $this->service);
|
||||
}
|
||||
|
||||
public function openExecution(string $executionUuid): void
|
||||
{
|
||||
$this->selectedExecution = $this->executions()->firstWhere('uuid', $executionUuid);
|
||||
abort_unless($this->selectedExecution, 404);
|
||||
$this->executionModalOpen = true;
|
||||
}
|
||||
|
||||
public function closeExecutionModal(): void
|
||||
{
|
||||
$this->executionModalOpen = false;
|
||||
$this->selectedExecution = null;
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.project.service.backup-executions', [
|
||||
'executions' => $this->executions(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function executions(): Collection
|
||||
{
|
||||
$databaseScheduleIds = ScheduledDatabaseBackup::query()
|
||||
->where('database_type', (new ServiceDatabase)->getMorphClass())
|
||||
->whereHasMorph('database', [ServiceDatabase::class], fn ($query) => $query->where('service_id', $this->service->id))
|
||||
->pluck('id');
|
||||
|
||||
$databaseExecutions = ScheduledDatabaseBackupExecution::query()
|
||||
->with('scheduledDatabaseBackup.database')
|
||||
->whereIn('scheduled_database_backup_id', $databaseScheduleIds)
|
||||
->latest()
|
||||
->limit(100)
|
||||
->get()
|
||||
->map(fn (ScheduledDatabaseBackupExecution $execution): array => [
|
||||
'id' => 'database:'.$execution->id,
|
||||
'uuid' => $execution->uuid,
|
||||
'target' => $execution->scheduledDatabaseBackup->database->human_name ?: $execution->scheduledDatabaseBackup->database->name,
|
||||
'type' => 'Database',
|
||||
'schedule' => $execution->scheduledDatabaseBackup->frequency,
|
||||
'status' => $execution->status,
|
||||
'started_at' => $execution->created_at,
|
||||
'size' => $execution->size,
|
||||
'message' => $execution->message,
|
||||
'filename' => $execution->filename,
|
||||
'download_url' => $execution->status === 'success' && ! $execution->local_storage_deleted
|
||||
? route('download.backup', $execution->id)
|
||||
: null,
|
||||
]);
|
||||
|
||||
$volumeSchedules = ScheduledVolumeBackup::query()
|
||||
->with('backupable.resource')
|
||||
->forService($this->service)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
$volumeExecutions = ScheduledVolumeBackupExecution::query()
|
||||
->whereIn('scheduled_volume_backup_id', $volumeSchedules->keys())
|
||||
->latest()
|
||||
->limit(100)
|
||||
->get()
|
||||
->map(function (ScheduledVolumeBackupExecution $execution) use ($volumeSchedules): array {
|
||||
$schedule = $volumeSchedules->get($execution->scheduled_volume_backup_id);
|
||||
|
||||
return [
|
||||
'id' => 'storage:'.$execution->id,
|
||||
'uuid' => $execution->uuid,
|
||||
'target' => $schedule->targetName(),
|
||||
'type' => $schedule->targetType(),
|
||||
'schedule' => $schedule->frequency,
|
||||
'status' => $execution->status,
|
||||
'started_at' => $execution->created_at,
|
||||
'size' => $execution->size,
|
||||
'message' => $execution->message,
|
||||
'filename' => $execution->filename,
|
||||
'download_url' => $execution->status === 'success' && ! $execution->local_storage_deleted
|
||||
? route('download.volume-backup', $execution->id)
|
||||
: null,
|
||||
];
|
||||
});
|
||||
|
||||
return $databaseExecutions->concat($volumeExecutions)->sortByDesc('started_at')->values();
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,6 @@ class DatabaseBackups extends Component
|
||||
|
||||
public array $query;
|
||||
|
||||
public bool $isImportSupported = false;
|
||||
|
||||
public ?ScheduledDatabaseBackup $backup = null;
|
||||
|
||||
public string $section = 'index';
|
||||
@@ -32,7 +30,7 @@ class DatabaseBackups extends Component
|
||||
|
||||
protected $listeners = ['refreshScheduledBackups' => '$refresh'];
|
||||
|
||||
public function mount()
|
||||
public function mount(): mixed
|
||||
{
|
||||
try {
|
||||
$this->parameters = array_filter(
|
||||
@@ -67,10 +65,13 @@ class DatabaseBackups extends Component
|
||||
return redirect()->route('project.service.index', $this->parameters);
|
||||
}
|
||||
|
||||
// Check if import is supported for this database type
|
||||
$dbType = $this->serviceDatabase->databaseType();
|
||||
$supportedTypes = ['mysql', 'mariadb', 'postgres', 'mongo'];
|
||||
$this->isImportSupported = collect($supportedTypes)->contains(fn ($type) => str_contains($dbType, $type));
|
||||
if (! request()->route('backup_uuid')) {
|
||||
return redirect()->route('project.service.volume-backups.index', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||||
'service_uuid' => $this->parameters['service_uuid'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (request()->route('backup_uuid')) {
|
||||
$this->backup = $this->serviceDatabase->scheduledBackups()
|
||||
@@ -85,6 +86,14 @@ class DatabaseBackups extends Component
|
||||
'project.service.database.backup.danger' => 'danger',
|
||||
default => 'general',
|
||||
};
|
||||
|
||||
$routeParameters = [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||||
'service_uuid' => $this->parameters['service_uuid'],
|
||||
];
|
||||
|
||||
return redirect()->route('project.service.volume-backups.index', $routeParameters);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class ImportBackup extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Service $service;
|
||||
|
||||
public Collection $databases;
|
||||
|
||||
public ?ServiceDatabase $selectedDatabase = null;
|
||||
|
||||
public string $selectedDatabaseUuid = '';
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public function mount(): mixed
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$project = currentTeam()->projects()->whereUuid($this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->whereUuid($this->parameters['environment_uuid'])->firstOrFail();
|
||||
$this->service = $environment->services()->whereUuid($this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->authorize('update', $this->service);
|
||||
|
||||
$this->databases = $this->service->databases
|
||||
->filter(fn (ServiceDatabase $database): bool => $this->supportsImport($database))
|
||||
->values();
|
||||
|
||||
$databaseUuid = request()->route('stack_service_uuid');
|
||||
if ($databaseUuid) {
|
||||
$selectedDatabase = $this->databases->firstWhere('uuid', $databaseUuid);
|
||||
abort_unless($selectedDatabase instanceof ServiceDatabase, 404);
|
||||
$this->authorize('update', $selectedDatabase);
|
||||
$this->selectedDatabase = $selectedDatabase;
|
||||
$this->selectedDatabaseUuid = $selectedDatabase->uuid;
|
||||
|
||||
if (request()->routeIs('project.service.database.import')) {
|
||||
return redirect()->route('project.service.import-backup.database', $this->parameters);
|
||||
}
|
||||
} elseif ($this->databases->count() === 1) {
|
||||
return redirect()->route('project.service.import-backup.database', [
|
||||
...$this->parameters,
|
||||
'stack_service_uuid' => $this->databases->first()->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function updatedSelectedDatabaseUuid(): mixed
|
||||
{
|
||||
$database = $this->databases->firstWhere('uuid', $this->selectedDatabaseUuid);
|
||||
abort_unless($database instanceof ServiceDatabase, 404);
|
||||
$this->authorize('update', $database);
|
||||
|
||||
return redirect()->route('project.service.import-backup.database', [
|
||||
...$this->parameters,
|
||||
'stack_service_uuid' => $database->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.project.service.import-backup');
|
||||
}
|
||||
|
||||
private function supportsImport(ServiceDatabase $database): bool
|
||||
{
|
||||
return str($database->databaseType())->contains(['mysql', 'mariadb', 'postgres', 'mongo']);
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,6 @@ class Index extends Component
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public bool $isImportSupported = false;
|
||||
|
||||
// Application-specific properties
|
||||
public $docker_cleanup = true;
|
||||
|
||||
@@ -153,10 +151,6 @@ class Index extends Component
|
||||
$this->refreshFileStorages();
|
||||
$this->syncDatabaseData(false);
|
||||
|
||||
// Check if import is supported for this database type
|
||||
$dbType = $this->serviceDatabase->databaseType();
|
||||
$supportedTypes = ['mysql', 'mariadb', 'postgres', 'mongo'];
|
||||
$this->isImportSupported = collect($supportedTypes)->contains(fn ($type) => str_contains($dbType, $type));
|
||||
}
|
||||
|
||||
private function syncDatabaseData(bool $toModel = false): void
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Livewire\Project\Service\VolumeBackup;
|
||||
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\VolumeBackupJob;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledVolumeBackup;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -20,14 +23,70 @@ class Index extends Component
|
||||
|
||||
public string $search = '';
|
||||
|
||||
protected $listeners = ['refreshVolumeBackups' => '$refresh'];
|
||||
public bool $scheduleModalOpen = false;
|
||||
|
||||
public function mount(): void
|
||||
public ?ScheduledDatabaseBackup $selectedDatabaseBackup = null;
|
||||
|
||||
public ?ScheduledVolumeBackup $selectedVolumeBackup = null;
|
||||
|
||||
public ?Collection $s3s = null;
|
||||
|
||||
public function getListeners(): array
|
||||
{
|
||||
$this->service = $this->findService();
|
||||
$teamId = currentTeam()->id;
|
||||
|
||||
return [
|
||||
'refreshVolumeBackups' => '$refresh',
|
||||
'modalClosed' => 'closeScheduleModal',
|
||||
"echo-private:team.{$teamId},BackupCreated" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(?Service $service = null): void
|
||||
{
|
||||
$this->service = $service ?? $this->findService();
|
||||
$this->authorize('view', $this->service);
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->search = request()->string('search')->toString();
|
||||
|
||||
}
|
||||
|
||||
public function openSchedule(string $backupUuid): void
|
||||
{
|
||||
$this->loadSelectedSchedule($backupUuid);
|
||||
$this->s3s = currentTeam()->s3s;
|
||||
$this->scheduleModalOpen = true;
|
||||
}
|
||||
|
||||
public function closeScheduleModal(): void
|
||||
{
|
||||
$this->scheduleModalOpen = false;
|
||||
$this->selectedDatabaseBackup = null;
|
||||
$this->selectedVolumeBackup = null;
|
||||
}
|
||||
|
||||
public function backupNow(string $type, string $backupUuid): void
|
||||
{
|
||||
try {
|
||||
if ($type === 'database') {
|
||||
$this->loadSelectedSchedule($backupUuid);
|
||||
abort_unless($this->selectedDatabaseBackup, 404);
|
||||
$this->authorize('manageBackups', $this->selectedDatabaseBackup->database);
|
||||
DatabaseBackupJob::dispatch($this->selectedDatabaseBackup);
|
||||
} else {
|
||||
abort_unless($type === 'storage', 404);
|
||||
$this->loadSelectedSchedule($backupUuid);
|
||||
abort_unless($this->selectedVolumeBackup, 404);
|
||||
$this->authorize('update', $this->selectedVolumeBackup->targetResource());
|
||||
VolumeBackupJob::dispatch($this->selectedVolumeBackup);
|
||||
}
|
||||
|
||||
$this->selectedDatabaseBackup = null;
|
||||
$this->selectedVolumeBackup = null;
|
||||
$this->dispatch('success', 'Backup queued.');
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
@@ -68,4 +127,24 @@ class Index extends Component
|
||||
->where('uuid', request()->route('service_uuid'))
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
private function loadSelectedSchedule(string $backupUuid): void
|
||||
{
|
||||
$this->selectedDatabaseBackup = ScheduledDatabaseBackup::query()
|
||||
->with('database')
|
||||
->whereUuid($backupUuid)
|
||||
->where('database_type', (new ServiceDatabase)->getMorphClass())
|
||||
->whereHasMorph('database', [ServiceDatabase::class], fn ($query) => $query->where('service_id', $this->service->id))
|
||||
->first();
|
||||
|
||||
if ($this->selectedDatabaseBackup) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->selectedVolumeBackup = ScheduledVolumeBackup::query()
|
||||
->with('backupable.resource')
|
||||
->whereUuid($backupUuid)
|
||||
->forService($this->service)
|
||||
->firstOrFail();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class Show extends Component
|
||||
|
||||
public string $section = 'general';
|
||||
|
||||
public function mount(): void
|
||||
public function mount(): mixed
|
||||
{
|
||||
$project = currentTeam()->projects()->where('uuid', request()->route('project_uuid'))->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', request()->route('environment_uuid'))->firstOrFail();
|
||||
@@ -43,6 +43,10 @@ class Show extends Component
|
||||
'project.service.volume-backups.danger' => 'danger',
|
||||
default => 'general',
|
||||
};
|
||||
|
||||
$routeParameters = collect($this->parameters)->except('backup_uuid')->all();
|
||||
|
||||
return redirect()->route('project.service.volume-backups.index', $routeParameters);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
||||
Reference in New Issue
Block a user