feat(backup): add disable local backup option and related logic for S3 uploads

This commit is contained in:
Andras Bacsai
2025-08-17 12:34:20 +02:00
parent 5ded100300
commit 0dada987a2
5 changed files with 77 additions and 10 deletions

View File

@@ -351,6 +351,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$size = $this->calculate_size(); $size = $this->calculate_size();
if ($this->backup->save_s3) { if ($this->backup->save_s3) {
$this->upload_to_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)); $this->team->notify(new BackupSuccess($this->backup, $this->database, $database));

View File

@@ -64,6 +64,9 @@ class BackupEdit extends Component
#[Validate(['required', 'boolean'])] #[Validate(['required', 'boolean'])]
public bool $saveS3 = false; public bool $saveS3 = false;
#[Validate(['required', 'boolean'])]
public bool $disableLocalBackup = false;
#[Validate(['nullable', 'integer'])] #[Validate(['nullable', 'integer'])]
public ?int $s3StorageId = 1; 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_days_s3 = $this->databaseBackupRetentionDaysS3;
$this->backup->database_backup_retention_max_storage_s3 = $this->databaseBackupRetentionMaxStorageS3; $this->backup->database_backup_retention_max_storage_s3 = $this->databaseBackupRetentionMaxStorageS3;
$this->backup->save_s3 = $this->saveS3; $this->backup->save_s3 = $this->saveS3;
$this->backup->disable_local_backup = $this->disableLocalBackup;
$this->backup->s3_storage_id = $this->s3StorageId; $this->backup->s3_storage_id = $this->s3StorageId;
$this->backup->databases_to_backup = $this->databasesToBackup; $this->backup->databases_to_backup = $this->databasesToBackup;
$this->backup->dump_all = $this->dumpAll; $this->backup->dump_all = $this->dumpAll;
@@ -115,6 +119,7 @@ class BackupEdit extends Component
$this->databaseBackupRetentionDaysS3 = $this->backup->database_backup_retention_days_s3; $this->databaseBackupRetentionDaysS3 = $this->backup->database_backup_retention_days_s3;
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3; $this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
$this->saveS3 = $this->backup->save_s3; $this->saveS3 = $this->backup->save_s3;
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
$this->s3StorageId = $this->backup->s3_storage_id; $this->s3StorageId = $this->backup->s3_storage_id;
$this->databasesToBackup = $this->backup->databases_to_backup; $this->databasesToBackup = $this->backup->databases_to_backup;
$this->dumpAll = $this->backup->dump_all; $this->dumpAll = $this->backup->dump_all;
@@ -193,6 +198,12 @@ class BackupEdit extends Component
if (! is_numeric($this->backup->s3_storage_id)) { if (! is_numeric($this->backup->s3_storage_id)) {
$this->backup->s3_storage_id = null; $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); $isValid = validate_cron_expression($this->backup->frequency);
if (! $isValid) { if (! $isValid) {
throw new \Exception('Invalid Cron / Human expression'); throw new \Exception('Invalid Cron / Human expression');

View File

@@ -237,11 +237,18 @@ function removeOldBackups($backup): void
{ {
try { try {
if ($backup->executions) { if ($backup->executions) {
$localBackupsToDelete = deleteOldBackupsLocally($backup); // If local backup is disabled, mark all executions as having local storage deleted
if ($localBackupsToDelete->isNotEmpty()) { if ($backup->disable_local_backup && $backup->save_s3) {
$backup->executions() $backup->executions()
->whereIn('id', $localBackupsToDelete->pluck('id')) ->where('local_storage_deleted', false)
->update(['local_storage_deleted' => true]); ->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() // Delete executions where both local and S3 storage are marked as deleted
->where('local_storage_deleted', true) // or where only S3 is enabled and S3 storage is deleted
->where('s3_storage_deleted', true) if ($backup->disable_local_backup && $backup->save_s3) {
->delete(); $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) { } catch (\Exception $e) {
throw $e; throw $e;

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->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');
});
}
};

View File

@@ -18,7 +18,7 @@
shortConfirmationLabel="Database Name" /> shortConfirmationLabel="Database Name" />
@endif @endif
</div> </div>
<div class="w-48 pb-2"> <div class="w-64 pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" /> <x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
@if ($s3s->count() > 0) @if ($s3s->count() > 0)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" /> <x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
@@ -26,11 +26,18 @@
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3" <x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
disabled /> disabled />
@endif @endif
@if ($backup->save_s3)
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@else
<x-forms.checkbox disabled label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@endif
</div> </div>
@if ($backup->save_s3) @if ($backup->save_s3)
<div class="pb-6"> <div class="pb-6">
<x-forms.select id="s3StorageId" label="S3 Storage" required> <x-forms.select id="s3StorageId" label="S3 Storage" required>
<option value="default">Select a S3 storage</option> <option value="default" disabled>Select a S3 storage</option>
@foreach ($s3s as $s3) @foreach ($s3s as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option> <option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach @endforeach
@@ -77,7 +84,7 @@
<x-forms.input label="Frequency" id="frequency" /> <x-forms.input label="Frequency" id="frequency" />
<x-forms.input label="Timezone" id="timezone" disabled <x-forms.input label="Timezone" id="timezone" disabled
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" /> helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" />
<x-forms.input label="Timeout" id="timeout" helper="The timeout of the backup job in seconds."/> <x-forms.input label="Timeout" id="timeout" helper="The timeout of the backup job in seconds." />
</div> </div>
<h3 class="mt-6 mb-2 text-lg font-medium">Backup Retention Settings</h3> <h3 class="mt-6 mb-2 text-lg font-medium">Backup Retention Settings</h3>