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'))
-