mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-29 10:06:44 -05:00
Resolve custom container names consistently, clean up legacy container names during deployments, and route Livewire resource redirects through the navigation helper.
391 lines
14 KiB
PHP
391 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Database;
|
|
|
|
use App\Jobs\DatabaseBackupJob;
|
|
use App\Models\S3Storage;
|
|
use App\Models\ScheduledDatabaseBackup;
|
|
use App\Models\ServiceDatabase;
|
|
use Exception;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Livewire\Attributes\Locked;
|
|
use Livewire\Attributes\Validate;
|
|
use Livewire\Component;
|
|
|
|
class BackupEdit extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public ScheduledDatabaseBackup $backup;
|
|
|
|
public string $section = 'general';
|
|
|
|
#[Locked]
|
|
public $availableS3Storages;
|
|
|
|
#[Locked]
|
|
public $parameters;
|
|
|
|
#[Validate(['required', 'boolean'])]
|
|
public bool $delete_associated_backups_locally = false;
|
|
|
|
#[Validate(['required', 'boolean'])]
|
|
public bool $delete_associated_backups_s3 = false;
|
|
|
|
#[Validate(['required', 'boolean'])]
|
|
public bool $delete_associated_backups_sftp = false;
|
|
|
|
#[Validate(['nullable', 'string'])]
|
|
public ?string $status = null;
|
|
|
|
#[Validate(['required', 'boolean'])]
|
|
public bool $backupEnabled = false;
|
|
|
|
#[Validate(['required', 'string'])]
|
|
public string $frequency = '';
|
|
|
|
#[Validate(['string'])]
|
|
public string $timezone = '';
|
|
|
|
#[Validate(['required', 'integer'])]
|
|
public int $databaseBackupRetentionAmountLocally = 0;
|
|
|
|
#[Validate(['required', 'integer'])]
|
|
public ?int $databaseBackupRetentionDaysLocally = 0;
|
|
|
|
#[Validate(['required', 'numeric', 'min:0'])]
|
|
public ?float $databaseBackupRetentionMaxStorageLocally = 0;
|
|
|
|
#[Validate(['required', 'integer'])]
|
|
public ?int $databaseBackupRetentionAmountS3 = 0;
|
|
|
|
#[Validate(['required', 'integer'])]
|
|
public ?int $databaseBackupRetentionDaysS3 = 0;
|
|
|
|
#[Validate(['required', 'numeric', 'min:0'])]
|
|
public ?float $databaseBackupRetentionMaxStorageS3 = 0;
|
|
|
|
#[Validate(['required', 'boolean'])]
|
|
public bool $saveS3 = false;
|
|
|
|
#[Validate(['required', 'boolean'])]
|
|
public bool $disableLocalBackup = false;
|
|
|
|
#[Validate(['nullable', 'integer'])]
|
|
public ?int $s3StorageId = null;
|
|
|
|
#[Validate(['nullable', 'string'])]
|
|
public ?string $databasesToBackup = null;
|
|
|
|
#[Validate(['required', 'boolean'])]
|
|
public bool $dumpAll = false;
|
|
|
|
#[Validate(['required', 'int', 'min:60', 'max:36000'])]
|
|
public int|string $timeout = 3600;
|
|
|
|
public function getListeners(): array
|
|
{
|
|
// Keep "Backup Now" in sync when the database starts/stops without a full page refresh.
|
|
$listeners = ['databaseUpdated' => 'refreshStatus'];
|
|
|
|
$user = Auth::user();
|
|
if (! $user) {
|
|
return $listeners;
|
|
}
|
|
|
|
$listeners["echo-private:user.{$user->id},DatabaseStatusChanged"] = 'refreshStatus';
|
|
|
|
$team = $user->currentTeam();
|
|
if ($team) {
|
|
$listeners["echo-private:team.{$team->id},ServiceChecked"] = 'refreshStatus';
|
|
}
|
|
|
|
return $listeners;
|
|
}
|
|
|
|
public function mount()
|
|
{
|
|
try {
|
|
$this->authorize('view', $this->backup->database);
|
|
$this->parameters = get_route_parameters();
|
|
$this->syncData();
|
|
$this->refreshStatus();
|
|
} catch (Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function refreshStatus(): void
|
|
{
|
|
$database = $this->backup->database;
|
|
if (! $database) {
|
|
return;
|
|
}
|
|
|
|
$database->refresh();
|
|
$this->status = $database->status;
|
|
}
|
|
|
|
public function syncData(bool $toModel = false)
|
|
{
|
|
if ($toModel) {
|
|
$this->backup->enabled = $this->backupEnabled;
|
|
$this->backup->frequency = $this->frequency;
|
|
$this->backup->database_backup_retention_amount_locally = $this->databaseBackupRetentionAmountLocally;
|
|
$this->backup->database_backup_retention_days_locally = $this->databaseBackupRetentionDaysLocally;
|
|
$this->backup->database_backup_retention_max_storage_locally = $this->databaseBackupRetentionMaxStorageLocally;
|
|
$this->backup->database_backup_retention_amount_s3 = $this->databaseBackupRetentionAmountS3;
|
|
$this->backup->database_backup_retention_days_s3 = $this->databaseBackupRetentionDaysS3;
|
|
$this->backup->database_backup_retention_max_storage_s3 = $this->databaseBackupRetentionMaxStorageS3;
|
|
$this->backup->save_s3 = $this->saveS3;
|
|
$this->backup->disable_local_backup = $this->disableLocalBackup;
|
|
$this->backup->s3_storage_id = $this->s3StorageId;
|
|
|
|
// Validate databases_to_backup to prevent command injection
|
|
// Handles all formats including MongoDB's "db:col1,col2|db2:col3"
|
|
if (filled($this->databasesToBackup)) {
|
|
validateDatabasesBackupInput($this->databasesToBackup);
|
|
}
|
|
|
|
$this->backup->databases_to_backup = $this->databasesToBackup;
|
|
$this->backup->dump_all = $this->dumpAll;
|
|
$this->backup->timeout = $this->timeout;
|
|
$this->customValidate();
|
|
$this->backup->save();
|
|
} else {
|
|
$this->backupEnabled = $this->backup->enabled;
|
|
$this->frequency = $this->backup->frequency;
|
|
$this->timezone = data_get($this->backup->server(), 'settings.server_timezone', 'Instance timezone');
|
|
$this->databaseBackupRetentionAmountLocally = $this->backup->database_backup_retention_amount_locally;
|
|
$this->databaseBackupRetentionDaysLocally = $this->backup->database_backup_retention_days_locally;
|
|
$this->databaseBackupRetentionMaxStorageLocally = $this->backup->database_backup_retention_max_storage_locally;
|
|
$this->databaseBackupRetentionAmountS3 = $this->backup->database_backup_retention_amount_s3;
|
|
$this->databaseBackupRetentionDaysS3 = $this->backup->database_backup_retention_days_s3;
|
|
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
|
|
$this->saveS3 = $this->backup->save_s3;
|
|
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
|
|
$this->s3StorageId = $this->backup->s3_storage_id ?? $this->availableS3StorageIds()->first();
|
|
$this->databasesToBackup = $this->backup->databases_to_backup;
|
|
$this->dumpAll = $this->backup->dump_all;
|
|
$this->timeout = $this->backup->timeout;
|
|
}
|
|
}
|
|
|
|
public function delete($password, $selectedActions = [])
|
|
{
|
|
$this->authorize('manageBackups', $this->backup->database);
|
|
|
|
if (! verifyPasswordConfirmation($password, $this)) {
|
|
return 'The provided password is incorrect.';
|
|
}
|
|
|
|
try {
|
|
$server = null;
|
|
if ($this->backup->database instanceof ServiceDatabase) {
|
|
$server = $this->backup->database->service->destination->server;
|
|
} elseif ($this->backup->database->destination && $this->backup->database->destination->server) {
|
|
$server = $this->backup->database->destination->server;
|
|
}
|
|
|
|
$filenames = $this->backup->executions()
|
|
->whereNotNull('filename')
|
|
->where('filename', '!=', '')
|
|
->where('scheduled_database_backup_id', $this->backup->id)
|
|
->pluck('filename')
|
|
->filter()
|
|
->all();
|
|
|
|
if (! empty($filenames)) {
|
|
if ($this->delete_associated_backups_locally && $server) {
|
|
deleteBackupsLocally($filenames, $server);
|
|
}
|
|
|
|
if ($this->delete_associated_backups_s3 && $this->backup->s3) {
|
|
deleteBackupsS3($filenames, $this->backup->s3);
|
|
}
|
|
}
|
|
|
|
$this->backup->delete();
|
|
|
|
if ($this->backup->database->getMorphClass() === ServiceDatabase::class) {
|
|
$serviceDatabase = $this->backup->database;
|
|
|
|
return redirectRoute($this, 'project.service.database.backups', [
|
|
'project_uuid' => $this->parameters['project_uuid'],
|
|
'environment_uuid' => $this->parameters['environment_uuid'],
|
|
'service_uuid' => $serviceDatabase->service->uuid,
|
|
'stack_service_uuid' => $serviceDatabase->uuid,
|
|
]);
|
|
} else {
|
|
return redirectRoute($this, 'project.database.backup.index', [
|
|
'project_uuid' => $this->parameters['project_uuid'],
|
|
'environment_uuid' => $this->parameters['environment_uuid'],
|
|
'database_uuid' => $this->parameters['database_uuid'],
|
|
]);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->dispatch('error', 'Failed to delete backup: '.$e->getMessage());
|
|
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function backupNow()
|
|
{
|
|
try {
|
|
$this->authorize('manageBackups', $this->backup->database);
|
|
|
|
DatabaseBackupJob::dispatch($this->backup);
|
|
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
|
|
|
|
$database = $this->backup->database;
|
|
|
|
if ($database instanceof ServiceDatabase) {
|
|
return redirect()->route('project.service.database.backup.executions', [
|
|
'project_uuid' => $database->service->project()->uuid,
|
|
'environment_uuid' => $database->service->environment->uuid,
|
|
'service_uuid' => $database->service->uuid,
|
|
'stack_service_uuid' => $database->uuid,
|
|
'backup_uuid' => $this->backup->uuid,
|
|
]);
|
|
}
|
|
|
|
// Instance databases (e.g. coolify-db) have no project/environment.
|
|
// Stay on the current page (settings.backup) instead of redirecting.
|
|
$project = $database->project();
|
|
$environment = $database->environment;
|
|
if (! $project || ! $environment) {
|
|
return null;
|
|
}
|
|
|
|
return redirect()->route('project.database.backup.executions', [
|
|
'project_uuid' => $project->uuid,
|
|
'environment_uuid' => $environment->uuid,
|
|
'database_uuid' => $database->uuid,
|
|
'backup_uuid' => $this->backup->uuid,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function instantSave()
|
|
{
|
|
try {
|
|
$this->authorize('manageBackups', $this->backup->database);
|
|
|
|
$this->syncData(true);
|
|
$this->dispatch('success', 'Backup updated successfully.');
|
|
} catch (\Throwable $e) {
|
|
$this->dispatch('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function toggleEnabled(): void
|
|
{
|
|
try {
|
|
$this->authorize('manageBackups', $this->backup->database);
|
|
|
|
$this->backupEnabled = ! $this->backupEnabled;
|
|
$this->backup->update(['enabled' => $this->backupEnabled]);
|
|
$this->dispatch('success', $this->backupEnabled ? 'Backup enabled.' : 'Backup disabled.');
|
|
} catch (\Throwable $e) {
|
|
$this->dispatch('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function updatedS3StorageId(): void
|
|
{
|
|
$this->instantSave();
|
|
}
|
|
|
|
public function toggleS3(): void
|
|
{
|
|
if (! $this->saveS3 && $this->availableS3StorageIds()->isEmpty()) {
|
|
$this->dispatch('error', 'Select a usable S3 storage before enabling S3 backups.');
|
|
|
|
return;
|
|
}
|
|
|
|
$this->saveS3 = ! $this->saveS3;
|
|
$this->disableLocalBackup = $this->saveS3 && $this->disableLocalBackup;
|
|
$this->instantSave();
|
|
}
|
|
|
|
private function customValidate()
|
|
{
|
|
if (! is_numeric($this->backup->s3_storage_id)) {
|
|
$this->backup->s3_storage_id = null;
|
|
}
|
|
|
|
// S3 backup cannot be enabled without a valid S3 storage owned by the team
|
|
$availableS3Ids = $this->availableS3StorageIds();
|
|
if ($availableS3Ids->isEmpty()) {
|
|
$this->backup->s3_storage_id = $this->s3StorageId = null;
|
|
if ($this->backup->save_s3) {
|
|
$this->backup->save_s3 = $this->saveS3 = false;
|
|
}
|
|
} elseif (! $availableS3Ids->contains($this->backup->s3_storage_id)) {
|
|
$this->backup->s3_storage_id = $this->s3StorageId = $availableS3Ids->first();
|
|
}
|
|
|
|
// Validate that disable_local_backup can only be true when S3 backup is enabled
|
|
if ($this->backup->disable_local_backup && ! $this->backup->save_s3) {
|
|
$this->backup->disable_local_backup = $this->disableLocalBackup = false;
|
|
}
|
|
|
|
$isValid = validate_cron_expression($this->backup->frequency);
|
|
if (! $isValid) {
|
|
throw new Exception('Invalid Cron / Human expression');
|
|
}
|
|
$this->validate();
|
|
}
|
|
|
|
private function availableS3StorageIds(): Collection
|
|
{
|
|
$storages = collect($this->availableS3Storages);
|
|
$storageIds = $storages->pluck('id')->filter()->all();
|
|
|
|
if (empty($storageIds)) {
|
|
return collect();
|
|
}
|
|
|
|
$teamIds = $storages->pluck('team_id')->reject(fn ($teamId) => $teamId === null)->unique()->values()->all();
|
|
|
|
if (empty($teamIds)) {
|
|
return collect();
|
|
}
|
|
|
|
return S3Storage::query()
|
|
->whereKey($storageIds)
|
|
->whereIn('team_id', $teamIds)
|
|
->where('is_usable', true)
|
|
->pluck('id');
|
|
}
|
|
|
|
public function submit()
|
|
{
|
|
try {
|
|
$this->authorize('manageBackups', $this->backup->database);
|
|
|
|
$this->syncData(true);
|
|
$this->dispatch('success', 'Backup updated successfully.');
|
|
} catch (\Throwable $e) {
|
|
$this->dispatch('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.project.database.backup-edit', [
|
|
'checkboxes' => [
|
|
['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')],
|
|
['id' => 'delete_associated_backups_s3', 'label' => 'All backups will be permanently deleted (associated with this backup job) from the selected S3 Storage.'],
|
|
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
|
|
],
|
|
]);
|
|
}
|
|
}
|