diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 27fefb33ef..c6eaaf84e0 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -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) { diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index 96d2ac7aaf..e113173ffc 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -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); diff --git a/resources/views/livewire/project/database/backup-edit/general.blade.php b/resources/views/livewire/project/database/backup-edit/general.blade.php index 5c794f2aaf..49385b0258 100644 --- a/resources/views/livewire/project/database/backup-edit/general.blade.php +++ b/resources/views/livewire/project/database/backup-edit/general.blade.php @@ -79,6 +79,10 @@ + @elseif ($backup->database_type === 'App\Models\StandaloneSqlite') + @endif
diff --git a/tests/Feature/Jobs/DatabaseBackupJobTest.php b/tests/Feature/Jobs/DatabaseBackupJobTest.php index 23e1bbd8d2..1c4047252d 100644 --- a/tests/Feature/Jobs/DatabaseBackupJobTest.php +++ b/tests/Feature/Jobs/DatabaseBackupJobTest.php @@ -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 >'); });