authorize('view', $this->backup->database); $this->parameters = get_route_parameters(); $this->syncData(); } catch (Exception $e) { return handleError($e, $this); } } public function syncData(bool $toModel = false) { if ($toModel) { $this->backup->enabled = $this->backupEnabled; $this->backup->frequency = $this->frequency; $this->backup->engine = $this->engine; if ($this->engine === 'pgbackrest') { $this->backup->save_s3 = $this->saveS3; $this->backup->disable_local_backup = $this->saveS3 && $this->disableLocalBackup; $this->backup->pgbackrest_backup_type = $this->pgbackrestBackupType; $this->backup->pgbackrest_compress_type = $this->pgbackrestCompressType; $this->backup->pgbackrest_compress_level = $this->pgbackrestCompressLevel; $this->backup->pgbackrest_log_level = $this->pgbackrestLogLevel; $this->backup->pgbackrest_archive_mode = $this->pgbackrestArchiveMode; $this->backup->save(); $this->syncPgbackrestRepos(); } else { $this->backup->save_s3 = $this->saveS3; $this->backup->disable_local_backup = $this->disableLocalBackup; $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->s3_storage_id = $this->s3StorageId; if (filled($this->databasesToBackup)) { $databases = str($this->databasesToBackup)->explode(','); foreach ($databases as $index => $db) { $dbName = trim($db); try { validateShellSafePath($dbName, 'database name'); } catch (\Exception $e) { $position = $index + 1; throw new \Exception( "Database #{$position} ('{$dbName}') validation failed: ". $e->getMessage() ); } } } $this->backup->databases_to_backup = $this->databasesToBackup; $this->backup->dump_all = $this->dumpAll; $this->backup->save(); } $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->engine = $this->backup->engine ?? 'native'; $this->pgbackrestBackupType = $this->backup->pgbackrest_backup_type ?? 'full'; $this->pgbackrestCompressType = $this->backup->pgbackrest_compress_type ?? 'lz4'; $this->pgbackrestCompressLevel = $this->backup->pgbackrest_compress_level ?? 6; $this->pgbackrestLogLevel = $this->backup->pgbackrest_log_level ?? 'info'; $this->pgbackrestArchiveMode = $this->backup->pgbackrest_archive_mode ?? 'standard'; $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; if ($this->engine === 'pgbackrest') { $this->loadPgbackrestRepos(); } else { $this->saveS3 = $this->backup->save_s3; $this->disableLocalBackup = $this->backup->disable_local_backup ?? false; } $this->s3StorageId = $this->backup->s3_storage_id; $this->databasesToBackup = $this->backup->databases_to_backup; $this->dumpAll = $this->backup->dump_all; $this->timeout = $this->backup->timeout; } } private function loadPgbackrestRepos(): void { $localRepo = $this->backup->localRepo(); $s3Repo = $this->backup->s3Repo(); $this->disableLocalBackup = $localRepo === null || ! $localRepo->enabled; $this->saveS3 = $s3Repo !== null && $s3Repo->enabled; if ($localRepo) { $this->localRepoRetentionFullType = $localRepo->retention_full_type ?? 'count'; $this->localRepoRetentionFull = $localRepo->retention_full ?? 2; $this->localRepoRetentionDiff = $localRepo->retention_diff ?? 7; } if ($s3Repo) { $this->s3RepoStorageId = $s3Repo->s3_storage_id; $this->s3RepoRetentionFullType = $s3Repo->retention_full_type ?? 'count'; $this->s3RepoRetentionFull = $s3Repo->retention_full ?? 2; $this->s3RepoRetentionDiff = $s3Repo->retention_diff ?? 7; } } private function syncPgbackrestRepos(): void { $hasLocal = ! $this->disableLocalBackup; $hasS3 = $this->saveS3 && ! empty($this->s3RepoStorageId); if (! $hasLocal && ! $hasS3) { throw new \Exception( 'At least one backup repository (local or S3) must be enabled for pgBackRest. '. 'Either enable local backups or configure S3 storage and enable it.' ); } if ($hasS3 && empty($this->s3RepoStorageId)) { throw new \Exception('S3 storage must be selected when S3 backups are enabled.'); } $repoNumber = 1; if ($hasLocal) { $localRepo = $this->backup->localRepo(); if (! $localRepo) { $localRepo = new PgbackrestRepo; $localRepo->scheduled_database_backup_id = $this->backup->id; $localRepo->type = 'posix'; } $localRepo->repo_number = $repoNumber; $localRepo->enabled = true; $localRepo->retention_full_type = $this->localRepoRetentionFullType ?? 'count'; $localRepo->retention_full = $this->localRepoRetentionFull ?? 2; $localRepo->retention_diff = $this->localRepoRetentionDiff ?? 7; $localRepo->save(); $repoNumber++; } else { $this->backup->pgbackrestRepos()->where('type', 'posix')->delete(); } if ($hasS3) { if (empty($this->s3RepoStorageId)) { throw new \Exception('S3 Storage is required when enabling S3 backups for pgBackRest.'); } $s3Repo = $this->backup->s3Repo(); if (! $s3Repo) { $s3Repo = new PgbackrestRepo; $s3Repo->scheduled_database_backup_id = $this->backup->id; $s3Repo->type = 's3'; } $s3Repo->repo_number = $repoNumber; $s3Repo->enabled = true; $s3Repo->s3_storage_id = $this->s3RepoStorageId; $s3Repo->retention_full_type = $this->s3RepoRetentionFullType ?? 'count'; $s3Repo->retention_full = $this->s3RepoRetentionFull ?? 2; $s3Repo->retention_diff = $this->s3RepoRetentionDiff ?? 7; $s3Repo->save(); } else { $this->backup->pgbackrestRepos()->where('type', 's3')->delete(); } } public function delete($password) { $this->authorize('manageBackups', $this->backup->database); if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); return; } } try { $server = null; if ($this->backup->database instanceof \App\Models\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() === \App\Models\ServiceDatabase::class) { $previousUrl = url()->previous(); $url = Url::fromString($previousUrl); $url = $url->withoutQueryParameter('selectedBackupId'); $url = $url->withFragment('backups'); $url = $url->getPath()."#{$url->getFragment()}"; return redirect($url); } else { return redirect()->route('project.database.backup.index', $this->parameters); } } catch (\Exception $e) { $this->dispatch('error', 'Failed to delete backup: '.$e->getMessage()); 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()); } } private function customValidate() { if (! is_numeric($this->backup->s3_storage_id)) { $this->backup->s3_storage_id = null; } 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(); } 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 isPostgresql(): bool { return $this->backup->database_type === 'App\Models\StandalonePostgresql'; } 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.'], ], 'isPostgresql' => $this->isPostgresql(), ]); } }