diff --git a/Elwig/App.xaml.cs b/Elwig/App.xaml.cs index c08cec3..8b446a3 100644 --- a/Elwig/App.xaml.cs +++ b/Elwig/App.xaml.cs @@ -242,7 +242,9 @@ namespace Elwig { public static async Task ReplaceDatabase(string filename) { try { - await ElwigData.ImportDatabase(filename); + await Task.Run(async () => { + await Database.Import(filename); + }); MessageBox.Show("Das Ersetzen war erfolgreich!\n\nBitte starten Sie Elwig neu!", "Datenbank ersetzen", MessageBoxButton.OK, MessageBoxImage.Information); ForceShutdown = true; Current.Shutdown(); diff --git a/Elwig/Helpers/Export/Database.cs b/Elwig/Helpers/Export/Database.cs new file mode 100644 index 0000000..d1c4aa1 --- /dev/null +++ b/Elwig/Helpers/Export/Database.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; + +namespace Elwig.Helpers.Export { + public static class Database { + + public static async Task ExportSqlite(string filename, bool zipFile) { + if (zipFile) { + File.Delete(filename); + using var zip = ZipFile.Open(filename, ZipArchiveMode.Create); + await zip.CheckIntegrity(); + var db = zip.CreateEntryFromFile(App.Config.DatabaseFile, "database.sqlite3", CompressionLevel.SmallestSize); + } else { + File.Copy(App.Config.DatabaseFile, filename, true); + } + } + + public static async Task ExportSql(string filename, bool zipFile) { + if (zipFile) { + File.Delete(filename); + using var zip = ZipFile.Open(filename, ZipArchiveMode.Create); + var entry = zip.CreateEntry("database.sql", CompressionLevel.SmallestSize); + using var stream = entry.Open(); + using var writer = new StreamWriter(stream, Utils.UTF8); + await ExportSql(writer); + } else { + using var stream = File.OpenWrite(filename); + using var writer = new StreamWriter(stream, Utils.UTF8); + await ExportSql(writer); + } + } + + public static async Task ExportSql(StreamWriter writer) { + using var cnx = await AppDbContext.ConnectAsync(); + + var tables = new List<(string Name, string Sql)>(); + using (var cmd = cnx.CreateCommand()) { + cmd.CommandText = "SELECT name, sql FROM sqlite_schema WHERE type = 'table'"; + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) { + tables.Add((reader.GetString(0), reader.GetString(1))); + } + } + + var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0; + var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0; + var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0; + + await writer.WriteLineAsync($"-- Elwig database dump, {DateTime.Now:yyyy-MM-dd, HH:mm:ss}"); + await writer.WriteLineAsync($"-- {Environment.MachineName}, Zwst. {App.BranchName}, {App.Client.Name}"); + await writer.WriteLineAsync("BEGIN TRANSACTION;"); + await writer.WriteLineAsync("PRAGMA foreign_keys=OFF;"); + await writer.WriteLineAsync($"PRAGMA application_id=0x{applId:X8};"); + await writer.WriteLineAsync($"PRAGMA user_version=0x{userVers:X8};"); + + foreach (var t in tables) { + await writer.WriteAsync(t.Sql); + await writer.WriteLineAsync(";"); + + var columnNames = new List(); + using (var cmd = cnx.CreateCommand()) { + cmd.CommandText = $"PRAGMA table_info({t.Name})"; + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) { + columnNames.Add(reader.GetString(1)); + } + } + + using (var cmd = cnx.CreateCommand()) { + cmd.CommandText = $"SELECT {string.Join(',', columnNames)} FROM {t.Name}"; + using var reader = await cmd.ExecuteReaderAsync(); + var columns = await reader.GetColumnSchemaAsync(); + var values = new object[reader.FieldCount]; + while (await reader.ReadAsync()) { + await writer.WriteAsync($"INSERT INTO {t.Name} VALUES ("); + + reader.GetValues(values); + for (int i = 0; i < columns.Count; i++) { + var c = columns[i]; + var v = values[i]; + if (i > 0) await writer.WriteAsync(","); + if (v == null || v is DBNull) { + await writer.WriteAsync("NULL"); + } else if (c.DataTypeName == "TEXT") { + await writer.WriteAsync($"'{v.ToString()?.Replace("'", "''")}'"); + } else { + await writer.WriteAsync(v.ToString()?.Replace(',', '.')); + } + } + + await writer.WriteLineAsync(");"); + } + } + } + + using (var cmd = cnx.CreateCommand()) { + cmd.CommandText = "SELECT sql FROM sqlite_schema WHERE type != 'table' AND sql IS NOT NULL"; + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) { + await writer.WriteAsync(reader.GetString(0)); + await writer.WriteLineAsync(";"); + } + } + + await writer.WriteLineAsync($"PRAGMA schema_version={schemaVers};"); + await writer.WriteLineAsync("PRAGMA foreign_keys=ON;"); + await writer.WriteLineAsync("COMMIT;"); + await writer.WriteLineAsync("VACUUM;"); + } + + public static async Task Import(string filename) { + if (filename.EndsWith(".sql")) { + await ImportSql(filename, false); + } else if (filename.EndsWith(".sql.zip")) { + await ImportSql(filename, true); + } else if (filename.EndsWith(".sqlite3")) { + await ImportSqlite(filename, false); + } else if (filename.EndsWith(".sqlite3.zip")) { + await ImportSqlite(filename, true); + } else { + throw new ArgumentException($"Unknown file extension for importing: '{filename}'"); + } + } + + public static async Task ImportSql(string filename, bool zipFile = false) { + if (zipFile) { + using var zip = ZipFile.Open(filename, ZipArchiveMode.Read); + await zip.CheckIntegrity(); + foreach (var entry in zip.Entries) { + if (entry.Name.EndsWith(".sql")) { + using var stream = entry.Open(); + using var reader = new StreamReader(stream, Utils.UTF8); + await ImportSql(reader); + return; + } + } + throw new FileFormatException("ZIP archive has to contain at least one .sql file"); + } else { + using var stream = File.Open(filename, FileMode.Open); + using var reader = new StreamReader(stream, Utils.UTF8); + await ImportSql(reader); + } + } + + public static async Task ImportSqlite(string filename, bool zipFile = false) { + if (zipFile) { + var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3"); + try { + using var zip = ZipFile.Open(filename, ZipArchiveMode.Read); + await zip.CheckIntegrity(); + foreach (var entry in zip.Entries) { + if (entry.Name.EndsWith(".sqlite3")) { + entry.ExtractToFile(newName); + await ImportSqlite(newName); + return; + } + } + throw new FileFormatException("ZIP archive has to contain at least one .sqlite3 file"); + } finally { + if (File.Exists(newName)) File.Delete(newName); + } + } + + var oldName = Path.ChangeExtension(App.Config.DatabaseFile, ".old.sqlite3"); + File.Move(App.Config.DatabaseFile, oldName, true); + File.Move(filename, App.Config.DatabaseFile, false); + + using var cnx = await AppDbContext.ConnectAsync(); + await AppDbContext.ExecuteBatch(cnx, "VACUUM"); + } + + public static async Task ImportSql(StreamReader reader) { + var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3"); + File.Delete(newName); + try { + using (var cnx = await AppDbContext.ConnectAsync($"Data Source=\"{newName}\"; Mode=ReadWriteCreate; Foreign Keys=False; Cache=Default; Pooling=False")) { + await AppDbContext.ExecuteBatch(cnx, await reader.ReadToEndAsync()); + } + await ImportSqlite(newName); + } finally { + if (File.Exists(newName)) File.Delete(newName); + } + } + } +} diff --git a/Elwig/Helpers/Export/ElwigData.cs b/Elwig/Helpers/Export/ElwigData.cs index b59d319..a081f4e 100644 --- a/Elwig/Helpers/Export/ElwigData.cs +++ b/Elwig/Helpers/Export/ElwigData.cs @@ -405,29 +405,6 @@ namespace Elwig.Helpers.Export { }.Export(filename); } - public static async Task ImportDatabase(string filename) { - var oldName = Path.ChangeExtension(App.Config.DatabaseFile, ".old.sqlite3"); - var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3"); - try { - using (var zip = ZipFile.Open(filename, ZipArchiveMode.Read)) { - await zip.CheckIntegrity(); - var db = zip.GetEntry("database.sqlite3")!; - db.ExtractToFile(newName, true); - } - File.Move(App.Config.DatabaseFile, oldName, true); - File.Move(newName, App.Config.DatabaseFile, false); - } finally { - if (File.Exists(newName)) - File.Delete(newName); - } - } - - public static void ExportDatabase(string filename) { - File.Delete(filename); - using var zip = ZipFile.Open(filename, ZipArchiveMode.Create); - var db = zip.CreateEntryFromFile(App.Config.DatabaseFile, "database.sqlite3", CompressionLevel.SmallestSize); - } - public class ElwigExport { public (IEnumerable WbKgs, IEnumerable Filters)? WbKgs { get; set; } public (IEnumerable Members, IEnumerable Filters)? Members { get; set; } diff --git a/Elwig/Windows/MainWindow.xaml b/Elwig/Windows/MainWindow.xaml index 3835407..dd9ce9f 100644 --- a/Elwig/Windows/MainWindow.xaml +++ b/Elwig/Windows/MainWindow.xaml @@ -31,6 +31,17 @@ + + + + + + + + + + + diff --git a/Elwig/Windows/MainWindow.xaml.cs b/Elwig/Windows/MainWindow.xaml.cs index d048abb..fecc228 100644 --- a/Elwig/Windows/MainWindow.xaml.cs +++ b/Elwig/Windows/MainWindow.xaml.cs @@ -146,6 +146,48 @@ namespace Elwig.Windows { Mouse.OverrideCursor = null; } + private async void Menu_Database_Backup_Click(object sender, RoutedEventArgs evt) { + try { + var d = new SaveFileDialog() { + Title = "Datenbank sichern - Elwig", + FileName = $"database_{Utils.Today:yyyy-MM-dd}.sql.zip", + DefaultExt = "sql.zip", + Filter = "Komprimierte SQL-Datei (*.sql.zip)|*.sql.zip", + }; + if (d.ShowDialog() == true) { + Mouse.OverrideCursor = Cursors.Wait; + await Task.Run(async () => { + await Database.ExportSql(d.FileName, true); + }); + } + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + Mouse.OverrideCursor = null; + } + + private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) { + try { + var d = new OpenFileDialog() { + Title = "Datenbank wiederherstellen - Elwig", + DefaultExt = "sql.zip", + Filter = "SQLite-Datenbank (*.sqlite3, *.sqlite3.zip, *.sql, *.sql.zip)|*.sqlite3;*.sqlite3.zip;*.sql;*.sql.zip", + }; + if (d.ShowDialog() == true) { + var res = MessageBox.Show("Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?", "Datenbank wiederherstellen", + MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel); + if (res != MessageBoxResult.OK) + return; + + Mouse.OverrideCursor = Cursors.Wait; + await App.ReplaceDatabase(d.FileName); + } + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + Mouse.OverrideCursor = null; + } + private async void DownloadButton_Click(object sender, RoutedEventArgs evt) { if (App.Config.SyncUrl == null) return; @@ -243,21 +285,23 @@ namespace Elwig.Windows { await Task.Run(async () => { try { var data = await Utils.GetExportMetaData(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword); - var file = data + var files = data .Select(f => new { Name = f!["name"]!.AsValue().GetValue(), Timestamp = f!["modified"] != null && DateTime.TryParseExact(f!["modified"]!.AsValue().GetValue(), "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : (DateTime?)null, Url = f!["url"]!.AsValue().GetValue(), Size = f!["size"]!.AsValue().GetValue(), }) - .Where(f => f.Name == "database.sqlite3.zip") - .FirstOrDefault(); + .Where(f => f.Name.StartsWith("database.") && f.Name.EndsWith(".zip")) + .OrderBy(f => f.Size) + .ToList(); - if (file == null) { + if (files.Count == 0) { MessageBox.Show("Die Datenbank wurde noch nicht vom Hauptgerät hochgeladen!", "Datenbank herunterladen", MessageBoxButton.OK, MessageBoxImage.Error); return; } + var file = files[0]; var res = MessageBox.Show($"Es wurde eine komprimierte Datenbank (ca. {file.Size / 1024 / 1024} MB) vom {file.Timestamp:dd.MM.yyyy, HH:mm} gefunden.\n\nWollen Sie wirklich die aktuelle Datenbank unwiederruflich\nlöschen und durch die gefundene ersetzen?\n\nDas kann zu Datenverlust führen!", "Datenbank herunterladen", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel); @@ -301,8 +345,8 @@ namespace Elwig.Windows { Mouse.OverrideCursor = Cursors.Wait; await Task.Run(async () => { try { - var path = Path.Combine(App.TempPath, "database.sqlite3.zip"); - ElwigData.ExportDatabase(path); + var path = Path.Combine(App.TempPath, "database.sql.zip"); + await Database.ExportSql(path, true); await Utils.UploadExportData(path, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword); MessageBox.Show($"Hochladen der gesamten Datenbank erfolgreich!", "Datenbank hochladen", MessageBoxButton.OK, MessageBoxImage.Information); diff --git a/Elwig/Windows/QueryWindow.xaml.cs b/Elwig/Windows/QueryWindow.xaml.cs index e7a2b93..5bd2756 100644 --- a/Elwig/Windows/QueryWindow.xaml.cs +++ b/Elwig/Windows/QueryWindow.xaml.cs @@ -38,7 +38,7 @@ namespace Elwig.Windows { IList header; using (var cnx = await AppDbContext.ConnectAsync()) { - var cmd = cnx.CreateCommand(); + using var cmd = cnx.CreateCommand(); cmd.CommandText = sqlQuery; using var reader = await cmd.ExecuteReaderAsync(); header = await reader.GetColumnSchemaAsync();