App: Add option to restore database when a DB error occurs at startup
Test / Run tests (push) Successful in 2m12s
Test / Run tests (push) Successful in 2m12s
This commit is contained in:
+8
-2
@@ -93,11 +93,15 @@ namespace Elwig {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await AppDbUpdater.CheckDb();
|
await AppDbUpdater.CheckDb();
|
||||||
|
} catch (FileNotFoundException exc) {
|
||||||
|
InteractionService.ShowException("Datenbank nicht gefunden", "Die Datenbank konnte nicht gefunden werden", exc);
|
||||||
} catch (Exception exc) {
|
} catch (Exception exc) {
|
||||||
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
|
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
|
||||||
await CheckForUpdates();
|
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();
|
Shutdown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,7 +116,9 @@ namespace Elwig {
|
|||||||
try {
|
try {
|
||||||
Client = new(ctx);
|
Client = new(ctx);
|
||||||
} catch (Exception exc) {
|
} 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();
|
Shutdown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,17 @@ namespace Elwig.Helpers {
|
|||||||
private static int VersionOffset = 0;
|
private static int VersionOffset = 0;
|
||||||
|
|
||||||
public static async Task<Version> CheckDb() {
|
public static async Task<Version> CheckDb() {
|
||||||
|
if (!File.Exists(App.Config.DatabaseFile))
|
||||||
|
throw new FileNotFoundException($"Die Datei \"{App.Config.DatabaseFile}\" exisitiert nicht");
|
||||||
|
|
||||||
long? applId, schemaVers;
|
long? applId, schemaVers;
|
||||||
using (var cnx = await AppDbContext.ConnectAsync()) {
|
using (var cnx = await AppDbContext.ConnectAsync()) {
|
||||||
|
|
||||||
|
await cnx.QuickCheck();
|
||||||
|
|
||||||
applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
|
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;
|
schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
|
||||||
VersionOffset = (int)(schemaVers % 100);
|
VersionOffset = (int)(schemaVers % 100);
|
||||||
@@ -76,16 +83,16 @@ namespace Elwig.Helpers {
|
|||||||
try {
|
try {
|
||||||
using var cnx = await AppDbContext.ConnectAsync();
|
using var cnx = await AppDbContext.ConnectAsync();
|
||||||
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
|
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
|
||||||
|
|
||||||
|
await cnx.IntegrityCheck();
|
||||||
|
await cnx.ForeignKeyCheck();
|
||||||
|
|
||||||
foreach (var script in toExecute) {
|
foreach (var script in toExecute) {
|
||||||
await cnx.ExecuteEmbeddedScript(asm, script);
|
await cnx.ExecuteEmbeddedScript(asm, script);
|
||||||
}
|
}
|
||||||
|
|
||||||
var violations = await cnx.ForeignKeyCheck();
|
await cnx.IntegrityCheck();
|
||||||
if (violations.Length > 0) {
|
await cnx.ForeignKeyCheck();
|
||||||
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.ExecuteBatch("VACUUM");
|
await cnx.ExecuteBatch("VACUUM");
|
||||||
await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
|
await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
|
||||||
|
|||||||
@@ -135,11 +135,39 @@ namespace Elwig.Helpers {
|
|||||||
return await cmd.ExecuteScalarAsync();
|
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<string>();
|
||||||
|
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<string>();
|
||||||
|
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();
|
using var cmd = cnx.CreateCommand();
|
||||||
cmd.CommandText = "PRAGMA foreign_key_check";
|
cmd.CommandText = "PRAGMA foreign_key_check";
|
||||||
using var reader = await cmd.ExecuteReaderAsync();
|
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()) {
|
while (await reader.ReadAsync()) {
|
||||||
var table = reader.GetString(0);
|
var table = reader.GetString(0);
|
||||||
var rowid = reader.GetInt64(1);
|
var rowid = reader.GetInt64(1);
|
||||||
@@ -147,7 +175,11 @@ namespace Elwig.Helpers {
|
|||||||
var fkid = reader.GetInt64(3);
|
var fkid = reader.GetInt64(3);
|
||||||
list.Add((table, rowid, parent, fkid));
|
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<T> Join<T>(this IEnumerable<T> src, Func<T> separatorFactory) {
|
public static IEnumerable<T> Join<T>(this IEnumerable<T> src, Func<T> separatorFactory) {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using Elwig.Helpers;
|
using Elwig.Helpers;
|
||||||
using Elwig.Helpers.Export;
|
using Elwig.Helpers.Export;
|
||||||
|
using Microsoft.Win32;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Elwig.Services {
|
namespace Elwig.Services {
|
||||||
public static class BackupService {
|
public static class BackupService {
|
||||||
@@ -106,5 +108,26 @@ namespace Elwig.Services {
|
|||||||
} catch { }
|
} 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,24 +182,7 @@ namespace Elwig.Windows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) {
|
private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) {
|
||||||
try {
|
await BackupService.RestoreDatabase();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SyncButton_Click(object sender, RoutedEventArgs evt) {
|
private async void SyncButton_Click(object sender, RoutedEventArgs evt) {
|
||||||
|
|||||||
Reference in New Issue
Block a user