diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 428cdfda2..7ec5656da 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -351,6 +351,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $size = $this->calculate_size(); if ($this->backup->save_s3) { $this->upload_to_s3(); + + // If local backup is disabled, delete the local file immediately after S3 upload + if ($this->backup->disable_local_backup) { + deleteBackupsLocally($this->backup_location, $this->server); + $this->add_to_backup_output('Local backup file deleted after S3 upload (disable_local_backup enabled).'); + } } $this->team->notify(new BackupSuccess($this->backup, $this->database, $database)); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index abc88d736..8a5b3d9a2 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -64,6 +64,9 @@ class BackupEdit extends Component #[Validate(['required', 'boolean'])] public bool $saveS3 = false; + #[Validate(['required', 'boolean'])] + public bool $disableLocalBackup = false; + #[Validate(['nullable', 'integer'])] public ?int $s3StorageId = 1; @@ -98,6 +101,7 @@ class BackupEdit extends Component $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; $this->backup->databases_to_backup = $this->databasesToBackup; $this->backup->dump_all = $this->dumpAll; @@ -115,6 +119,7 @@ class BackupEdit extends Component $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->databasesToBackup = $this->backup->databases_to_backup; $this->dumpAll = $this->backup->dump_all; @@ -193,6 +198,12 @@ class BackupEdit extends Component if (! is_numeric($this->backup->s3_storage_id)) { $this->backup->s3_storage_id = null; } + + // Validate that disable_local_backup can only be true when S3 backup is enabled + if ($this->backup->disable_local_backup && ! $this->backup->save_s3) { + throw new \Exception('Local backup can only be disabled when S3 backup is enabled.'); + } + $isValid = validate_cron_expression($this->backup->frequency); if (! $isValid) { throw new \Exception('Invalid Cron / Human expression'); diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 48962f89c..5dbd46b5e 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -237,11 +237,18 @@ function removeOldBackups($backup): void { try { if ($backup->executions) { - $localBackupsToDelete = deleteOldBackupsLocally($backup); - if ($localBackupsToDelete->isNotEmpty()) { + // If local backup is disabled, mark all executions as having local storage deleted + if ($backup->disable_local_backup && $backup->save_s3) { $backup->executions() - ->whereIn('id', $localBackupsToDelete->pluck('id')) + ->where('local_storage_deleted', false) ->update(['local_storage_deleted' => true]); + } else { + $localBackupsToDelete = deleteOldBackupsLocally($backup); + if ($localBackupsToDelete->isNotEmpty()) { + $backup->executions() + ->whereIn('id', $localBackupsToDelete->pluck('id')) + ->update(['local_storage_deleted' => true]); + } } } @@ -254,10 +261,18 @@ function removeOldBackups($backup): void } } - $backup->executions() - ->where('local_storage_deleted', true) - ->where('s3_storage_deleted', true) - ->delete(); + // Delete executions where both local and S3 storage are marked as deleted + // or where only S3 is enabled and S3 storage is deleted + if ($backup->disable_local_backup && $backup->save_s3) { + $backup->executions() + ->where('s3_storage_deleted', true) + ->delete(); + } else { + $backup->executions() + ->where('local_storage_deleted', true) + ->where('s3_storage_deleted', true) + ->delete(); + } } catch (\Exception $e) { throw $e; diff --git a/database/migrations/2025_08_17_102422_add_disable_local_backup_to_scheduled_database_backups_table.php b/database/migrations/2025_08_17_102422_add_disable_local_backup_to_scheduled_database_backups_table.php new file mode 100644 index 000000000..e414472df --- /dev/null +++ b/database/migrations/2025_08_17_102422_add_disable_local_backup_to_scheduled_database_backups_table.php @@ -0,0 +1,28 @@ +boolean('disable_local_backup')->default(false)->after('save_s3'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('scheduled_database_backups', function (Blueprint $table) { + $table->dropColumn('disable_local_backup'); + }); + } +}; diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index c070edba2..94a187ad8 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -18,7 +18,7 @@ shortConfirmationLabel="Database Name" /> @endif -