diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 6730dceb7..577c1f11a 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -299,7 +299,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue throw new \Exception('Unsupported database type'); } $size = $this->calculate_size(); - $this->remove_old_backups(); if ($this->backup->save_s3) { $this->upload_to_s3(); } @@ -323,6 +322,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database)); } } + if ($this->backup_log && $this->backup_log->status === 'success') { + removeOldBackups($this->backup); + } } catch (\Throwable $e) { throw $e; } finally { @@ -457,19 +459,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false); } - private function remove_old_backups(): void - { - if ($this->backup->number_of_backups_locally === 0) { - $deletable = $this->backup->executions()->where('status', 'success'); - } else { - $deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1); - } - foreach ($deletable->get() as $execution) { - delete_backup_locally($execution->filename, $this->server); - $execution->delete(); - } - } - private function upload_to_s3(): void { try { diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 0dea0496c..012c6bb24 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -43,8 +43,23 @@ class BackupEdit extends Component #[Validate(['string'])] public string $timezone = ''; - #[Validate(['required', 'integer', 'min:1'])] - public int $numberOfBackupsLocally = 1; + #[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; @@ -73,7 +88,12 @@ class BackupEdit extends Component if ($toModel) { $this->backup->enabled = $this->backupEnabled; $this->backup->frequency = $this->frequency; - $this->backup->number_of_backups_locally = $this->numberOfBackupsLocally; + $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->s3_storage_id = $this->s3StorageId; $this->backup->databases_to_backup = $this->databasesToBackup; @@ -84,7 +104,12 @@ class BackupEdit extends Component $this->backupEnabled = $this->backup->enabled; $this->frequency = $this->backup->frequency; $this->timezone = data_get($this->backup->server(), 'settings.server_timezone', 'Instance timezone'); - $this->numberOfBackupsLocally = $this->backup->number_of_backups_locally; + $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->s3StorageId = $this->backup->s3_storage_id; $this->databasesToBackup = $this->backup->databases_to_backup; @@ -103,11 +128,29 @@ class BackupEdit extends Component } try { - if ($this->delete_associated_backups_locally) { - $this->deleteAssociatedBackupsLocally(); + $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; } - if ($this->delete_associated_backups_s3) { - $this->deleteAssociatedBackupsS3(); + + $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(); @@ -123,7 +166,9 @@ class BackupEdit extends Component } else { return redirect()->route('project.database.backup.index', $this->parameters); } - } catch (\Throwable $e) { + } catch (\Exception $e) { + $this->dispatch('error', 'Failed to delete backup: '.$e->getMessage()); + return handleError($e, $this); } } @@ -160,63 +205,12 @@ class BackupEdit extends Component } } - private function deleteAssociatedBackupsLocally() - { - $executions = $this->backup->executions; - $backupFolder = null; - - foreach ($executions as $execution) { - if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { - $server = $this->backup->database->service->destination->server; - } else { - $server = $this->backup->database->destination->server; - } - - if (! $backupFolder) { - $backupFolder = dirname($execution->filename); - } - - delete_backup_locally($execution->filename, $server); - $execution->delete(); - } - - if (str($backupFolder)->isNotEmpty()) { - $this->deleteEmptyBackupFolder($backupFolder, $server); - } - } - - private function deleteAssociatedBackupsS3() - { - // Add function to delete backups from S3 - } - - private function deleteAssociatedBackupsSftp() - { - // Add function to delete backups from SFTP - } - - private function deleteEmptyBackupFolder($folderPath, $server) - { - $checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server); - - if (trim($checkEmpty) === 'empty') { - instant_remote_process(["rmdir '$folderPath'"], $server); - - $parentFolder = dirname($folderPath); - $checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server); - - if (trim($checkParentEmpty) === 'empty') { - instant_remote_process(["rmdir '$parentFolder'"], $server); - } - } - } - 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 associated with this backup job from this database will be permanently deleted from the selected S3 Storage.'] + ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job for this database will be permanently deleted 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.'] ], ]); diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index ce168a352..7eef1a539 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -18,9 +18,9 @@ class BackupExecutions extends Component public $setDeletableBackup; - public $delete_backup_s3 = true; + public $delete_backup_s3 = false; - public $delete_backup_sftp = true; + public $delete_backup_sftp = false; public function getListeners() { @@ -57,23 +57,25 @@ class BackupExecutions extends Component return; } - if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { - delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); - } else { - delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); - } + $server = $execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class + ? $execution->scheduledDatabaseBackup->database->service->destination->server + : $execution->scheduledDatabaseBackup->database->destination->server; - if ($this->delete_backup_s3) { - // Add logic to delete from S3 - } + try { + if ($execution->filename) { + deleteBackupsLocally($execution->filename, $server); - if ($this->delete_backup_sftp) { - // Add logic to delete from SFTP - } + if ($this->delete_backup_s3 && $execution->scheduledDatabaseBackup->s3) { + deleteBackupsS3($execution->filename, $execution->scheduledDatabaseBackup->s3); + } + } - $execution->delete(); - $this->dispatch('success', 'Backup deleted.'); - $this->refreshBackupExecutions(); + $execution->delete(); + $this->dispatch('success', 'Backup deleted.'); + $this->refreshBackupExecutions(); + } catch (\Exception $e) { + $this->dispatch('error', 'Failed to delete backup: '.$e->getMessage()); + } } public function download_file($exeuctionId) @@ -143,7 +145,7 @@ class BackupExecutions extends Component return view('livewire.project.database.backup-executions', [ 'checkboxes' => [ ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'], - ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'], + // ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'], ], ]); } diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index e12910f82..6a834ee6f 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -1,6 +1,7 @@ first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destinationUuid)->firstOrFail(); $database = new StandalonePostgresql; $database->name = generate_database_name('postgresql'); $database->image = $databaseImage; @@ -43,10 +41,7 @@ function create_standalone_postgresql($environmentId, $destinationUuid, ?array $ function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis { - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail(); $database = new StandaloneRedis; $database->name = generate_database_name('redis'); $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); @@ -77,10 +72,7 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb { - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail(); $database = new StandaloneMongodb; $database->name = generate_database_name('mongodb'); $database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); @@ -94,12 +86,10 @@ function create_standalone_mongodb($environment_id, $destination_uuid, ?array $o return $database; } + function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql { - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail(); $database = new StandaloneMysql; $database->name = generate_database_name('mysql'); $database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); @@ -114,12 +104,10 @@ function create_standalone_mysql($environment_id, $destination_uuid, ?array $oth return $database; } + function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb { - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail(); $database = new StandaloneMariadb; $database->name = generate_database_name('mariadb'); $database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); @@ -127,7 +115,6 @@ function create_standalone_mariadb($environment_id, $destination_uuid, ?array $o $database->environment_id = $environment_id; $database->destination_id = $destination->id; $database->destination_type = $destination->getMorphClass(); - if ($otherData) { $database->fill($otherData); } @@ -135,12 +122,10 @@ function create_standalone_mariadb($environment_id, $destination_uuid, ?array $o return $database; } + function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb { - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail(); $database = new StandaloneKeydb; $database->name = generate_database_name('keydb'); $database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); @@ -157,10 +142,7 @@ function create_standalone_keydb($environment_id, $destination_uuid, ?array $oth function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly { - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail(); $database = new StandaloneDragonfly; $database->name = generate_database_name('dragonfly'); $database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false); @@ -174,12 +156,10 @@ function create_standalone_dragonfly($environment_id, $destination_uuid, ?array return $database; } + function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse { - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (! $destination) { - throw new Exception('Destination not found'); - } + $destination = StandaloneDocker::where('uuid', $destination_uuid)->firstOrFail(); $database = new StandaloneClickhouse; $database->name = generate_database_name('clickhouse'); $database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false); @@ -194,12 +174,231 @@ function create_standalone_clickhouse($environment_id, $destination_uuid, ?array return $database; } -function delete_backup_locally(?string $filename, Server $server): void +function deleteBackupsLocally(string|array|null $filenames, Server $server): void { - if (empty($filename)) { + if (empty($filenames)) { return; } - instant_remote_process(["rm -f \"{$filename}\""], $server, throwError: false); + if (is_string($filenames)) { + $filenames = [$filenames]; + } + $quotedFiles = array_map(fn ($file) => "\"$file\"", $filenames); + instant_remote_process(['rm -f '.implode(' ', $quotedFiles)], $server, throwError: false); + + $foldersToCheck = collect($filenames)->map(fn ($file) => dirname($file))->unique(); + $foldersToCheck->each(fn ($folder) => deleteEmptyBackupFolder($folder, $server)); +} + +function deleteBackupsS3(string|array|null $filenames, S3Storage $s3): void +{ + if (empty($filenames) || ! $s3) { + return; + } + if (is_string($filenames)) { + $filenames = [$filenames]; + } + + $disk = Storage::build([ + 'driver' => 's3', + 'key' => $s3->key, + 'secret' => $s3->secret, + 'region' => $s3->region, + 'bucket' => $s3->bucket, + 'endpoint' => $s3->endpoint, + 'use_path_style_endpoint' => true, + 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(), + 'aws_url' => $s3->awsUrl(), + ]); + + $disk->delete($filenames); +} + +function deleteEmptyBackupFolder($folderPath, Server $server): void +{ + $escapedPath = escapeshellarg($folderPath); + $escapedParentPath = escapeshellarg(dirname($folderPath)); + + $checkEmpty = instant_remote_process(["[ -d $escapedPath ] && [ -z \"$(ls -A $escapedPath)\" ] && echo 'empty' || echo 'not empty'"], $server, throwError: false); + + if (trim($checkEmpty) === 'empty') { + instant_remote_process(["rmdir $escapedPath"], $server, throwError: false); + $checkParentEmpty = instant_remote_process(["[ -d $escapedParentPath ] && [ -z \"$(ls -A $escapedParentPath)\" ] && echo 'empty' || echo 'not empty'"], $server, throwError: false); + if (trim($checkParentEmpty) === 'empty') { + instant_remote_process(["rmdir $escapedParentPath"], $server, throwError: false); + } + } +} + +function removeOldBackups($backup): void +{ + try { + $processedBackups = deleteOldBackupsLocally($backup); + + if ($backup->save_s3) { + $processedBackups = $processedBackups->merge(deleteOldBackupsFromS3($backup)); + } + + if ($processedBackups->isNotEmpty()) { + $backup->executions()->whereIn('id', $processedBackups->pluck('id'))->delete(); + } + } catch (\Exception $e) { + throw $e; + } +} + +function deleteOldBackupsLocally($backup): Collection +{ + if (! $backup || ! $backup->executions) { + return collect(); + } + + $successfulBackups = $backup->executions() + ->where('status', 'success') + ->orderBy('created_at', 'desc') + ->get(); + + if ($successfulBackups->isEmpty()) { + return collect(); + } + + $retentionAmount = $backup->database_backup_retention_amount_locally; + $retentionDays = $backup->database_backup_retention_days_locally; + $maxStorageGB = $backup->database_backup_retention_max_storage_locally; + + if ($retentionAmount === 0 && $retentionDays === 0 && $maxStorageGB === 0) { + return collect(); + } + + $backupsToDelete = collect(); + + if ($retentionAmount > 0) { + $byAmount = $successfulBackups->skip($retentionAmount); + $backupsToDelete = $backupsToDelete->merge($byAmount); + } + + if ($retentionDays > 0) { + $oldestAllowedDate = $successfulBackups->first()->created_at->clone()->utc()->subDays($retentionDays); + $oldBackups = $successfulBackups->filter(fn ($execution) => $execution->created_at->utc() < $oldestAllowedDate); + $backupsToDelete = $backupsToDelete->merge($oldBackups); + } + + if ($maxStorageGB > 0) { + $maxStorageBytes = $maxStorageGB * pow(1024, 3); + $totalSize = 0; + $backupsOverLimit = collect(); + + $backupsToCheck = $successfulBackups->skip(1); + + foreach ($backupsToCheck as $backupExecution) { + $totalSize += (int) $backupExecution->size; + if ($totalSize > $maxStorageBytes) { + $backupsOverLimit = $successfulBackups->filter( + fn ($b) => $b->created_at->utc() <= $backupExecution->created_at->utc() + )->skip(1); + break; + } + } + + $backupsToDelete = $backupsToDelete->merge($backupsOverLimit); + } + + $backupsToDelete = $backupsToDelete->unique('id'); + $processedBackups = collect(); + + $server = null; + if ($backup->database_type === \App\Models\ServiceDatabase::class) { + $server = $backup->database->service->server; + } else { + $server = $backup->database->destination->server; + } + + if (! $server) { + return collect(); + } + + $filesToDelete = $backupsToDelete + ->filter(fn ($execution) => ! empty($execution->filename)) + ->pluck('filename') + ->all(); + + if (! empty($filesToDelete)) { + deleteBackupsLocally($filesToDelete, $server); + $processedBackups = $backupsToDelete; + } + + return $processedBackups; +} + +function deleteOldBackupsFromS3($backup): Collection +{ + if (! $backup || ! $backup->executions || ! $backup->s3) { + return collect(); + } + + $successfulBackups = $backup->executions() + ->where('status', 'success') + ->orderBy('created_at', 'desc') + ->get(); + + if ($successfulBackups->isEmpty()) { + return collect(); + } + + $retentionAmount = $backup->database_backup_retention_amount_s3; + $retentionDays = $backup->database_backup_retention_days_s3; + $maxStorageGB = $backup->database_backup_retention_max_storage_s3; + + if ($retentionAmount === 0 && $retentionDays === 0 && $maxStorageGB === 0) { + return collect(); + } + + $backupsToDelete = collect(); + + if ($retentionAmount > 0) { + $byAmount = $successfulBackups->skip($retentionAmount); + $backupsToDelete = $backupsToDelete->merge($byAmount); + } + + if ($retentionDays > 0) { + $oldestAllowedDate = $successfulBackups->first()->created_at->clone()->utc()->subDays($retentionDays); + $oldBackups = $successfulBackups->filter(fn ($execution) => $execution->created_at->utc() < $oldestAllowedDate); + $backupsToDelete = $backupsToDelete->merge($oldBackups); + } + + if ($maxStorageGB > 0) { + $maxStorageBytes = $maxStorageGB * pow(1024, 3); + $totalSize = 0; + $backupsOverLimit = collect(); + + $backupsToCheck = $successfulBackups->skip(1); + + foreach ($backupsToCheck as $backupExecution) { + $totalSize += (int) $backupExecution->size; + if ($totalSize > $maxStorageBytes) { + $backupsOverLimit = $successfulBackups->filter( + fn ($b) => $b->created_at->utc() <= $backupExecution->created_at->utc() + )->skip(1); + break; + } + } + + $backupsToDelete = $backupsToDelete->merge($backupsOverLimit); + } + + $backupsToDelete = $backupsToDelete->unique('id'); + $processedBackups = collect(); + + $filesToDelete = $backupsToDelete + ->filter(fn ($execution) => ! empty($execution->filename)) + ->pluck('filename') + ->all(); + + if (! empty($filesToDelete)) { + deleteBackupsS3($filesToDelete, $backup->s3); + $processedBackups = $backupsToDelete; + } + + return $processedBackups; } function isPublicPortAlreadyUsed(Server $server, int $port, ?string $id = null): bool diff --git a/database/migrations/2025_01_13_130238_add_backup_retention_fields_to_scheduled_database_backups_table.php b/database/migrations/2025_01_13_130238_add_backup_retention_fields_to_scheduled_database_backups_table.php new file mode 100644 index 000000000..f06bc367e --- /dev/null +++ b/database/migrations/2025_01_13_130238_add_backup_retention_fields_to_scheduled_database_backups_table.php @@ -0,0 +1,36 @@ +renameColumn('number_of_backups_locally', 'database_backup_retention_amount_locally'); + $table->integer('database_backup_retention_amount_locally')->default(0)->nullable(false)->change(); + $table->integer('database_backup_retention_days_locally')->default(0)->nullable(false); + $table->decimal('database_backup_retention_max_storage_locally', 17, 7)->default(0)->nullable(false); + + $table->integer('database_backup_retention_amount_s3')->default(0)->nullable(false); + $table->integer('database_backup_retention_days_s3')->default(0)->nullable(false); + $table->decimal('database_backup_retention_max_storage_s3', 17, 7)->default(0)->nullable(false); + }); + } + + public function down() + { + Schema::table('scheduled_database_backups', function (Blueprint $table) { + $table->renameColumn('database_backup_retention_amount_locally', 'number_of_backups_locally')->nullable(true)->change(); + $table->dropColumn([ + 'database_backup_retention_days_locally', + 'database_backup_retention_max_storage_locally', + 'database_backup_retention_amount_s3', + 'database_backup_retention_days_s3', + 'database_backup_retention_max_storage_s3', + ]); + }); + } +}; diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index 59628636d..f1f604a7d 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -5,17 +5,13 @@ Save @if (str($status)->startsWith('running')) - + @endif @if ($backup->database_id !== 0) - + ]" confirmationText="{{ $backup->database->name }}" confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below" shortConfirmationLabel="Database Name" /> @endif
@@ -23,56 +19,88 @@
@if ($backup->save_s3) -
- - - @foreach ($s3s as $s3) - - @endforeach - -
+
+ + + @foreach ($s3s as $s3) + + @endforeach + +
@endif

Settings

@if ($backup->database_type === 'App\Models\StandalonePostgresql' && $backup->database_id !== 0) -
- -
- @if (!$backup->dump_all) - - @endif +
+ +
+ @if (!$backup->dump_all) + + @endif @elseif($backup->database_type === 'App\Models\StandaloneMongodb') - + @elseif($backup->database_type === 'App\Models\StandaloneMysql') -
- -
- @if (!$backup->dump_all) - - @endif +
+ +
+ @if (!$backup->dump_all) + + @endif @elseif($backup->database_type === 'App\Models\StandaloneMariadb') -
- -
- @if (!$backup->dump_all) - - @endif +
+ +
+ @if (!$backup->dump_all) + + @endif @endif
- - + +
+ +

Backup Retention Settings

+
+
    +
  • Setting a value to 0 means unlimited retention
  • +
  • The retention rules work independently - whichever limit is reached first will trigger cleanup
  • +
+
+ +
+
+

Local Backup Retention

+
+ + + +
+
+ + @if ($backup->save_s3) +
+

S3 Storage Retention

+
+ + + +
+
+ @endif
diff --git a/resources/views/livewire/project/database/backup-executions.blade.php b/resources/views/livewire/project/database/backup-executions.blade.php index ff2bc15b2..7f8350a3e 100644 --- a/resources/views/livewire/project/database/backup-executions.blade.php +++ b/resources/views/livewire/project/database/backup-executions.blade.php @@ -47,6 +47,7 @@ @endif