From cadac4302dbc653fb947a287b2c9c650d898b735 Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Wed, 26 Aug 2026 12:01:03 +0200 Subject: [PATCH] App: Add option to restore database when a DB error occurs at startup --- Elwig/App.xaml.cs | 10 +++++++-- Elwig/Helpers/AppDbUpdater.cs | 21 ++++++++++++------ Elwig/Helpers/Extensions.cs | 38 +++++++++++++++++++++++++++++--- Elwig/Services/BackupService.cs | 23 +++++++++++++++++++ Elwig/Windows/MainWindow.xaml.cs | 19 +--------------- 5 files changed, 81 insertions(+), 30 deletions(-) diff --git a/Elwig/App.xaml.cs b/Elwig/App.xaml.cs index 5c139eb..628b7ce 100644 --- a/Elwig/App.xaml.cs +++ b/Elwig/App.xaml.cs @@ -93,11 +93,15 @@ namespace Elwig { try { await AppDbUpdater.CheckDb(); + } catch (FileNotFoundException exc) { + InteractionService.ShowException("Datenbank nicht gefunden", "Die Datenbank konnte nicht gefunden werden", exc); } catch (Exception exc) { if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) { await CheckForUpdates(); } - InteractionService.ShowException("Fehlerhafte Datenbank", "Fehlerhafte Datenbank", exc); + InteractionService.ShowException("Fehlerhafte Datenbank", "Schwerwiegender Fehler in der Datenbank, unbedingt den Programmierern melden!", exc); + if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false)) + await BackupService.RestoreDatabase(); Shutdown(); return; } @@ -112,7 +116,9 @@ namespace Elwig { try { Client = new(ctx); } catch (Exception exc) { - InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten", exc); + InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten, unbedingt den Programmierern melden!", exc); + if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false)) + await BackupService.RestoreDatabase(); Shutdown(); return; } diff --git a/Elwig/Helpers/AppDbUpdater.cs b/Elwig/Helpers/AppDbUpdater.cs index 7ed3427..db54d3f 100644 --- a/Elwig/Helpers/AppDbUpdater.cs +++ b/Elwig/Helpers/AppDbUpdater.cs @@ -14,10 +14,17 @@ namespace Elwig.Helpers { private static int VersionOffset = 0; public static async Task CheckDb() { + if (!File.Exists(App.Config.DatabaseFile)) + throw new FileNotFoundException($"Die Datei \"{App.Config.DatabaseFile}\" exisitiert nicht"); + long? applId, schemaVers; using (var cnx = await AppDbContext.ConnectAsync()) { + + await cnx.QuickCheck(); + applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0; - if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})"); + if (applId != 0x454C5747) + throw new FileFormatException($"Invalid application_id in database (0x{applId:X08})"); schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0; VersionOffset = (int)(schemaVers % 100); @@ -76,16 +83,16 @@ namespace Elwig.Helpers { try { using var cnx = await AppDbContext.ConnectAsync(); await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE"); + + await cnx.IntegrityCheck(); + await cnx.ForeignKeyCheck(); + foreach (var script in toExecute) { await cnx.ExecuteEmbeddedScript(asm, script); } - var violations = await cnx.ForeignKeyCheck(); - if (violations.Length > 0) { - throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations - .Take(50) - .Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}"))); - } + await cnx.IntegrityCheck(); + await cnx.ForeignKeyCheck(); await cnx.ExecuteBatch("VACUUM"); await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}"); diff --git a/Elwig/Helpers/Extensions.cs b/Elwig/Helpers/Extensions.cs index 18f7310..f0e348c 100644 --- a/Elwig/Helpers/Extensions.cs +++ b/Elwig/Helpers/Extensions.cs @@ -135,11 +135,39 @@ namespace Elwig.Helpers { return await cmd.ExecuteScalarAsync(); } - public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(this SqliteConnection cnx) { + public static async Task QuickCheck(this SqliteConnection cnx) { + using var cmd = cnx.CreateCommand(); + cmd.CommandText = "PRAGMA quick_check"; + using var reader = await cmd.ExecuteReaderAsync(); + var list = new List(); + while (await reader.ReadAsync()) { + list.Add(reader.GetString(0)); + } + + if (list.Count != 1 || list[0] != "ok") { + throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50))); + } + } + + public static async Task IntegrityCheck(this SqliteConnection cnx) { + using var cmd = cnx.CreateCommand(); + cmd.CommandText = "PRAGMA integrity_check"; + using var reader = await cmd.ExecuteReaderAsync(); + var list = new List(); + while (await reader.ReadAsync()) { + list.Add(reader.GetString(0)); + } + + if (list.Count != 1 || list[0] != "ok") { + throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50))); + } + } + + public static async Task ForeignKeyCheck(this SqliteConnection cnx) { using var cmd = cnx.CreateCommand(); cmd.CommandText = "PRAGMA foreign_key_check"; using var reader = await cmd.ExecuteReaderAsync(); - var list = new List<(string, long, string, long)>(); + var list = new List<(string Table, long RowId, string Parent, long FkId)>(); while (await reader.ReadAsync()) { var table = reader.GetString(0); var rowid = reader.GetInt64(1); @@ -147,7 +175,11 @@ namespace Elwig.Helpers { var fkid = reader.GetInt64(3); list.Add((table, rowid, parent, fkid)); } - return [.. list]; + + if (list.Count > 0) { + throw new InvalidDataException($"Foreign key violations ({list.Count}):\n" + string.Join("\n", list.Take(50) + .Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}"))); + } } public static IEnumerable Join(this IEnumerable src, Func separatorFactory) { diff --git a/Elwig/Services/BackupService.cs b/Elwig/Services/BackupService.cs index 68a3f63..22dee01 100644 --- a/Elwig/Services/BackupService.cs +++ b/Elwig/Services/BackupService.cs @@ -1,11 +1,13 @@ using Elwig.Helpers; using Elwig.Helpers.Export; +using Microsoft.Win32; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Windows.Input; namespace Elwig.Services { public static class BackupService { @@ -106,5 +108,26 @@ namespace Elwig.Services { } catch { } } } + + public static async Task RestoreDatabase() { + try { + var d = new OpenFileDialog() { + Title = "Datenbank wiederherstellen - Elwig", + DefaultDirectory = App.Config.BackupPath, + InitialDirectory = App.Config.BackupPath, + DefaultExt = "sql.zip", + Filter = "SQLite-Datenbank (*.sqlite3, *.sqlite3.zip, *.sql, *.sql.zip)|*.sqlite3;*.sqlite3.zip;*.sql;*.sql.zip", + }; + if (d.ShowDialog() == true) { + if (!InteractionService.AskContinue("Datenbank wiederherstellen", "Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?")) + return; + Mouse.OverrideCursor = Cursors.Wait; + await App.ReplaceDatabase(d.FileName); + } + } catch (Exception exc) { + InteractionService.ShowException(exc); + } + Mouse.OverrideCursor = null; + } } } diff --git a/Elwig/Windows/MainWindow.xaml.cs b/Elwig/Windows/MainWindow.xaml.cs index 1c88408..070a985 100644 --- a/Elwig/Windows/MainWindow.xaml.cs +++ b/Elwig/Windows/MainWindow.xaml.cs @@ -182,24 +182,7 @@ namespace Elwig.Windows { } private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) { - try { - var d = new OpenFileDialog() { - Title = "Datenbank wiederherstellen - Elwig", - DefaultDirectory = App.Config.BackupPath, - InitialDirectory = App.Config.BackupPath, - DefaultExt = "sql.zip", - Filter = "SQLite-Datenbank (*.sqlite3, *.sqlite3.zip, *.sql, *.sql.zip)|*.sqlite3;*.sqlite3.zip;*.sql;*.sql.zip", - }; - if (d.ShowDialog() == true) { - if (!InteractionService.AskContinue("Datenbank wiederherstellen", "Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?")) - return; - Mouse.OverrideCursor = Cursors.Wait; - await App.ReplaceDatabase(d.FileName); - } - } catch (Exception exc) { - InteractionService.ShowException(exc); - } - Mouse.OverrideCursor = null; + await BackupService.RestoreDatabase(); } private async void SyncButton_Click(object sender, RoutedEventArgs evt) {