feat(backups): support scheduled sqlite backups

This commit is contained in:
peaklabs-dev
2026-09-24 19:03:43 +02:00
parent f9d93eb7d4
commit 9998dccc0a
4 changed files with 45 additions and 3 deletions
+38 -2
View File
@@ -13,6 +13,7 @@ use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneSqlite;
use App\Models\Team;
use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
@@ -44,7 +45,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public Server $server;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneClickhouse|ServiceDatabase $database;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneClickhouse|StandaloneSqlite|ServiceDatabase $database;
public ?string $container_name = null;
@@ -282,6 +283,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databasesToBackup = [$this->database->mariadb_database];
} elseif ($this->database instanceof StandaloneClickhouse) {
$databasesToBackup = [$this->database->clickhouse_db];
} elseif ($this->database instanceof StandaloneSqlite) {
$databasesToBackup = $this->database->sqlite_databases;
} else {
return;
}
@@ -398,6 +401,18 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
]);
BackupCreated::dispatch($this->team->id);
$this->backup_standalone_clickhouse($database);
} elseif ($this->database instanceof StandaloneSqlite) {
$this->backup_file = "/sqlite-backup-$database-".Carbon::now()->timestamp.'.gz';
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'uuid' => $this->backup_log_uuid,
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
'local_storage_deleted' => false,
]);
BackupCreated::dispatch($this->team->id);
$this->backup_standalone_sqlite($database);
} else {
throw new \Exception('Unsupported database type');
}
@@ -622,7 +637,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return array_map('trim', explode('|', $databases));
}
if ($type->contains(['postgres', 'mysql', 'mariadb', 'clickhouse'])) {
if ($type->contains(['postgres', 'mysql', 'mariadb', 'clickhouse', 'sqlite'])) {
return array_map('trim', explode(',', $databases));
}
@@ -742,6 +757,27 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
private function backup_standalone_sqlite(string $database): void
{
try {
if (! preg_match(StandaloneSqlite::DATABASES_PATTERN, $database)) {
throw new \Exception("Invalid database file name: {$database}");
}
$commands[] = 'mkdir -p '.escapeshellarg($this->backup_dir);
$script = 'f=$(mktemp) && sqlite3 -readonly '.escapeshellarg(StandaloneSqlite::DATA_DIRECTORY.'/'.$database).' "VACUUM INTO \'$f\'" && cat "$f"; s=$?; rm -f "$f"; exit $s';
$dumpCommand = 'docker exec '.escapeshellarg($this->container_name).' sh -c '.escapeshellarg($script);
$commands[] = $this->buildCompressedDumpCommand($dumpCommand).' > '.escapeshellarg($this->backup_location);
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
} catch (Throwable $e) {
$this->add_to_error_output($e->getMessage());
throw $e;
}
}
private function add_to_backup_output($output): void
{
if ($this->backup_output) {
@@ -80,6 +80,8 @@ 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);
@@ -79,6 +79,10 @@
<x-forms.input label="Databases to back up"
helper="Comma-separated database names. Leave empty to include the default database."
id="databasesToBackup" />
@elseif ($backup->database_type === 'App\Models\StandaloneSqlite')
<x-forms.input label="Database files to back up"
helper="Comma-separated file names inside the data volume. Leave empty to back up every configured file."
id="databasesToBackup" />
@endif
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
+1 -1
View File
@@ -367,7 +367,7 @@ test('all dump all database commands use shared helper compression', function ()
->toContain('$this->buildCompressedDumpCommand($backupCommand)')
->toContain('mysqldump -u root')
->toContain('mariadb-dump -u root')
->and(substr_count($source, '$this->buildCompressedDumpCommand($dumpCommand)'))->toBe(2)
->and(substr_count($source, '$this->buildCompressedDumpCommand($dumpCommand)'))->toBe(3)
->and($source)->not->toContain('| gzip >');
});