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.
This commit is contained in:
Andras Bacsai
2026-10-01 20:31:00 +02:00
parent 5c57405331
commit fed15ff1b0
4 changed files with 55 additions and 7 deletions
@@ -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;
}
}
@@ -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);
@@ -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", [
@@ -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 () {