From fed15ff1b09a478148af10cea9022ce9113929e4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:31:00 +0200 Subject: [PATCH] fix(backups): back up every SQLite file unless the user picks some New SQLite backup schedules copied the database's file list at creation, so files added later were never backed up. Schedules created in the UI or the API without an explicit file list now store none, and the backup job resolves the current files at run time. An explicit choice is still stored. Existing schedules are not changed. --- .../Controllers/Api/DatabasesController.php | 2 - .../Database/CreateScheduledBackup.php | 2 - .../Api/DatabaseBackupCreationApiTest.php | 18 ++++++++- .../CreateScheduledBackupValidationTest.php | 40 ++++++++++++++++++- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 3c9c832d31..652871e89b 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1017,8 +1017,6 @@ class DatabasesController extends Controller $backupData['databases_to_backup'] = $database->mariadb_database; } elseif ($database->type() === 'standalone-clickhouse') { $backupData['databases_to_backup'] = $database->clickhouse_db; - } elseif ($database->type() === 'standalone-sqlite') { - $backupData['databases_to_backup'] = $database->sqlite_databases; } } diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index e113173ffc..96d2ac7aaf 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -80,8 +80,6 @@ class CreateScheduledBackup extends Component $payload['databases_to_backup'] = $database->mariadb_database; } elseif ($database->type() === 'standalone-clickhouse') { $payload['databases_to_backup'] = $database->clickhouse_db; - } elseif ($database->type() === 'standalone-sqlite') { - $payload['databases_to_backup'] = $database->sqlite_databases; } $databaseBackup = ScheduledDatabaseBackup::create($payload); diff --git a/tests/Feature/Api/DatabaseBackupCreationApiTest.php b/tests/Feature/Api/DatabaseBackupCreationApiTest.php index 0eab7b8fd0..9a5cdc90c9 100644 --- a/tests/Feature/Api/DatabaseBackupCreationApiTest.php +++ b/tests/Feature/Api/DatabaseBackupCreationApiTest.php @@ -130,7 +130,7 @@ describe('POST /api/v1/databases/{uuid}/backups', function () { ->and($backup->database_type)->toBe(StandaloneClickhouse::class); }); - test('defaults sqlite backups to every configured database file', function () { + test('leaves sqlite backup files unset so every configured file is backed up at run time', function () { $database = create_standalone_sqlite($this->environment->id, $this->destination, ['sqlite_databases' => 'app.db,jobs.db']); $response = $this->withHeaders(backupHeaders()) @@ -142,10 +142,24 @@ describe('POST /api/v1/databases/{uuid}/backups', function () { $backup = ScheduledDatabaseBackup::where('uuid', $response->json('uuid'))->firstOrFail(); - expect($backup->databases_to_backup)->toBe('app.db,jobs.db') + expect($backup->databases_to_backup)->toBeNull() ->and($backup->database_type)->toBe(StandaloneSqlite::class); }); + test('keeps explicitly selected sqlite backup files', function () { + $database = create_standalone_sqlite($this->environment->id, $this->destination, ['sqlite_databases' => 'app.db,jobs.db']); + + $response = $this->withHeaders(backupHeaders()) + ->postJson("/api/v1/databases/{$database->uuid}/backups", [ + 'frequency' => 'daily', + 'databases_to_backup' => 'jobs.db', + ]); + + $response->assertCreated(); + + expect(ScheduledDatabaseBackup::where('uuid', $response->json('uuid'))->firstOrFail()->databases_to_backup)->toBe('jobs.db'); + }); + test('creates backup configuration with valid frequency', function () { $response = $this->withHeaders(backupHeaders()) ->postJson("/api/v1/databases/{$this->database->uuid}/backups", [ diff --git a/tests/Feature/CreateScheduledBackupValidationTest.php b/tests/Feature/CreateScheduledBackupValidationTest.php index 23a212b3bc..18e6e33d0a 100644 --- a/tests/Feature/CreateScheduledBackupValidationTest.php +++ b/tests/Feature/CreateScheduledBackupValidationTest.php @@ -3,6 +3,8 @@ use App\Jobs\DatabaseBackupJob; use App\Livewire\Project\Database\CreateScheduledBackup; use App\Models\Environment; +use App\Models\InstanceSettings; +use App\Models\PrivateKey; use App\Models\Project; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; @@ -16,6 +18,8 @@ use App\Models\StandaloneSqlite; use App\Models\Team; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Facades\Process; use Livewire\Livewire; uses(RefreshDatabase::class); @@ -201,7 +205,41 @@ it('creates a sqlite backup for its configured database files', function () { $backup = ScheduledDatabaseBackup::firstOrFail(); expect($backup->database_type)->toBe(StandaloneSqlite::class) - ->and($backup->databases_to_backup)->toBe('app.db,jobs.db'); + ->and($backup->databases_to_backup)->toBeNull(); +}); + +it('backs up sqlite files added to the database after the backup was scheduled', function () { + InstanceSettings::forceCreate(['id' => 0]); + Notification::fake(); + $this->server->update(['private_key_id' => PrivateKey::factory()->create(['team_id' => $this->team->id])->id]); + $this->server->settings()->update(['is_reachable' => true, 'is_usable' => true]); + $database = create_standalone_sqlite($this->environment->id, $this->destination, [ + 'sqlite_databases' => 'app.db', + 'status' => 'running:healthy', + ]); + + Livewire::test(CreateScheduledBackup::class, ['database' => $database]) + ->set('frequency', 'daily') + ->call('submit'); + + $database->update(['sqlite_databases' => 'app.db,audit.db']); + + $commands = collect(); + Process::fake(function ($process) use ($commands) { + $commands->push(is_array($process->command) ? implode(' ', $process->command) : $process->command); + + return Process::result(output: ''); + }); + + try { + (new DatabaseBackupJob(ScheduledDatabaseBackup::query()->sole()))->handle(); + } catch (Throwable) { + // Faked remote commands return no backup size; only the files the job dumped matter here. + } + + $dumpedFiles = $commands->filter(fn (string $command): bool => str_contains($command, 'VACUUM INTO'))->implode("\n"); + expect($dumpedFiles)->toContain('/var/lib/sqlite/app.db') + ->toContain('/var/lib/sqlite/audit.db'); }); it('rejects scheduled backups for unsupported database types', function () {