1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2024-11-25 14:42:44 +01:00

Compare commits

...

3 Commits

10 changed files with 168 additions and 58 deletions

View File

@ -87,6 +87,9 @@ namespace DHT.Desktop.Main.Pages {
downloadStatisticsComputer.Recompute(); downloadStatisticsComputer.Recompute();
} }
} }
else if (e.PropertyName == nameof(DatabaseStatistics.TotalDownloads)) {
downloadStatisticsComputer.Recompute();
}
} }
private void EnqueueDownloadItems() { private void EnqueueDownloadItems() {

View File

@ -2,8 +2,10 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading;
using DHT.Desktop.Common; using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.Message; using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Dialogs.Progress; using DHT.Desktop.Dialogs.Progress;
@ -56,6 +58,10 @@ namespace DHT.Desktop.Main.Pages {
} }
} }
public void CloseDatabase() {
DatabaseClosed?.Invoke(this, EventArgs.Empty);
}
public async void MergeWithDatabase() { public async void MergeWithDatabase() {
var fileDialog = DatabaseGui.NewOpenDatabaseFileDialog(); var fileDialog = DatabaseGui.NewOpenDatabaseFileDialog();
fileDialog.Directory = Path.GetDirectoryName(Db.Path); fileDialog.Directory = Path.GetDirectoryName(Db.Path);
@ -74,10 +80,6 @@ namespace DHT.Desktop.Main.Pages {
await progressDialog.ShowDialog(window); await progressDialog.ShowDialog(window);
} }
public void CloseDatabase() {
DatabaseClosed?.Invoke(this, EventArgs.Empty);
}
private static async Task MergeWithDatabaseFromPaths(IDatabaseFile target, string[] paths, ProgressDialog dialog, IProgressCallback callback) { private static async Task MergeWithDatabaseFromPaths(IDatabaseFile target, string[] paths, ProgressDialog dialog, IProgressCallback callback) {
int total = paths.Length; int total = paths.Length;
@ -91,8 +93,28 @@ namespace DHT.Desktop.Main.Pages {
return DialogResult.YesNo.Yes == upgradeResult; return DialogResult.YesNo.Yes == upgradeResult;
} }
var oldStatistics = target.Statistics.Clone(); await PerformImport(target, paths, dialog, callback, "Database Merge", "Database Error", "database file", async path => {
var oldMessageCount = target.CountMessages(); SynchronizationContext? prevSyncContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(new AvaloniaSynchronizationContext());
IDatabaseFile? db = await DatabaseGui.TryOpenOrCreateDatabaseFromPath(path, dialog, CheckCanUpgradeDatabase);
SynchronizationContext.SetSynchronizationContext(prevSyncContext);
if (db == null) {
return false;
}
try {
target.AddFrom(db);
return true;
} finally {
db.Dispose();
}
});
}
private static async Task PerformImport(IDatabaseFile target, string[] paths, ProgressDialog dialog, IProgressCallback callback, string neutralDialogTitle, string errorDialogTitle, string itemName, Func<string, Task<bool>> performImport) {
int total = paths.Length;
var oldStatistics = target.SnapshotStatistics();
int successful = 0; int successful = 0;
int finished = 0; int finished = 0;
@ -102,56 +124,53 @@ namespace DHT.Desktop.Main.Pages {
++finished; ++finished;
if (!File.Exists(path)) { if (!File.Exists(path)) {
await Dialog.ShowOk(dialog, "Database Error", "Database '" + Path.GetFileName(path) + "' no longer exists."); await Dialog.ShowOk(dialog, errorDialogTitle, "File '" + Path.GetFileName(path) + "' no longer exists.");
continue;
}
IDatabaseFile? db = await DatabaseGui.TryOpenOrCreateDatabaseFromPath(path, dialog, CheckCanUpgradeDatabase);
if (db == null) {
continue; continue;
} }
try { try {
target.AddFrom(db); if (await performImport(path)) {
++successful;
}
} catch (Exception ex) { } catch (Exception ex) {
Log.Error(ex); Log.Error(ex);
await Dialog.ShowOk(dialog, "Database Error", "Database '" + Path.GetFileName(path) + "' could not be merged: " + ex.Message); await Dialog.ShowOk(dialog, errorDialogTitle, "File '" + Path.GetFileName(path) + "' could not be imported: " + ex.Message);
continue;
} finally {
db.Dispose();
} }
++successful;
} }
await callback.Update("Done", finished, total); await callback.Update("Done", finished, total);
if (successful == 0) { if (successful == 0) {
await Dialog.ShowOk(dialog, "Database Merge", "Nothing was merged."); await Dialog.ShowOk(dialog, neutralDialogTitle, "Nothing was imported.");
return; return;
} }
var newStatistics = target.Statistics; await Dialog.ShowOk(dialog, neutralDialogTitle, GetImportDialogMessage(oldStatistics, target.SnapshotStatistics(), successful, total, itemName));
}
private static string GetImportDialogMessage(DatabaseStatisticsSnapshot oldStatistics, DatabaseStatisticsSnapshot newStatistics, int successfulItems, int totalItems, string itemName) {
long newServers = newStatistics.TotalServers - oldStatistics.TotalServers; long newServers = newStatistics.TotalServers - oldStatistics.TotalServers;
long newChannels = newStatistics.TotalChannels - oldStatistics.TotalChannels; long newChannels = newStatistics.TotalChannels - oldStatistics.TotalChannels;
long newMessages = target.CountMessages() - oldMessageCount; long newUsers = newStatistics.TotalUsers - oldStatistics.TotalUsers;
long newMessages = newStatistics.TotalMessages - oldStatistics.TotalMessages;
StringBuilder message = new StringBuilder(); StringBuilder message = new StringBuilder();
message.Append("Processed "); message.Append("Processed ");
if (successful == total) { if (successfulItems == totalItems) {
message.Append(successful.Pluralize("database file")); message.Append(successfulItems.Pluralize(itemName));
} }
else { else {
message.Append(successful.Format()).Append(" out of ").Append(total.Pluralize("database file")); message.Append(successfulItems.Format()).Append(" out of ").Append(totalItems.Pluralize(itemName));
} }
message.Append(" and added:\n\n \u2022 "); message.Append(" and added:\n\n \u2022 ");
message.Append(newServers.Pluralize("server")).Append("\n \u2022 "); message.Append(newServers.Pluralize("server")).Append("\n \u2022 ");
message.Append(newChannels.Pluralize("channel")).Append("\n \u2022 "); message.Append(newChannels.Pluralize("channel")).Append("\n \u2022 ");
message.Append(newUsers.Pluralize("user")).Append("\n \u2022 ");
message.Append(newMessages.Pluralize("message")); message.Append(newMessages.Pluralize("message"));
await Dialog.ShowOk(dialog, "Database Merge", message.ToString()); return message.ToString();
} }
} }
} }

View File

@ -3,10 +3,6 @@ using System.Net;
namespace DHT.Server.Data { namespace DHT.Server.Data {
public readonly struct Download { public readonly struct Download {
internal static Download NewEnqueued(string url, ulong size) {
return new Download(url, DownloadStatus.Enqueued, size);
}
internal static Download NewSuccess(string url, byte[] data) { internal static Download NewSuccess(string url, byte[] data) {
return new Download(url, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data); return new Download(url, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data);
} }
@ -20,11 +16,15 @@ namespace DHT.Server.Data {
public ulong Size { get; } public ulong Size { get; }
public byte[]? Data { get; } public byte[]? Data { get; }
private Download(string url, DownloadStatus status, ulong size, byte[]? data = null) { internal Download(string url, DownloadStatus status, ulong size, byte[]? data = null) {
Url = url; Url = url;
Status = status; Status = status;
Size = size; Size = size;
Data = data; Data = data;
} }
internal Download WithData(byte[] data) {
return new Download(Url, Status, Size, data);
}
} }
} }

View File

@ -1,3 +1,5 @@
using DHT.Server.Data;
namespace DHT.Server.Database { namespace DHT.Server.Database {
public static class DatabaseExtensions { public static class DatabaseExtensions {
public static void AddFrom(this IDatabaseFile target, IDatabaseFile source) { public static void AddFrom(this IDatabaseFile target, IDatabaseFile source) {
@ -11,6 +13,10 @@ namespace DHT.Server.Database {
target.AddUsers(source.GetAllUsers().ToArray()); target.AddUsers(source.GetAllUsers().ToArray());
target.AddMessages(source.GetMessages().ToArray()); target.AddMessages(source.GetMessages().ToArray());
foreach (var download in source.GetDownloadsWithoutData()) {
target.AddDownload(download.Status == DownloadStatus.Success ? source.GetDownloadWithData(download) : download);
}
} }
} }
} }

View File

@ -1,12 +1,17 @@
using DHT.Utils.Models; using DHT.Utils.Models;
namespace DHT.Server.Database { namespace DHT.Server.Database {
/// <summary>
/// A live view of database statistics.
/// Some of the totals are computed asynchronously and may not reflect the most recent version of the database, or may not be available at all until computed for the first time.
/// </summary>
public sealed class DatabaseStatistics : BaseModel { public sealed class DatabaseStatistics : BaseModel {
private long totalServers; private long totalServers;
private long totalChannels; private long totalChannels;
private long totalUsers; private long totalUsers;
private long? totalMessages; private long? totalMessages;
private long? totalAttachments; private long? totalAttachments;
private long? totalDownloads;
public long TotalServers { public long TotalServers {
get => totalServers; get => totalServers;
@ -33,14 +38,9 @@ namespace DHT.Server.Database {
internal set => Change(ref totalAttachments, value); internal set => Change(ref totalAttachments, value);
} }
public DatabaseStatistics Clone() { public long? TotalDownloads {
return new DatabaseStatistics { get => totalDownloads;
totalServers = totalServers, internal set => Change(ref totalDownloads, value);
totalChannels = totalChannels,
totalUsers = TotalUsers,
totalMessages = totalMessages,
totalAttachments = totalAttachments
};
} }
} }
} }

View File

@ -0,0 +1,11 @@
namespace DHT.Server.Database {
/// <summary>
/// A complete snapshot of database statistics at a particular point in time.
/// </summary>
public readonly struct DatabaseStatisticsSnapshot {
public long TotalServers { get; internal init; }
public long TotalChannels { get; internal init; }
public long TotalUsers { get; internal init; }
public long TotalMessages { get; internal init; }
}
}

View File

@ -13,6 +13,10 @@ namespace DHT.Server.Database {
private DummyDatabaseFile() {} private DummyDatabaseFile() {}
public DatabaseStatisticsSnapshot SnapshotStatistics() {
return new();
}
public void AddServer(Data.Server server) {} public void AddServer(Data.Server server) {}
public List<Data.Server> GetAllServers() { public List<Data.Server> GetAllServers() {
@ -47,7 +51,15 @@ namespace DHT.Server.Database {
return new(); return new();
} }
public void AddDownloads(IEnumerable<Data.Download> downloads) {} public List<Data.Download> GetDownloadsWithoutData() {
return new();
}
public Data.Download GetDownloadWithData(Data.Download download) {
return download;
}
public void AddDownload(Data.Download download) {}
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {} public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}

View File

@ -9,6 +9,7 @@ namespace DHT.Server.Database {
public interface IDatabaseFile : IDisposable { public interface IDatabaseFile : IDisposable {
string Path { get; } string Path { get; }
DatabaseStatistics Statistics { get; } DatabaseStatistics Statistics { get; }
DatabaseStatisticsSnapshot SnapshotStatistics();
void AddServer(Data.Server server); void AddServer(Data.Server server);
List<Data.Server> GetAllServers(); List<Data.Server> GetAllServers();
@ -26,7 +27,10 @@ namespace DHT.Server.Database {
int CountAttachments(AttachmentFilter? filter = null); int CountAttachments(AttachmentFilter? filter = null);
void AddDownloads(IEnumerable<Data.Download> downloads); void AddDownload(Data.Download download);
List<Data.Download> GetDownloadsWithoutData();
Data.Download GetDownloadWithData(Data.Download download);
void EnqueueDownloadItems(AttachmentFilter? filter = null); void EnqueueDownloadItems(AttachmentFilter? filter = null);
List<DownloadItem> GetEnqueuedDownloadItems(int count); List<DownloadItem> GetEnqueuedDownloadItems(int count);
void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode); void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode);

View File

@ -24,15 +24,20 @@ namespace DHT.Server.Database.Sqlite {
}; };
var pool = new SqliteConnectionPool(connectionString, DefaultPoolSize); var pool = new SqliteConnectionPool(connectionString, DefaultPoolSize);
bool wasOpened;
using (var conn = pool.Take()) { using (var conn = pool.Take()) {
if (!await new Schema(conn).Setup(checkCanUpgradeSchemas)) { wasOpened = await new Schema(conn).Setup(checkCanUpgradeSchemas);
return null;
}
} }
if (wasOpened) {
return new SqliteDatabaseFile(path, pool); return new SqliteDatabaseFile(path, pool);
} }
else {
pool.Dispose();
return null;
}
}
public string Path { get; } public string Path { get; }
public DatabaseStatistics Statistics { get; } public DatabaseStatistics Statistics { get; }
@ -41,6 +46,7 @@ namespace DHT.Server.Database.Sqlite {
private readonly SqliteConnectionPool pool; private readonly SqliteConnectionPool pool;
private readonly AsyncValueComputer<long>.Single totalMessagesComputer; private readonly AsyncValueComputer<long>.Single totalMessagesComputer;
private readonly AsyncValueComputer<long>.Single totalAttachmentsComputer; private readonly AsyncValueComputer<long>.Single totalAttachmentsComputer;
private readonly AsyncValueComputer<long>.Single totalDownloadsComputer;
private SqliteDatabaseFile(string path, SqliteConnectionPool pool) { private SqliteDatabaseFile(string path, SqliteConnectionPool pool) {
this.log = Log.ForType(typeof(SqliteDatabaseFile), System.IO.Path.GetFileName(path)); this.log = Log.ForType(typeof(SqliteDatabaseFile), System.IO.Path.GetFileName(path));
@ -48,6 +54,7 @@ namespace DHT.Server.Database.Sqlite {
this.totalMessagesComputer = AsyncValueComputer<long>.WithResultProcessor(UpdateMessageStatistics).WithOutdatedResults().BuildWithComputer(ComputeMessageStatistics); this.totalMessagesComputer = AsyncValueComputer<long>.WithResultProcessor(UpdateMessageStatistics).WithOutdatedResults().BuildWithComputer(ComputeMessageStatistics);
this.totalAttachmentsComputer = AsyncValueComputer<long>.WithResultProcessor(UpdateAttachmentStatistics).WithOutdatedResults().BuildWithComputer(ComputeAttachmentStatistics); this.totalAttachmentsComputer = AsyncValueComputer<long>.WithResultProcessor(UpdateAttachmentStatistics).WithOutdatedResults().BuildWithComputer(ComputeAttachmentStatistics);
this.totalDownloadsComputer = AsyncValueComputer<long>.WithResultProcessor(UpdateDownloadStatistics).WithOutdatedResults().BuildWithComputer(ComputeDownloadStatistics);
this.Path = path; this.Path = path;
this.Statistics = new DatabaseStatistics(); this.Statistics = new DatabaseStatistics();
@ -60,12 +67,22 @@ namespace DHT.Server.Database.Sqlite {
totalMessagesComputer.Recompute(); totalMessagesComputer.Recompute();
totalAttachmentsComputer.Recompute(); totalAttachmentsComputer.Recompute();
totalDownloadsComputer.Recompute();
} }
public void Dispose() { public void Dispose() {
pool.Dispose(); pool.Dispose();
} }
public DatabaseStatisticsSnapshot SnapshotStatistics() {
return new DatabaseStatisticsSnapshot {
TotalServers = Statistics.TotalServers,
TotalChannels = Statistics.TotalChannels,
TotalUsers = Statistics.TotalUsers,
TotalMessages = ComputeMessageStatistics()
};
}
public void AddServer(Data.Server server) { public void AddServer(Data.Server server) {
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Upsert("servers", new[] { using var cmd = conn.Upsert("servers", new[] {
@ -390,10 +407,8 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
return reader.Read() ? reader.GetInt32(0) : 0; return reader.Read() ? reader.GetInt32(0) : 0;
} }
public void AddDownloads(IEnumerable<Data.Download> downloads) { public void AddDownload(Data.Download download) {
using var conn = pool.Take(); using var conn = pool.Take();
using var tx = conn.BeginTransaction();
using var cmd = conn.Upsert("downloads", new[] { using var cmd = conn.Upsert("downloads", new[] {
("url", SqliteType.Text), ("url", SqliteType.Text),
("status", SqliteType.Integer), ("status", SqliteType.Integer),
@ -401,15 +416,46 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
("blob", SqliteType.Blob) ("blob", SqliteType.Blob)
}); });
foreach (var download in downloads) {
cmd.Set(":url", download.Url); cmd.Set(":url", download.Url);
cmd.Set(":status", (int) download.Status); cmd.Set(":status", (int) download.Status);
cmd.Set(":size", download.Size); cmd.Set(":size", download.Size);
cmd.Set(":blob", download.Data); cmd.Set(":blob", download.Data);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
totalDownloadsComputer.Recompute();
} }
tx.Commit(); public List<Data.Download> GetDownloadsWithoutData() {
var list = new List<Data.Download>();
using var conn = pool.Take();
using var cmd = conn.Command("SELECT url, status, size FROM downloads");
using var reader = cmd.ExecuteReader();
while (reader.Read()) {
string url = reader.GetString(0);
var status = (DownloadStatus) reader.GetInt32(1);
ulong size = reader.GetUint64(2);
list.Add(new Data.Download(url, status, size));
}
return list;
}
public Data.Download GetDownloadWithData(Data.Download download) {
using var conn = pool.Take();
using var cmd = conn.Command("SELECT blob FROM downloads WHERE url = :url");
cmd.AddAndSet(":url", SqliteType.Text, download.Url);
using var reader = cmd.ExecuteReader();
if (reader.Read() && !reader.IsDBNull(0)) {
return download.WithData((byte[]) reader["blob"]);
}
else {
return download;
}
} }
public void EnqueueDownloadItems(AttachmentFilter? filter = null) { public void EnqueueDownloadItems(AttachmentFilter? filter = null) {
@ -599,5 +645,14 @@ FROM downloads");
private void UpdateAttachmentStatistics(long totalAttachments) { private void UpdateAttachmentStatistics(long totalAttachments) {
Statistics.TotalAttachments = totalAttachments; Statistics.TotalAttachments = totalAttachments;
} }
private long ComputeDownloadStatistics() {
using var conn = pool.Take();
return conn.SelectScalar("SELECT COUNT(*) FROM downloads") as long? ?? 0L;
}
private void UpdateDownloadStatistics(long totalDownloads) {
Statistics.TotalDownloads = totalDownloads;
}
} }
} }

View File

@ -92,9 +92,9 @@ namespace DHT.Server.Download {
Log.Debug("Downloading " + url + "..."); Log.Debug("Downloading " + url + "...");
try { try {
db.AddDownloads(new [] { Data.Download.NewSuccess(url, client.DownloadData(url)) }); db.AddDownload(Data.Download.NewSuccess(url, client.DownloadData(url)));
} catch (WebException e) { } catch (WebException e) {
db.AddDownloads(new [] { Data.Download.NewFailure(url, e.Response is HttpWebResponse response ? response.StatusCode : null, item.Size) }); db.AddDownload(Data.Download.NewFailure(url, e.Response is HttpWebResponse response ? response.StatusCode : null, item.Size));
Log.Error(e); Log.Error(e);
} finally { } finally {
parameters.FireOnItemFinished(item); parameters.FireOnItemFinished(item);