Add feature to merge multiple database files into one

This commit is contained in:
chylex 2021-11-27 15:25:39 +01:00
parent 879a69608c
commit e9e8c95a19
No known key found for this signature in database
GPG Key ID: 4DE42C8F19A80548
7 changed files with 200 additions and 31 deletions

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using DHT.Desktop.Dialogs;
using DHT.Server.Database;
using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite;
using DHT.Server.Logging;
namespace DHT.Desktop.Common {
public static class DatabaseGui {
private const string DatabaseFileInitialName = "archive.dht";
private static readonly List<FileDialogFilter> DatabaseFileDialogFilter = new() {
new FileDialogFilter {
Name = "Discord History Tracker Database",
Extensions = { "dht" }
}
};
public static OpenFileDialog NewOpenDatabaseFileDialog() {
return new OpenFileDialog {
Title = "Open Database File",
InitialFileName = DatabaseFileInitialName,
Filters = DatabaseFileDialogFilter
};
}
public static SaveFileDialog NewOpenOrCreateDatabaseFileDialog() {
return new SaveFileDialog {
Title = "Open or Create Database File",
InitialFileName = DatabaseFileInitialName,
Filters = DatabaseFileDialogFilter
};
}
public static async Task<IDatabaseFile?> TryOpenOrCreateDatabaseFromPath(string path, Window window, Func<Task<bool>> checkCanUpgradeDatabase) {
IDatabaseFile? file = null;
try {
file = await SqliteDatabaseFile.OpenOrCreate(path, checkCanUpgradeDatabase);
} catch (InvalidDatabaseVersionException ex) {
await Dialog.ShowOk(window, "Database Error", "Database '" + Path.GetFileName(path) + "' appears to be corrupted (invalid version: " + ex.Version + ").");
} catch (DatabaseTooNewException ex) {
await Dialog.ShowOk(window, "Database Error", "Database '" + Path.GetFileName(path) + "' was opened in a newer version of DHT (database version " + ex.DatabaseVersion + ", app version " + ex.CurrentVersion + ").");
} catch (Exception ex) {
Log.Error(ex);
await Dialog.ShowOk(window, "Database Error", "Database '" + Path.GetFileName(path) + "' could not be opened:" + ex.Message);
}
return file;
}
public static async Task<DialogResult.YesNo> ShowCanUpgradeDatabaseDialog(Window window) {
return await Dialog.ShowYesNo(window, "Database Upgrade", "This database was created with an older version of DHT. If you proceed, the database will be upgraded and will no longer open in previous versions of DHT. Do you want to proceed with the upgrade?");
}
public static async Task<DialogResult.YesNo> ShowCanUpgradeMultipleDatabaseDialog(Window window) {
return await Dialog.ShowYesNo(window, "Database Upgrade", "One or more databases were created with an older version of DHT. If you proceed, these databases will be upgraded and will no longer open in previous versions of DHT. Otherwise, these databases will be skipped. Do you want to proceed with the upgrade?");
}
}
}

View File

@ -15,7 +15,10 @@
<Button Command="{Binding CloseDatabase}" DockPanel.Dock="Right">Close Database</Button>
<TextBox Text="{Binding Db.Path}" Width="NaN" Margin="0 0 10 0" IsEnabled="True" />
</DockPanel>
<Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button>
<StackPanel Spacing="10" Orientation="Horizontal">
<Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button>
<Button Command="{Binding MergeWithDatabase}">Merge with Database(s)...</Button>
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -1,10 +1,14 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs;
using DHT.Desktop.Models;
using DHT.Server.Database;
using DHT.Server.Logging;
using DHT.Server.Service;
namespace DHT.Desktop.Main.Pages {
@ -50,9 +54,105 @@ namespace DHT.Desktop.Main.Pages {
}
}
public async void MergeWithDatabase() {
var fileDialog = DatabaseGui.NewOpenDatabaseFileDialog();
fileDialog.Directory = Path.GetDirectoryName(Db.Path);
fileDialog.AllowMultiple = true;
string[] paths = await fileDialog.ShowAsync(window);
if (paths == null || paths.Length == 0) {
return;
}
ProgressDialog progressDialog = new ProgressDialog();
progressDialog.DataContext = new ProgressDialogModel(async callback => await MergeWithDatabaseFromPaths(Db, paths, progressDialog, callback)) {
Title = "Database Merge"
};
await progressDialog.ShowDialog(window);
}
public void CloseDatabase() {
ServerLauncher.Stop();
DatabaseClosed?.Invoke(this, EventArgs.Empty);
}
private static async Task MergeWithDatabaseFromPaths(IDatabaseFile target, string[] paths, ProgressDialog dialog, IProgressCallback callback) {
int total = paths.Length;
DialogResult.YesNo? upgradeResult = null;
async Task<bool> CheckCanUpgradeDatabase() {
upgradeResult ??= total > 1
? await DatabaseGui.ShowCanUpgradeMultipleDatabaseDialog(dialog)
: await DatabaseGui.ShowCanUpgradeDatabaseDialog(dialog);
return DialogResult.YesNo.Yes == upgradeResult;
}
var oldStatistics = target.Statistics.Clone();
int successful = 0;
int finished = 0;
foreach (string path in paths) {
await callback.Update(Path.GetFileName(path), finished, total);
++finished;
if (!File.Exists(path)) {
await Dialog.ShowOk(dialog, "Database Error", "Database '" + Path.GetFileName(path) + "' no longer exists.");
continue;
}
IDatabaseFile? db = await DatabaseGui.TryOpenOrCreateDatabaseFromPath(path, dialog, CheckCanUpgradeDatabase);
if (db == null) {
continue;
}
try {
target.AddFrom(db);
} catch (Exception ex) {
Log.Error(ex);
await Dialog.ShowOk(dialog, "Database Error", "Database '" + Path.GetFileName(path) + "' could not be merged: " + ex.Message);
continue;
} finally {
db.Dispose();
}
++successful;
}
await callback.Update("Done", finished, total);
if (successful == 0) {
await Dialog.ShowOk(dialog, "Database Merge", "Nothing was merged.");
return;
}
var newStatistics = target.Statistics;
long newServers = newStatistics.TotalServers - oldStatistics.TotalServers;
long newChannels = newStatistics.TotalChannels - oldStatistics.TotalChannels;
long newMessages = newStatistics.TotalMessages - oldStatistics.TotalMessages;
string Pluralize(long count, string text) {
return count + "\u00A0" + (count == 1 ? text : text + "s");
}
StringBuilder message = new StringBuilder();
message.Append("Processed ");
if (successful == total) {
message.Append(Pluralize(successful, "database file"));
}
else {
message.Append(successful).Append(" out of ").Append(Pluralize(total, "database file"));
}
message.Append(" and added:\n\n \u2022 ");
message.Append(Pluralize(newServers, "server")).Append("\n \u2022 ");
message.Append(Pluralize(newChannels, "channel")).Append("\n \u2022 ");
message.Append(Pluralize(newMessages, "message"));
await Dialog.ShowOk(dialog, "Database Merge", message.ToString());
}
}
}

View File

@ -1,14 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs;
using DHT.Desktop.Models;
using DHT.Server.Database;
using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite;
using DHT.Server.Logging;
namespace DHT.Desktop.Main {
public class WelcomeScreenModel : BaseModel {
@ -29,19 +26,10 @@ namespace DHT.Desktop.Main {
}
public async void OpenOrCreateDatabase() {
var dialog = new SaveFileDialog {
Title = "Open or Create Database File",
InitialFileName = "archive.dht",
Directory = Path.GetDirectoryName(dbFilePath),
Filters = new List<FileDialogFilter> {
new() {
Name = "Discord History Tracker Database",
Extensions = { "dht" }
}
}
}.ShowAsync(window);
var dialog = DatabaseGui.NewOpenOrCreateDatabaseFileDialog();
dialog.Directory = Path.GetDirectoryName(dbFilePath);
string path = await dialog;
string path = await dialog.ShowAsync(window);
if (!string.IsNullOrWhiteSpace(path)) {
await OpenOrCreateDatabaseFromPath(path);
}
@ -53,24 +41,14 @@ namespace DHT.Desktop.Main {
}
dbFilePath = path;
try {
Db = await SqliteDatabaseFile.OpenOrCreate(path, CheckCanUpgradeDatabase);
} catch (InvalidDatabaseVersionException ex) {
await Dialog.ShowOk(window, "Database Error", "This database appears to be corrupted (invalid version: " + ex.Version + ").");
} catch (DatabaseTooNewException ex) {
await Dialog.ShowOk(window, "Database Error", "This database was opened in a newer version of DHT (database version " + ex.DatabaseVersion + ", app version " + ex.CurrentVersion + ").");
} catch (Exception ex) {
Log.Error(ex);
await Dialog.ShowOk(window, "Database Error", ex.Message);
}
Db = await DatabaseGui.TryOpenOrCreateDatabaseFromPath(path, window, CheckCanUpgradeDatabase);
OnPropertyChanged(nameof(Db));
OnPropertyChanged(nameof(HasDatabase));
}
private async Task<bool> CheckCanUpgradeDatabase() {
return DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Database Upgrade", "This database was created with an older version of DHT. If you proceed, the database will be upgraded and will no longer open in previous versions of DHT. Do you want to proceed?");
return DialogResult.YesNo.Yes == await DatabaseGui.ShowCanUpgradeDatabaseDialog(window);
}
public void CloseDatabase() {
@ -81,7 +59,7 @@ namespace DHT.Desktop.Main {
}
public async void ShowAboutDialog() {
await new AboutWindow() { DataContext = new AboutWindowModel() }.ShowDialog(this.window);
await new AboutWindow { DataContext = new AboutWindowModel() }.ShowDialog(this.window);
}
public void Exit() {

View File

@ -0,0 +1,16 @@
namespace DHT.Server.Database {
public static class DatabaseExtensions {
public static void AddFrom(this IDatabaseFile target, IDatabaseFile source) {
foreach (var server in source.GetAllServers()) {
target.AddServer(server);
}
foreach (var channel in source.GetAllChannels()) {
target.AddChannel(channel);
}
target.AddUsers(source.GetAllUsers().ToArray());
target.AddMessages(source.GetMessages().ToArray());
}
}
}

View File

@ -28,5 +28,13 @@ namespace DHT.Server.Database {
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public DatabaseStatistics Clone() {
return new DatabaseStatistics {
totalServers = totalServers,
totalChannels = totalChannels,
totalMessages = totalMessages
};
}
}
}

View File

@ -80,7 +80,7 @@ namespace DHT.Server.Database.Sqlite {
Execute(@"CREATE TABLE attachments (
message_id INTEGER NOT NULL,
attachment_id INTEGER NOT NULL PRIMARY KEY NOT NULL,
attachment_id INTEGER NOT NULL PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
type TEXT,
url TEXT NOT NULL,