mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-09-14 17:32:08 +02:00
Compare commits
11 Commits
v39.1
...
031d521402
Author | SHA1 | Date | |
---|---|---|---|
031d521402
|
|||
0131f8cb50
|
|||
3bf5acfa65
|
|||
f603c861c5
|
|||
d2934f4d6a
|
|||
567253d147
|
|||
aa6555990c
|
|||
3d9d6a454a
|
|||
ee39780928
|
|||
7b58f973a0
|
|||
93fe018343
|
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
@@ -13,7 +12,7 @@ sealed class App : Application {
|
|||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted() {
|
public override void OnFrameworkInitializationCompleted() {
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||||
desktop.MainWindow = new MainWindow(new Arguments(desktop.Args ?? Array.Empty<string>()));
|
desktop.MainWindow = new MainWindow(Program.Arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
@@ -6,25 +6,32 @@ namespace DHT.Desktop;
|
|||||||
sealed class Arguments {
|
sealed class Arguments {
|
||||||
private static readonly Log Log = Log.ForType<Arguments>();
|
private static readonly Log Log = Log.ForType<Arguments>();
|
||||||
|
|
||||||
|
private const int FirstArgument = 1;
|
||||||
|
|
||||||
public static Arguments Empty => new(Array.Empty<string>());
|
public static Arguments Empty => new(Array.Empty<string>());
|
||||||
|
|
||||||
|
public bool Console { get; }
|
||||||
public string? DatabaseFile { get; }
|
public string? DatabaseFile { get; }
|
||||||
public ushort? ServerPort { get; }
|
public ushort? ServerPort { get; }
|
||||||
public string? ServerToken { get; }
|
public string? ServerToken { get; }
|
||||||
|
|
||||||
public Arguments(string[] args) {
|
public Arguments(string[] args) {
|
||||||
for (int i = 0; i < args.Length; i++) {
|
for (int i = FirstArgument; i < args.Length; i++) {
|
||||||
string key = args[i];
|
string key = args[i];
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "-debug":
|
case "-debug":
|
||||||
Log.IsDebugEnabled = true;
|
Log.IsDebugEnabled = true;
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
case "-console":
|
||||||
|
Console = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string value;
|
string value;
|
||||||
|
|
||||||
if (i == 0 && !key.StartsWith('-')) {
|
if (i == FirstArgument && !key.StartsWith('-')) {
|
||||||
value = key;
|
value = key;
|
||||||
key = "-db";
|
key = "-db";
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.6" Condition=" '$(Configuration)' == 'Debug' " />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.6" Condition=" '$(Configuration)' == 'Debug' " />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
|
||||||
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.6" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using DHT.Desktop.Common;
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Data.Filters;
|
using DHT.Server.Data.Filters;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
@@ -47,7 +48,7 @@ sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
|
|||||||
|
|
||||||
public IEnumerable<Unit> Units => AllUnits;
|
public IEnumerable<Unit> Units => AllUnits;
|
||||||
|
|
||||||
private readonly IDatabaseFile db;
|
private readonly State state;
|
||||||
private readonly string verb;
|
private readonly string verb;
|
||||||
|
|
||||||
private readonly AsyncValueComputer<long> matchingAttachmentCountComputer;
|
private readonly AsyncValueComputer<long> matchingAttachmentCountComputer;
|
||||||
@@ -55,10 +56,10 @@ sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
|
|||||||
private long? totalAttachmentCount;
|
private long? totalAttachmentCount;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public AttachmentFilterPanelModel() : this(DummyDatabaseFile.Instance) {}
|
public AttachmentFilterPanelModel() : this(State.Dummy) {}
|
||||||
|
|
||||||
public AttachmentFilterPanelModel(IDatabaseFile db, string verb = "Matches") {
|
public AttachmentFilterPanelModel(State state, string verb = "Matches") {
|
||||||
this.db = db;
|
this.state = state;
|
||||||
this.verb = verb;
|
this.verb = verb;
|
||||||
|
|
||||||
this.matchingAttachmentCountComputer = AsyncValueComputer<long>.WithResultProcessor(SetAttachmentCounts).Build();
|
this.matchingAttachmentCountComputer = AsyncValueComputer<long>.WithResultProcessor(SetAttachmentCounts).Build();
|
||||||
@@ -66,11 +67,11 @@ sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
|
|||||||
UpdateFilterStatistics();
|
UpdateFilterStatistics();
|
||||||
|
|
||||||
PropertyChanged += OnPropertyChanged;
|
PropertyChanged += OnPropertyChanged;
|
||||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
state.Db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
state.Db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
@@ -81,7 +82,7 @@ sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
|
|||||||
|
|
||||||
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
if (e.PropertyName == nameof(DatabaseStatistics.TotalAttachments)) {
|
if (e.PropertyName == nameof(DatabaseStatistics.TotalAttachments)) {
|
||||||
totalAttachmentCount = db.Statistics.TotalAttachments;
|
totalAttachmentCount = state.Db.Statistics.TotalAttachments;
|
||||||
UpdateFilterStatistics();
|
UpdateFilterStatistics();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ sealed class AttachmentFilterPanelModel : BaseModel, IDisposable {
|
|||||||
else {
|
else {
|
||||||
matchingAttachmentCount = null;
|
matchingAttachmentCount = null;
|
||||||
UpdateFilterStatisticsText();
|
UpdateFilterStatisticsText();
|
||||||
matchingAttachmentCountComputer.Compute(() => db.CountAttachments(filter));
|
matchingAttachmentCountComputer.Compute(() => state.Db.CountAttachments(filter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ using Avalonia.Controls;
|
|||||||
using DHT.Desktop.Common;
|
using DHT.Desktop.Common;
|
||||||
using DHT.Desktop.Dialogs.CheckBox;
|
using DHT.Desktop.Dialogs.CheckBox;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
using DHT.Server.Data.Filters;
|
using DHT.Server.Data.Filters;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
@@ -62,7 +63,7 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HashSet<ulong> IncludedChannels {
|
public HashSet<ulong> IncludedChannels {
|
||||||
get => includedChannels ?? db.GetAllChannels().Select(static channel => channel.Id).ToHashSet();
|
get => includedChannels ?? state.Db.GetAllChannels().Select(static channel => channel.Id).ToHashSet();
|
||||||
set => Change(ref includedChannels, value);
|
set => Change(ref includedChannels, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HashSet<ulong> IncludedUsers {
|
public HashSet<ulong> IncludedUsers {
|
||||||
get => includedUsers ?? db.GetAllUsers().Select(static user => user.Id).ToHashSet();
|
get => includedUsers ?? state.Db.GetAllUsers().Select(static user => user.Id).ToHashSet();
|
||||||
set => Change(ref includedUsers, value);
|
set => Change(ref includedUsers, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
private readonly IDatabaseFile db;
|
private readonly State state;
|
||||||
private readonly string verb;
|
private readonly string verb;
|
||||||
|
|
||||||
private readonly AsyncValueComputer<long> exportedMessageCountComputer;
|
private readonly AsyncValueComputer<long> exportedMessageCountComputer;
|
||||||
@@ -99,11 +100,11 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
private long? totalMessageCount;
|
private long? totalMessageCount;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public MessageFilterPanelModel() : this(null!, DummyDatabaseFile.Instance) {}
|
public MessageFilterPanelModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public MessageFilterPanelModel(Window window, IDatabaseFile db, string verb = "Matches") {
|
public MessageFilterPanelModel(Window window, State state, string verb = "Matches") {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.db = db;
|
this.state = state;
|
||||||
this.verb = verb;
|
this.verb = verb;
|
||||||
|
|
||||||
this.exportedMessageCountComputer = AsyncValueComputer<long>.WithResultProcessor(SetExportedMessageCount).Build();
|
this.exportedMessageCountComputer = AsyncValueComputer<long>.WithResultProcessor(SetExportedMessageCount).Build();
|
||||||
@@ -113,11 +114,11 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
UpdateUserFilterLabel();
|
UpdateUserFilterLabel();
|
||||||
|
|
||||||
PropertyChanged += OnPropertyChanged;
|
PropertyChanged += OnPropertyChanged;
|
||||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
state.Db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
state.Db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
@@ -136,7 +137,7 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
|
|
||||||
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
if (e.PropertyName == nameof(DatabaseStatistics.TotalMessages)) {
|
if (e.PropertyName == nameof(DatabaseStatistics.TotalMessages)) {
|
||||||
totalMessageCount = db.Statistics.TotalMessages;
|
totalMessageCount = state.Db.Statistics.TotalMessages;
|
||||||
UpdateFilterStatistics();
|
UpdateFilterStatistics();
|
||||||
}
|
}
|
||||||
else if (e.PropertyName == nameof(DatabaseStatistics.TotalChannels)) {
|
else if (e.PropertyName == nameof(DatabaseStatistics.TotalChannels)) {
|
||||||
@@ -157,7 +158,7 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
else {
|
else {
|
||||||
exportedMessageCount = null;
|
exportedMessageCount = null;
|
||||||
UpdateFilterStatisticsText();
|
UpdateFilterStatisticsText();
|
||||||
exportedMessageCountComputer.Compute(() => db.CountMessages(filter));
|
exportedMessageCountComputer.Compute(() => state.Db.CountMessages(filter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,11 +176,11 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenChannelFilterDialog() {
|
public async void OpenChannelFilterDialog() {
|
||||||
var servers = db.GetAllServers().ToDictionary(static server => server.Id);
|
var servers = state.Db.GetAllServers().ToDictionary(static server => server.Id);
|
||||||
var items = new List<CheckBoxItem<ulong>>();
|
var items = new List<CheckBoxItem<ulong>>();
|
||||||
var included = IncludedChannels;
|
var included = IncludedChannels;
|
||||||
|
|
||||||
foreach (var channel in db.GetAllChannels()) {
|
foreach (var channel in state.Db.GetAllChannels()) {
|
||||||
var channelId = channel.Id;
|
var channelId = channel.Id;
|
||||||
var channelName = channel.Name;
|
var channelName = channel.Name;
|
||||||
|
|
||||||
@@ -223,7 +224,7 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
var items = new List<CheckBoxItem<ulong>>();
|
var items = new List<CheckBoxItem<ulong>>();
|
||||||
var included = IncludedUsers;
|
var included = IncludedUsers;
|
||||||
|
|
||||||
foreach (var user in db.GetAllUsers()) {
|
foreach (var user in state.Db.GetAllUsers()) {
|
||||||
var name = user.Name;
|
var name = user.Name;
|
||||||
var discriminator = user.Discriminator;
|
var discriminator = user.Discriminator;
|
||||||
|
|
||||||
@@ -240,13 +241,13 @@ sealed class MessageFilterPanelModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateChannelFilterLabel() {
|
private void UpdateChannelFilterLabel() {
|
||||||
long total = db.Statistics.TotalChannels;
|
long total = state.Db.Statistics.TotalChannels;
|
||||||
long included = FilterByChannel ? IncludedChannels.Count : total;
|
long included = FilterByChannel ? IncludedChannels.Count : total;
|
||||||
ChannelFilterLabel = "Selected " + included.Format() + " / " + total.Pluralize("channel") + ".";
|
ChannelFilterLabel = "Selected " + included.Format() + " / " + total.Pluralize("channel") + ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateUserFilterLabel() {
|
private void UpdateUserFilterLabel() {
|
||||||
long total = db.Statistics.TotalUsers;
|
long total = state.Db.Statistics.TotalUsers;
|
||||||
long included = FilterByUser ? IncludedUsers.Count : total;
|
long included = FilterByUser ? IncludedUsers.Count : total;
|
||||||
UserFilterLabel = "Selected " + included.Format() + " / " + total.Pluralize("user") + ".";
|
UserFilterLabel = "Selected " + included.Format() + " / " + total.Pluralize("user") + ".";
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Server;
|
using DHT.Desktop.Server;
|
||||||
using DHT.Server.Database;
|
using DHT.Server;
|
||||||
using DHT.Server.Service;
|
using DHT.Server.Service;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Controls;
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<ServerConfigurationPanelModel>();
|
||||||
|
|
||||||
private string inputPort;
|
private string inputPort;
|
||||||
|
|
||||||
public string InputPort {
|
public string InputPort {
|
||||||
@@ -29,7 +34,7 @@ sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasMadeChanges => ServerManager.Port.ToString() != InputPort || ServerManager.Token != InputToken;
|
public bool HasMadeChanges => ServerConfiguration.Port.ToString() != InputPort || ServerConfiguration.Token != InputToken;
|
||||||
|
|
||||||
private bool isToggleServerButtonEnabled = true;
|
private bool isToggleServerButtonEnabled = true;
|
||||||
|
|
||||||
@@ -38,59 +43,69 @@ sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
|||||||
set => Change(ref isToggleServerButtonEnabled, value);
|
set => Change(ref isToggleServerButtonEnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToggleServerButtonText => serverManager.IsRunning ? "Stop Server" : "Start Server";
|
public string ToggleServerButtonText => server.IsRunning ? "Stop Server" : "Start Server";
|
||||||
|
|
||||||
public event EventHandler<StatusBarModel.Status>? ServerStatusChanged;
|
|
||||||
|
|
||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
private readonly ServerManager serverManager;
|
private readonly ServerManager server;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public ServerConfigurationPanelModel() : this(null!, new ServerManager(DummyDatabaseFile.Instance)) {}
|
public ServerConfigurationPanelModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public ServerConfigurationPanelModel(Window window, ServerManager serverManager) {
|
public ServerConfigurationPanelModel(Window window, State state) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.serverManager = serverManager;
|
this.server = state.Server;
|
||||||
this.inputPort = ServerManager.Port.ToString();
|
this.inputPort = ServerConfiguration.Port.ToString();
|
||||||
this.inputToken = ServerManager.Token;
|
this.inputToken = ServerConfiguration.Token;
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
server.StatusChanged += OnServerStatusChanged;
|
||||||
ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged;
|
server.StatusChanged -= OnServerStatusChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) {
|
private void OnServerStatusChanged(object? sender, ServerManager.Status e) {
|
||||||
ServerStatusChanged?.Invoke(this, serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped);
|
Dispatcher.UIThread.InvokeAsync(UpdateServerStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateServerStatus() {
|
||||||
OnPropertyChanged(nameof(ToggleServerButtonText));
|
OnPropertyChanged(nameof(ToggleServerButtonText));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartServer() {
|
||||||
|
IsToggleServerButtonEnabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.Start(ServerConfiguration.Port, ServerConfiguration.Token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error(e);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateServerStatus();
|
||||||
IsToggleServerButtonEnabled = true;
|
IsToggleServerButtonEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BeforeServerStart() {
|
private async Task StopServer() {
|
||||||
IsToggleServerButtonEnabled = false;
|
IsToggleServerButtonEnabled = false;
|
||||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Starting);
|
|
||||||
|
try {
|
||||||
|
await server.Stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error(e);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartServer() {
|
UpdateServerStatus();
|
||||||
BeforeServerStart();
|
IsToggleServerButtonEnabled = true;
|
||||||
serverManager.Launch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopServer() {
|
public async Task OnClickToggleServerButton() {
|
||||||
IsToggleServerButtonEnabled = false;
|
if (server.IsRunning) {
|
||||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Stopping);
|
await StopServer();
|
||||||
serverManager.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnClickToggleServerButton() {
|
|
||||||
if (serverManager.IsRunning) {
|
|
||||||
StopServer();
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
StartServer();
|
await StartServer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,19 +113,21 @@ sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
|||||||
InputToken = ServerUtils.GenerateRandomToken(20);
|
InputToken = ServerUtils.GenerateRandomToken(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OnClickApplyChanges() {
|
public async Task OnClickApplyChanges() {
|
||||||
if (!ushort.TryParse(InputPort, out ushort port)) {
|
if (!ushort.TryParse(InputPort, out ushort port)) {
|
||||||
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BeforeServerStart();
|
ServerConfiguration.Port = port;
|
||||||
serverManager.Relaunch(port, InputToken);
|
ServerConfiguration.Token = inputToken;
|
||||||
|
await StartServer();
|
||||||
|
|
||||||
OnPropertyChanged(nameof(HasMadeChanges));
|
OnPropertyChanged(nameof(HasMadeChanges));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnClickCancelChanges() {
|
public void OnClickCancelChanges() {
|
||||||
InputPort = ServerManager.Port.ToString();
|
InputPort = ServerConfiguration.Port.ToString();
|
||||||
InputToken = ServerManager.Token;
|
InputToken = ServerConfiguration.Token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -40,22 +40,22 @@
|
|||||||
<StackPanel Orientation="Horizontal" Margin="6 3">
|
<StackPanel Orientation="Horizontal" Margin="6 3">
|
||||||
<StackPanel Orientation="Vertical" Width="65">
|
<StackPanel Orientation="Vertical" Width="65">
|
||||||
<TextBlock Classes="label">Status</TextBlock>
|
<TextBlock Classes="label">Status</TextBlock>
|
||||||
<TextBlock FontSize="12" Margin="0 3 0 0" Text="{Binding StatusText}" />
|
<TextBlock FontSize="12" Margin="0 3 0 0" Text="{Binding ServerStatusText}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Servers</TextBlock>
|
<TextBlock Classes="label">Servers</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, Converter={StaticResource NumberValueConverter}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Channels</TextBlock>
|
<TextBlock Classes="label">Channels</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, Converter={StaticResource NumberValueConverter}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Messages</TextBlock>
|
<TextBlock Classes="label">Messages</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, Converter={StaticResource NumberValueConverter}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
@@ -1,45 +1,46 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Service;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Controls;
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
sealed class StatusBarModel : BaseModel {
|
sealed class StatusBarModel : BaseModel, IDisposable {
|
||||||
public DatabaseStatistics DatabaseStatistics { get; }
|
public DatabaseStatistics DatabaseStatistics { get; }
|
||||||
|
|
||||||
private Status status = Status.Stopped;
|
private ServerManager.Status serverStatus;
|
||||||
|
|
||||||
public Status CurrentStatus {
|
public string ServerStatusText => serverStatus switch {
|
||||||
get => status;
|
ServerManager.Status.Starting => "STARTING",
|
||||||
set {
|
ServerManager.Status.Started => "READY",
|
||||||
status = value;
|
ServerManager.Status.Stopping => "STOPPING",
|
||||||
OnPropertyChanged(nameof(StatusText));
|
ServerManager.Status.Stopped => "STOPPED",
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusText {
|
|
||||||
get {
|
|
||||||
return CurrentStatus switch {
|
|
||||||
Status.Starting => "STARTING",
|
|
||||||
Status.Ready => "READY",
|
|
||||||
Status.Stopping => "STOPPING",
|
|
||||||
Status.Stopped => "STOPPED",
|
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
private readonly State state;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public StatusBarModel() : this(new DatabaseStatistics()) {}
|
public StatusBarModel() : this(State.Dummy) {}
|
||||||
|
|
||||||
public StatusBarModel(DatabaseStatistics databaseStatistics) {
|
public StatusBarModel(State state) {
|
||||||
this.DatabaseStatistics = databaseStatistics;
|
this.state = state;
|
||||||
|
this.DatabaseStatistics = state.Db.Statistics;
|
||||||
|
|
||||||
|
state.Server.StatusChanged += OnServerStatusChanged;
|
||||||
|
serverStatus = state.Server.IsRunning ? ServerManager.Status.Started : ServerManager.Status.Stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Status {
|
public void Dispose() {
|
||||||
Starting,
|
state.Server.StatusChanged += OnServerStatusChanged;
|
||||||
Ready,
|
}
|
||||||
Stopping,
|
|
||||||
Stopped
|
private void OnServerStatusChanged(object? sender, ServerManager.Status e) {
|
||||||
|
Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
|
serverStatus = e;
|
||||||
|
OnPropertyChanged(nameof(ServerStatusText));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
Width="800" Height="500"
|
Width="800" Height="500"
|
||||||
MinWidth="520" MinHeight="300"
|
MinWidth="520" MinHeight="300"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
Closed="OnClosed">
|
Closing="OnClosing">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<main:MainWindowModel />
|
<main:MainWindowModel />
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DHT.Desktop.Main.Pages;
|
using DHT.Desktop.Main.Pages;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main;
|
namespace DHT.Desktop.Main;
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
public sealed partial class MainWindow : Window {
|
public sealed partial class MainWindow : Window {
|
||||||
|
private static readonly Log Log = Log.ForType<MainWindow>();
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public MainWindow() {
|
public MainWindow() {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -20,9 +24,24 @@ public sealed partial class MainWindow : Window {
|
|||||||
DataContext = new MainWindowModel(this, args);
|
DataContext = new MainWindowModel(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnClosed(object? sender, EventArgs e) {
|
public async void OnClosing(object? sender, WindowClosingEventArgs e) {
|
||||||
if (DataContext is IDisposable disposable) {
|
e.Cancel = true;
|
||||||
disposable.Dispose();
|
Closing -= OnClosing;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Dispose();
|
||||||
|
} finally {
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Dispose() {
|
||||||
|
if (DataContext is MainWindowModel model) {
|
||||||
|
try {
|
||||||
|
await model.DisposeAsync();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error("Caught exception while disposing window: " + ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var temporaryFile in ViewerPageModel.TemporaryFiles) {
|
foreach (var temporaryFile in ViewerPageModel.TemporaryFiles) {
|
||||||
|
@@ -7,14 +7,17 @@ using Avalonia.Controls;
|
|||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Main.Screens;
|
using DHT.Desktop.Main.Screens;
|
||||||
using DHT.Desktop.Server;
|
using DHT.Desktop.Server;
|
||||||
using DHT.Server.Database;
|
using DHT.Server;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main;
|
namespace DHT.Desktop.Main;
|
||||||
|
|
||||||
sealed class MainWindowModel : BaseModel, IDisposable {
|
sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
||||||
private const string DefaultTitle = "Discord History Tracker";
|
private const string DefaultTitle = "Discord History Tracker";
|
||||||
|
|
||||||
|
private static readonly Log Log = Log.ForType<MainWindowModel>();
|
||||||
|
|
||||||
public string Title { get; private set; } = DefaultTitle;
|
public string Title { get; private set; } = DefaultTitle;
|
||||||
|
|
||||||
public UserControl CurrentScreen { get; private set; }
|
public UserControl CurrentScreen { get; private set; }
|
||||||
@@ -27,7 +30,7 @@ sealed class MainWindowModel : BaseModel, IDisposable {
|
|||||||
|
|
||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
|
|
||||||
private IDatabaseFile? db;
|
private State? state;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public MainWindowModel() : this(null!, Arguments.Empty) {}
|
public MainWindowModel() : this(null!, Arguments.Empty) {}
|
||||||
@@ -63,11 +66,11 @@ sealed class MainWindowModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServerPort != null) {
|
if (args.ServerPort != null) {
|
||||||
ServerManager.Port = args.ServerPort.Value;
|
ServerConfiguration.Port = args.ServerPort.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServerToken != null) {
|
if (args.ServerToken != null) {
|
||||||
ServerManager.Token = args.ServerToken;
|
ServerConfiguration.Token = args.ServerToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,18 +81,29 @@ sealed class MainWindowModel : BaseModel, IDisposable {
|
|||||||
mainContentScreenModel.Dispose();
|
mainContentScreenModel.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
db?.Dispose();
|
if (state != null) {
|
||||||
db = welcomeScreenModel.Db;
|
await state.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (db == null) {
|
if (welcomeScreenModel.Db == null) {
|
||||||
|
state = null;
|
||||||
Title = DefaultTitle;
|
Title = DefaultTitle;
|
||||||
mainContentScreenModel = null;
|
mainContentScreenModel = null;
|
||||||
mainContentScreen = null;
|
mainContentScreen = null;
|
||||||
CurrentScreen = welcomeScreen;
|
CurrentScreen = welcomeScreen;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Title = Path.GetFileName(db.Path) + " - " + DefaultTitle;
|
state = new State(welcomeScreenModel.Db);
|
||||||
mainContentScreenModel = new MainContentScreenModel(window, db);
|
|
||||||
|
try {
|
||||||
|
await state.Server.Start(ServerConfiguration.Port, ServerConfiguration.Token);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error(ex);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Title = Path.GetFileName(state.Db.Path) + " - " + DefaultTitle;
|
||||||
|
mainContentScreenModel = new MainContentScreenModel(window, state);
|
||||||
await mainContentScreenModel.Initialize();
|
await mainContentScreenModel.Initialize();
|
||||||
mainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
mainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
||||||
mainContentScreen = new MainContentScreen { DataContext = mainContentScreenModel };
|
mainContentScreen = new MainContentScreen { DataContext = mainContentScreenModel };
|
||||||
@@ -107,10 +121,14 @@ sealed class MainWindowModel : BaseModel, IDisposable {
|
|||||||
welcomeScreenModel.CloseDatabase();
|
welcomeScreenModel.CloseDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public async ValueTask DisposeAsync() {
|
||||||
welcomeScreenModel.Dispose();
|
|
||||||
mainContentScreenModel?.Dispose();
|
mainContentScreenModel?.Dispose();
|
||||||
db?.Dispose();
|
|
||||||
db = null;
|
if (state != null) {
|
||||||
|
await state.DisposeAsync();
|
||||||
|
state = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
welcomeScreenModel.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,7 @@ using System;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
using DHT.Desktop.Server;
|
using DHT.Server;
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages;
|
namespace DHT.Desktop.Main.Pages;
|
||||||
@@ -12,20 +11,16 @@ sealed class AdvancedPageModel : BaseModel, IDisposable {
|
|||||||
public ServerConfigurationPanelModel ServerConfigurationModel { get; }
|
public ServerConfigurationPanelModel ServerConfigurationModel { get; }
|
||||||
|
|
||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
private readonly IDatabaseFile db;
|
private readonly State state;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public AdvancedPageModel() : this(null!, DummyDatabaseFile.Instance, new ServerManager(DummyDatabaseFile.Instance)) {}
|
public AdvancedPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public AdvancedPageModel(Window window, IDatabaseFile db, ServerManager serverManager) {
|
public AdvancedPageModel(Window window, State state) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.db = db;
|
this.state = state;
|
||||||
|
|
||||||
ServerConfigurationModel = new ServerConfigurationPanelModel(window, serverManager);
|
ServerConfigurationModel = new ServerConfigurationPanelModel(window, state);
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
|
||||||
ServerConfigurationModel.Initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
@@ -33,7 +28,7 @@ sealed class AdvancedPageModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async void VacuumDatabase() {
|
public async void VacuumDatabase() {
|
||||||
db.Vacuum();
|
state.Db.Vacuum();
|
||||||
await Dialog.ShowOk(window, "Vacuum Database", "Done.");
|
await Dialog.ShowOk(window, "Vacuum Database", "Done.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,7 @@
|
|||||||
<StackPanel Orientation="Vertical" Spacing="20">
|
<StackPanel Orientation="Vertical" Spacing="20">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" DockPanel.Dock="Left" />
|
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" DockPanel.Dock="Left" />
|
||||||
<TextBlock Text="{Binding DownloadMessage}" Margin="10 0 0 0" VerticalAlignment="Center" DockPanel.Dock="Left" />
|
<TextBlock Text="{Binding DownloadMessage}" MinWidth="100" Margin="10 0 0 0" VerticalAlignment="Center" TextAlignment="Right" DockPanel.Dock="Left" />
|
||||||
<ProgressBar Value="{Binding DownloadProgress}" IsVisible="{Binding IsDownloading}" Margin="15 0" VerticalAlignment="Center" DockPanel.Dock="Right" />
|
<ProgressBar Value="{Binding DownloadProgress}" IsVisible="{Binding IsDownloading}" Margin="15 0" VerticalAlignment="Center" DockPanel.Dock="Right" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
<controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !IsDownloading, RelativeSource={RelativeSource AncestorType=pages:AttachmentsPageModel}}" />
|
<controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !IsDownloading, RelativeSource={RelativeSource AncestorType=pages:AttachmentsPageModel}}" />
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
|
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
|
||||||
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||||
<DataGridTextColumn Header="Size" Binding="{Binding Size, Converter={StaticResource BytesValueConverter}}" Width="*" CellStyleClasses="right" />
|
<DataGridTextColumn Header="Size" Binding="{Binding Size, Mode=OneWay, Converter={StaticResource BytesValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</Expander>
|
</Expander>
|
||||||
|
@@ -1,30 +1,32 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Threading;
|
using System.Reactive.Linq;
|
||||||
using Avalonia.Threading;
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
using DHT.Desktop.Common;
|
using DHT.Desktop.Common;
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
using DHT.Server.Data.Aggregations;
|
using DHT.Server.Data.Aggregations;
|
||||||
using DHT.Server.Data.Filters;
|
using DHT.Server.Data.Filters;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Download;
|
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
using DHT.Utils.Tasks;
|
using DHT.Utils.Tasks;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages;
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
||||||
private static readonly DownloadItemFilter EnqueuedItemFilter = new() {
|
private static readonly DownloadItemFilter EnqueuedItemFilter = new () {
|
||||||
IncludeStatuses = new HashSet<DownloadStatus> {
|
IncludeStatuses = new HashSet<DownloadStatus> {
|
||||||
DownloadStatus.Enqueued
|
DownloadStatus.Enqueued,
|
||||||
|
DownloadStatus.Downloading
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private bool isThreadDownloadButtonEnabled = true;
|
private bool isThreadDownloadButtonEnabled = true;
|
||||||
|
|
||||||
public string ToggleDownloadButtonText => downloadThread == null ? "Start Downloading" : "Stop Downloading";
|
public string ToggleDownloadButtonText => IsDownloading ? "Stop Downloading" : "Start Downloading";
|
||||||
|
|
||||||
public bool IsToggleDownloadButtonEnabled {
|
public bool IsToggleDownloadButtonEnabled {
|
||||||
get => isThreadDownloadButtonEnabled;
|
get => isThreadDownloadButtonEnabled;
|
||||||
@@ -32,7 +34,7 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string DownloadMessage { get; set; } = "";
|
public string DownloadMessage { get; set; } = "";
|
||||||
public double DownloadProgress => allItemsCount is null or 0 ? 0.0 : 100.0 * doneItemsCount / allItemsCount.Value;
|
public double DownloadProgress => totalItemsToDownloadCount is null or 0 ? 0.0 : 100.0 * doneItemsCount / totalItemsToDownloadCount.Value;
|
||||||
|
|
||||||
public AttachmentFilterPanelModel FilterModel { get; }
|
public AttachmentFilterPanelModel FilterModel { get; }
|
||||||
|
|
||||||
@@ -52,33 +54,34 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsDownloading => downloadThread != null;
|
public bool IsDownloading => state.Downloader.IsDownloading;
|
||||||
public bool HasFailedDownloads => statisticsFailed.Items > 0;
|
public bool HasFailedDownloads => statisticsFailed.Items > 0;
|
||||||
|
|
||||||
private readonly IDatabaseFile db;
|
private readonly State state;
|
||||||
private readonly AsyncValueComputer<DownloadStatusStatistics>.Single downloadStatisticsComputer;
|
private readonly AsyncValueComputer<DownloadStatusStatistics>.Single downloadStatisticsComputer;
|
||||||
private BackgroundDownloadThread? downloadThread;
|
|
||||||
|
|
||||||
|
private IDisposable? finishedItemsSubscription;
|
||||||
private int doneItemsCount;
|
private int doneItemsCount;
|
||||||
private int? allItemsCount;
|
private int initialFinishedCount;
|
||||||
|
private int? totalItemsToDownloadCount;
|
||||||
|
|
||||||
public AttachmentsPageModel() : this(DummyDatabaseFile.Instance) {}
|
public AttachmentsPageModel() : this(State.Dummy) {}
|
||||||
|
|
||||||
public AttachmentsPageModel(IDatabaseFile db) {
|
public AttachmentsPageModel(State state) {
|
||||||
this.db = db;
|
this.state = state;
|
||||||
this.FilterModel = new AttachmentFilterPanelModel(db);
|
|
||||||
|
|
||||||
this.downloadStatisticsComputer = AsyncValueComputer<DownloadStatusStatistics>.WithResultProcessor(UpdateStatistics).WithOutdatedResults().BuildWithComputer(db.GetDownloadStatusStatistics);
|
FilterModel = new AttachmentFilterPanelModel(state);
|
||||||
this.downloadStatisticsComputer.Recompute();
|
|
||||||
|
|
||||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
downloadStatisticsComputer = AsyncValueComputer<DownloadStatusStatistics>.WithResultProcessor(UpdateStatistics).WithOutdatedResults().BuildWithComputer(state.Db.GetDownloadStatusStatistics);
|
||||||
|
downloadStatisticsComputer.Recompute();
|
||||||
|
|
||||||
|
state.Db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
state.Db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||||
|
finishedItemsSubscription?.Dispose();
|
||||||
FilterModel.Dispose();
|
FilterModel.Dispose();
|
||||||
DisposeDownloadThread();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
@@ -98,7 +101,7 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
private void EnqueueDownloadItems() {
|
private void EnqueueDownloadItems() {
|
||||||
var filter = FilterModel.CreateFilter();
|
var filter = FilterModel.CreateFilter();
|
||||||
filter.DownloadItemRule = AttachmentFilter.DownloadItemRules.OnlyNotPresent;
|
filter.DownloadItemRule = AttachmentFilter.DownloadItemRules.OnlyNotPresent;
|
||||||
db.EnqueueDownloadItems(filter);
|
state.Db.EnqueueDownloadItems(filter);
|
||||||
|
|
||||||
downloadStatisticsComputer.Recompute();
|
downloadStatisticsComputer.Recompute();
|
||||||
}
|
}
|
||||||
@@ -124,75 +127,76 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
OnPropertyChanged(nameof(HasFailedDownloads));
|
OnPropertyChanged(nameof(HasFailedDownloads));
|
||||||
}
|
}
|
||||||
|
|
||||||
allItemsCount = doneItemsCount + statisticsEnqueued.Items;
|
totalItemsToDownloadCount = statisticsEnqueued.Items + statisticsDownloaded.Items + statisticsFailed.Items - initialFinishedCount;
|
||||||
UpdateDownloadMessage();
|
UpdateDownloadMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateDownloadMessage() {
|
private void UpdateDownloadMessage() {
|
||||||
DownloadMessage = IsDownloading ? doneItemsCount.Format() + " / " + (allItemsCount?.Format() ?? "?") : "";
|
DownloadMessage = IsDownloading ? doneItemsCount.Format() + " / " + (totalItemsToDownloadCount?.Format() ?? "?") : "";
|
||||||
|
|
||||||
OnPropertyChanged(nameof(DownloadMessage));
|
OnPropertyChanged(nameof(DownloadMessage));
|
||||||
OnPropertyChanged(nameof(DownloadProgress));
|
OnPropertyChanged(nameof(DownloadProgress));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadThreadOnOnItemFinished(object? sender, DownloadItem e) {
|
private void OnItemsFinished(int finishedItemCount) {
|
||||||
Interlocked.Increment(ref doneItemsCount);
|
doneItemsCount += finishedItemCount;
|
||||||
|
UpdateDownloadMessage();
|
||||||
Dispatcher.UIThread.Invoke(UpdateDownloadMessage);
|
|
||||||
downloadStatisticsComputer.Recompute();
|
downloadStatisticsComputer.Recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadThreadOnOnServerStopped(object? sender, EventArgs e) {
|
public async Task OnClickToggleDownload() {
|
||||||
downloadStatisticsComputer.Recompute();
|
|
||||||
IsToggleDownloadButtonEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnClickToggleDownload() {
|
|
||||||
if (downloadThread == null) {
|
|
||||||
EnqueueDownloadItems();
|
|
||||||
downloadThread = new BackgroundDownloadThread(db);
|
|
||||||
downloadThread.OnItemFinished += DownloadThreadOnOnItemFinished;
|
|
||||||
downloadThread.OnServerStopped += DownloadThreadOnOnServerStopped;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
IsToggleDownloadButtonEnabled = false;
|
IsToggleDownloadButtonEnabled = false;
|
||||||
DisposeDownloadThread();
|
|
||||||
|
|
||||||
db.RemoveDownloadItems(EnqueuedItemFilter, FilterRemovalMode.RemoveMatching);
|
if (IsDownloading) {
|
||||||
|
await state.Downloader.Stop();
|
||||||
|
|
||||||
|
finishedItemsSubscription?.Dispose();
|
||||||
|
finishedItemsSubscription = null;
|
||||||
|
|
||||||
|
downloadStatisticsComputer.Recompute();
|
||||||
|
|
||||||
|
state.Db.RemoveDownloadItems(EnqueuedItemFilter, FilterRemovalMode.RemoveMatching);
|
||||||
|
|
||||||
doneItemsCount = 0;
|
doneItemsCount = 0;
|
||||||
allItemsCount = null;
|
initialFinishedCount = 0;
|
||||||
|
totalItemsToDownloadCount = null;
|
||||||
UpdateDownloadMessage();
|
UpdateDownloadMessage();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
var finishedItems = await state.Downloader.Start();
|
||||||
|
|
||||||
|
initialFinishedCount = statisticsDownloaded.Items + statisticsFailed.Items;
|
||||||
|
finishedItemsSubscription = finishedItems.Select(static _ => true)
|
||||||
|
.Buffer(TimeSpan.FromMilliseconds(100))
|
||||||
|
.Select(static items => items.Count)
|
||||||
|
.Where(static items => items > 0)
|
||||||
|
.ObserveOn(AvaloniaScheduler.Instance)
|
||||||
|
.Subscribe(OnItemsFinished);
|
||||||
|
|
||||||
|
EnqueueDownloadItems();
|
||||||
|
}
|
||||||
|
|
||||||
OnPropertyChanged(nameof(ToggleDownloadButtonText));
|
OnPropertyChanged(nameof(ToggleDownloadButtonText));
|
||||||
OnPropertyChanged(nameof(IsDownloading));
|
OnPropertyChanged(nameof(IsDownloading));
|
||||||
|
IsToggleDownloadButtonEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnClickRetryFailedDownloads() {
|
public void OnClickRetryFailedDownloads() {
|
||||||
var allExceptFailedFilter = new DownloadItemFilter {
|
var allExceptFailedFilter = new DownloadItemFilter {
|
||||||
IncludeStatuses = new HashSet<DownloadStatus> {
|
IncludeStatuses = new HashSet<DownloadStatus> {
|
||||||
DownloadStatus.Enqueued,
|
DownloadStatus.Enqueued,
|
||||||
|
DownloadStatus.Downloading,
|
||||||
DownloadStatus.Success
|
DownloadStatus.Success
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
db.RemoveDownloadItems(allExceptFailedFilter, FilterRemovalMode.KeepMatching);
|
state.Db.RemoveDownloadItems(allExceptFailedFilter, FilterRemovalMode.KeepMatching);
|
||||||
|
|
||||||
if (IsDownloading) {
|
if (IsDownloading) {
|
||||||
EnqueueDownloadItems();
|
EnqueueDownloadItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeDownloadThread() {
|
|
||||||
if (downloadThread != null) {
|
|
||||||
downloadThread.OnItemFinished -= DownloadThreadOnOnItemFinished;
|
|
||||||
downloadThread.StopThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadThread = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class StatisticsRow {
|
public sealed class StatisticsRow {
|
||||||
public string State { get; }
|
public string State { get; }
|
||||||
public int Items { get; set; }
|
public int Items { get; set; }
|
||||||
|
@@ -14,6 +14,7 @@ using DHT.Desktop.Dialogs.File;
|
|||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Dialogs.Progress;
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
using DHT.Desktop.Dialogs.TextBox;
|
using DHT.Desktop.Dialogs.TextBox;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Database.Import;
|
using DHT.Server.Database.Import;
|
||||||
@@ -33,11 +34,11 @@ sealed class DatabasePageModel : BaseModel {
|
|||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public DatabasePageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
public DatabasePageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public DatabasePageModel(Window window, IDatabaseFile db) {
|
public DatabasePageModel(Window window, State state) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.Db = db;
|
this.Db = state.Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenDatabaseFolder() {
|
public async void OpenDatabaseFolder() {
|
||||||
|
@@ -6,8 +6,8 @@ using System.Threading.Tasks;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Dialogs.Progress;
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Service;
|
using DHT.Server.Service;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
@@ -18,14 +18,14 @@ namespace DHT.Desktop.Main.Pages {
|
|||||||
public string GenerateMessages { get; set; } = "0";
|
public string GenerateMessages { get; set; } = "0";
|
||||||
|
|
||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
private readonly IDatabaseFile db;
|
private readonly State state;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public DebugPageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
public DebugPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public DebugPageModel(Window window, IDatabaseFile db) {
|
public DebugPageModel(Window window, State state) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.db = db;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OnClickAddRandomDataToDatabase() {
|
public async void OnClickAddRandomDataToDatabase() {
|
||||||
@@ -83,11 +83,11 @@ namespace DHT.Desktop.Main.Pages {
|
|||||||
Discriminator = rand.Next(0, 9999).ToString(),
|
Discriminator = rand.Next(0, 9999).ToString(),
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
db.AddServer(server);
|
state.Db.AddServer(server);
|
||||||
db.AddUsers(users);
|
state.Db.AddUsers(users);
|
||||||
|
|
||||||
foreach (var channel in channels) {
|
foreach (var channel in channels) {
|
||||||
db.AddChannel(channel);
|
state.Db.AddChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
@@ -117,7 +117,7 @@ namespace DHT.Desktop.Main.Pages {
|
|||||||
};
|
};
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
db.AddMessages(messages);
|
state.Db.AddMessages(messages);
|
||||||
|
|
||||||
messageCount -= BatchSize;
|
messageCount -= BatchSize;
|
||||||
await callback.Update("Adding messages in batches of " + BatchSize, ++batchIndex, batchCount);
|
await callback.Update("Adding messages in batches of " + BatchSize, ++batchIndex, batchCount);
|
||||||
|
@@ -54,7 +54,7 @@ sealed class TrackingPageModel : BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> OnClickCopyTrackingScript() {
|
public async Task<bool> OnClickCopyTrackingScript() {
|
||||||
string url = $"http://127.0.0.1:{ServerManager.Port}/get-tracking-script?token={HttpUtility.UrlEncode(ServerManager.Token)}";
|
string url = $"http://127.0.0.1:{ServerConfiguration.Port}/get-tracking-script?token={HttpUtility.UrlEncode(ServerConfiguration.Token)}";
|
||||||
string script = (await Resources.ReadTextAsync("tracker-loader.js")).Trim().Replace("{url}", url);
|
string script = (await Resources.ReadTextAsync("tracker-loader.js")).Trim().Replace("{url}", url);
|
||||||
|
|
||||||
var clipboard = window.Clipboard;
|
var clipboard = window.Clipboard;
|
||||||
|
@@ -13,8 +13,8 @@ using DHT.Desktop.Dialogs.File;
|
|||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
using DHT.Desktop.Server;
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Data.Filters;
|
using DHT.Server.Data.Filters;
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Database.Export;
|
using DHT.Server.Database.Export;
|
||||||
using DHT.Server.Database.Export.Strategy;
|
using DHT.Server.Database.Export.Strategy;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
@@ -38,16 +38,16 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
|||||||
public MessageFilterPanelModel FilterModel { get; }
|
public MessageFilterPanelModel FilterModel { get; }
|
||||||
|
|
||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
private readonly IDatabaseFile db;
|
private readonly State state;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public ViewerPageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
public ViewerPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public ViewerPageModel(Window window, IDatabaseFile db) {
|
public ViewerPageModel(Window window, State state) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.db = db;
|
this.state = state;
|
||||||
|
|
||||||
FilterModel = new MessageFilterPanelModel(window, db, "Will export");
|
FilterModel = new MessageFilterPanelModel(window, state, "Will export");
|
||||||
FilterModel.FilterPropertyChanged += OnFilterPropertyChanged;
|
FilterModel.FilterPropertyChanged += OnFilterPropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
|||||||
string jsonTempFile = path + ".tmp";
|
string jsonTempFile = path + ".tmp";
|
||||||
|
|
||||||
await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) {
|
await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) {
|
||||||
await ViewerJsonExport.Generate(jsonStream, strategy, db, FilterModel.CreateFilter());
|
await ViewerJsonExport.Generate(jsonStream, strategy, state.Db, FilterModel.CreateFilter());
|
||||||
|
|
||||||
char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)];
|
char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)];
|
||||||
jsonStream.Position = 0;
|
jsonStream.Position = 0;
|
||||||
@@ -98,7 +98,7 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
|||||||
|
|
||||||
public async void OnClickOpenViewer() {
|
public async void OnClickOpenViewer() {
|
||||||
string rootPath = Path.Combine(Path.GetTempPath(), "DiscordHistoryTracker");
|
string rootPath = Path.Combine(Path.GetTempPath(), "DiscordHistoryTracker");
|
||||||
string filenameBase = Path.GetFileNameWithoutExtension(db.Path) + "-" + DateTime.Now.ToString("yyyy-MM-dd");
|
string filenameBase = Path.GetFileNameWithoutExtension(state.Db.Path) + "-" + DateTime.Now.ToString("yyyy-MM-dd");
|
||||||
string fullPath = Path.Combine(rootPath, filenameBase + ".html");
|
string fullPath = Path.Combine(rootPath, filenameBase + ".html");
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
|||||||
TemporaryFiles.Add(fullPath);
|
TemporaryFiles.Add(fullPath);
|
||||||
|
|
||||||
Directory.CreateDirectory(rootPath);
|
Directory.CreateDirectory(rootPath);
|
||||||
await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerManager.Port, ServerManager.Token));
|
await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerConfiguration.Port, ServerConfiguration.Token));
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
|
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
|
||||||
}
|
}
|
||||||
@@ -123,8 +123,8 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
|||||||
string? path = await window.StorageProvider.SaveFile(new FilePickerSaveOptions {
|
string? path = await window.StorageProvider.SaveFile(new FilePickerSaveOptions {
|
||||||
Title = "Save Viewer",
|
Title = "Save Viewer",
|
||||||
FileTypeChoices = ViewerFileTypes,
|
FileTypeChoices = ViewerFileTypes,
|
||||||
SuggestedFileName = Path.GetFileNameWithoutExtension(db.Path) + ".html",
|
SuggestedFileName = Path.GetFileNameWithoutExtension(state.Db.Path) + ".html",
|
||||||
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, Path.GetDirectoryName(db.Path)),
|
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, Path.GetDirectoryName(state.Db.Path)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
@@ -136,13 +136,13 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
|||||||
var filter = FilterModel.CreateFilter();
|
var filter = FilterModel.CreateFilter();
|
||||||
|
|
||||||
if (DatabaseToolFilterModeKeep) {
|
if (DatabaseToolFilterModeKeep) {
|
||||||
if (DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Keep Matching Messages in This Database", db.CountMessages(filter).Pluralize("message") + " will be kept, and the rest will be removed from this database. This action cannot be undone. Proceed?")) {
|
if (DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Keep Matching Messages in This Database", state.Db.CountMessages(filter).Pluralize("message") + " will be kept, and the rest will be removed from this database. This action cannot be undone. Proceed?")) {
|
||||||
db.RemoveMessages(filter, FilterRemovalMode.KeepMatching);
|
state.Db.RemoveMessages(filter, FilterRemovalMode.KeepMatching);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (DatabaseToolFilterModeRemove) {
|
else if (DatabaseToolFilterModeRemove) {
|
||||||
if (DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Remove Matching Messages in This Database", db.CountMessages(filter).Pluralize("message") + " will be removed from this database. This action cannot be undone. Proceed?")) {
|
if (DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Remove Matching Messages in This Database", state.Db.CountMessages(filter).Pluralize("message") + " will be removed from this database. This action cannot be undone. Proceed?")) {
|
||||||
db.RemoveMessages(filter, FilterRemovalMode.RemoveMatching);
|
state.Db.RemoveMessages(filter, FilterRemovalMode.RemoveMatching);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
using DHT.Desktop.Main.Pages;
|
using DHT.Desktop.Main.Pages;
|
||||||
using DHT.Desktop.Server;
|
using DHT.Server;
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Service;
|
|
||||||
using DHT.Utils.Logging;
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Screens;
|
namespace DHT.Desktop.Main.Screens;
|
||||||
@@ -49,71 +46,43 @@ sealed class MainContentScreenModel : IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
private readonly ServerManager serverManager;
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public MainContentScreenModel() : this(null!, DummyDatabaseFile.Instance) {}
|
public MainContentScreenModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public MainContentScreenModel(Window window, IDatabaseFile db) {
|
public MainContentScreenModel(Window window, State state) {
|
||||||
this.window = window;
|
DatabasePageModel = new DatabasePageModel(window, state);
|
||||||
this.serverManager = new ServerManager(db);
|
|
||||||
|
|
||||||
ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught;
|
|
||||||
|
|
||||||
DatabasePageModel = new DatabasePageModel(window, db);
|
|
||||||
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
||||||
|
|
||||||
TrackingPageModel = new TrackingPageModel(window);
|
TrackingPageModel = new TrackingPageModel(window);
|
||||||
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
||||||
|
|
||||||
AttachmentsPageModel = new AttachmentsPageModel(db);
|
AttachmentsPageModel = new AttachmentsPageModel(state);
|
||||||
AttachmentsPage = new AttachmentsPage { DataContext = AttachmentsPageModel };
|
AttachmentsPage = new AttachmentsPage { DataContext = AttachmentsPageModel };
|
||||||
|
|
||||||
ViewerPageModel = new ViewerPageModel(window, db);
|
ViewerPageModel = new ViewerPageModel(window, state);
|
||||||
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
||||||
|
|
||||||
AdvancedPageModel = new AdvancedPageModel(window, db, serverManager);
|
AdvancedPageModel = new AdvancedPageModel(window, state);
|
||||||
AdvancedPage = new AdvancedPage { DataContext = AdvancedPageModel };
|
AdvancedPage = new AdvancedPage { DataContext = AdvancedPageModel };
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
DebugPageModel = new DebugPageModel(window, db);
|
DebugPageModel = new DebugPageModel(window, state);
|
||||||
DebugPage = new DebugPage { DataContext = DebugPageModel };
|
DebugPage = new DebugPage { DataContext = DebugPageModel };
|
||||||
#else
|
#else
|
||||||
DebugPage = null;
|
DebugPage = null;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
StatusBarModel = new StatusBarModel(db.Statistics);
|
StatusBarModel = new StatusBarModel(state);
|
||||||
|
|
||||||
AdvancedPageModel.ServerConfigurationModel.ServerStatusChanged += OnServerStatusChanged;
|
|
||||||
DatabaseClosed += OnDatabaseClosed;
|
|
||||||
|
|
||||||
StatusBarModel.CurrentStatus = serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() {
|
public async Task Initialize() {
|
||||||
await TrackingPageModel.Initialize();
|
await TrackingPageModel.Initialize();
|
||||||
AdvancedPageModel.Initialize();
|
|
||||||
serverManager.Launch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
|
||||||
AttachmentsPageModel.Dispose();
|
AttachmentsPageModel.Dispose();
|
||||||
ViewerPageModel.Dispose();
|
ViewerPageModel.Dispose();
|
||||||
serverManager.Dispose();
|
AdvancedPageModel.Dispose();
|
||||||
}
|
StatusBarModel.Dispose();
|
||||||
|
|
||||||
private void OnServerStatusChanged(object? sender, StatusBarModel.Status e) {
|
|
||||||
StatusBarModel.CurrentStatus = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDatabaseClosed(object? sender, EventArgs e) {
|
|
||||||
serverManager.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) {
|
|
||||||
Log.Error(ex);
|
|
||||||
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
using System.Globalization;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
using DHT.Utils.Resources;
|
using DHT.Utils.Resources;
|
||||||
|
|
||||||
namespace DHT.Desktop;
|
namespace DHT.Desktop;
|
||||||
@@ -9,6 +11,7 @@ static class Program {
|
|||||||
public static string Version { get; }
|
public static string Version { get; }
|
||||||
public static CultureInfo Culture { get; }
|
public static CultureInfo Culture { get; }
|
||||||
public static ResourceLoader Resources { get; }
|
public static ResourceLoader Resources { get; }
|
||||||
|
public static Arguments Arguments { get; }
|
||||||
|
|
||||||
static Program() {
|
static Program() {
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
@@ -25,10 +28,21 @@ static class Program {
|
|||||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
Resources = new ResourceLoader(assembly);
|
Resources = new ResourceLoader(assembly);
|
||||||
|
Arguments = new Arguments(Environment.GetCommandLineArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Main(string[] args) {
|
public static void Main(string[] args) {
|
||||||
|
if (Arguments.Console && OperatingSystem.IsWindows()) {
|
||||||
|
WindowsConsole.AllocConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
} finally {
|
||||||
|
if (Arguments.Console && OperatingSystem.IsWindows()) {
|
||||||
|
WindowsConsole.FreeConsole();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AppBuilder BuildAvaloniaApp() {
|
private static AppBuilder BuildAvaloniaApp() {
|
||||||
|
8
app/Desktop/Server/ServerConfiguration.cs
Normal file
8
app/Desktop/Server/ServerConfiguration.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using DHT.Server.Service;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Server;
|
||||||
|
|
||||||
|
static class ServerConfiguration {
|
||||||
|
public static ushort Port { get; set; } = ServerUtils.FindAvailablePort(50000, 60000);
|
||||||
|
public static string Token { get; set; } = ServerUtils.GenerateRandomToken(20);
|
||||||
|
}
|
@@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Service;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Server;
|
|
||||||
|
|
||||||
sealed class ServerManager : IDisposable {
|
|
||||||
public static ushort Port { get; set; } = ServerUtils.FindAvailablePort(50000, 60000);
|
|
||||||
public static string Token { get; set; } = ServerUtils.GenerateRandomToken(20);
|
|
||||||
|
|
||||||
private static ServerManager? instance;
|
|
||||||
|
|
||||||
public bool IsRunning => ServerLauncher.IsRunning;
|
|
||||||
|
|
||||||
private readonly IDatabaseFile db;
|
|
||||||
|
|
||||||
public ServerManager(IDatabaseFile db) {
|
|
||||||
if (db != DummyDatabaseFile.Instance) {
|
|
||||||
if (instance != null) {
|
|
||||||
throw new InvalidOperationException("Only one instance of ServerManager can exist at the same time!");
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Launch() {
|
|
||||||
ServerLauncher.Relaunch(Port, Token, db);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Relaunch(ushort port, string token) {
|
|
||||||
Port = port;
|
|
||||||
Token = token;
|
|
||||||
Launch();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop() {
|
|
||||||
ServerLauncher.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
Stop();
|
|
||||||
|
|
||||||
if (instance == this) {
|
|
||||||
instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -8,5 +8,6 @@ namespace DHT.Server.Data;
|
|||||||
public enum DownloadStatus {
|
public enum DownloadStatus {
|
||||||
Enqueued = 0,
|
Enqueued = 0,
|
||||||
GenericError = 1,
|
GenericError = 1,
|
||||||
|
Downloading = 2,
|
||||||
Success = HttpStatusCode.OK
|
Success = HttpStatusCode.OK
|
||||||
}
|
}
|
||||||
|
@@ -74,7 +74,7 @@ public sealed class DummyDatabaseFile : IDatabaseFile {
|
|||||||
|
|
||||||
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}
|
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}
|
||||||
|
|
||||||
public List<DownloadItem> GetEnqueuedDownloadItems(int count) {
|
public List<DownloadItem> PullEnqueuedDownloadItems(int count) {
|
||||||
return new();
|
return new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ public interface IDatabaseFile : IDisposable {
|
|||||||
DownloadedAttachment? GetDownloadedAttachment(string url);
|
DownloadedAttachment? GetDownloadedAttachment(string url);
|
||||||
|
|
||||||
void EnqueueDownloadItems(AttachmentFilter? filter = null);
|
void EnqueueDownloadItems(AttachmentFilter? filter = null);
|
||||||
List<DownloadItem> GetEnqueuedDownloadItems(int count);
|
List<DownloadItem> PullEnqueuedDownloadItems(int count);
|
||||||
void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode);
|
void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode);
|
||||||
DownloadStatusStatistics GetDownloadStatusStatistics();
|
DownloadStatusStatistics GetDownloadStatusStatistics();
|
||||||
|
|
||||||
|
@@ -174,8 +174,8 @@ sealed class Schema {
|
|||||||
int processedUrls = -1;
|
int processedUrls = -1;
|
||||||
|
|
||||||
await using (var updateCmd = conn.Command("UPDATE attachments SET download_url = url, url = :normalized_url WHERE attachment_id = :attachment_id")) {
|
await using (var updateCmd = conn.Command("UPDATE attachments SET download_url = url, url = :normalized_url WHERE attachment_id = :attachment_id")) {
|
||||||
updateCmd.Parameters.Add(":attachment_id", SqliteType.Integer);
|
updateCmd.Add(":attachment_id", SqliteType.Integer);
|
||||||
updateCmd.Parameters.Add(":normalized_url", SqliteType.Text);
|
updateCmd.Add(":normalized_url", SqliteType.Text);
|
||||||
|
|
||||||
foreach (var (attachmentId, normalizedUrl) in normalizedUrls) {
|
foreach (var (attachmentId, normalizedUrl) in normalizedUrls) {
|
||||||
if (++processedUrls % 1000 == 0) {
|
if (++processedUrls % 1000 == 0) {
|
||||||
@@ -235,8 +235,8 @@ sealed class Schema {
|
|||||||
tx = conn.BeginTransaction();
|
tx = conn.BeginTransaction();
|
||||||
|
|
||||||
await using (var updateCmd = conn.Command("UPDATE downloads SET download_url = :download_url, url = :normalized_url WHERE url = :download_url")) {
|
await using (var updateCmd = conn.Command("UPDATE downloads SET download_url = :download_url, url = :normalized_url WHERE url = :download_url")) {
|
||||||
updateCmd.Parameters.Add(":normalized_url", SqliteType.Text);
|
updateCmd.Add(":normalized_url", SqliteType.Text);
|
||||||
updateCmd.Parameters.Add(":download_url", SqliteType.Text);
|
updateCmd.Add(":download_url", SqliteType.Text);
|
||||||
|
|
||||||
foreach (var (normalizedUrl, downloadUrl) in normalizedUrlsToOriginalUrls) {
|
foreach (var (normalizedUrl, downloadUrl) in normalizedUrlsToOriginalUrls) {
|
||||||
if (++processedUrls % 100 == 0) {
|
if (++processedUrls % 100 == 0) {
|
||||||
|
@@ -522,25 +522,42 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
|||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DownloadItem> GetEnqueuedDownloadItems(int count) {
|
public List<DownloadItem> PullEnqueuedDownloadItems(int count) {
|
||||||
var list = new List<DownloadItem>();
|
var found = new List<DownloadItem>();
|
||||||
|
var pulled = new List<DownloadItem>();
|
||||||
|
|
||||||
using var conn = pool.Take();
|
using var conn = pool.Take();
|
||||||
using var cmd = conn.Command("SELECT normalized_url, download_url, size FROM downloads WHERE status = :enqueued LIMIT :limit");
|
using (var cmd = conn.Command("SELECT normalized_url, download_url, size FROM downloads WHERE status = :enqueued LIMIT :limit")) {
|
||||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||||
cmd.AddAndSet(":limit", SqliteType.Integer, Math.Max(0, count));
|
cmd.AddAndSet(":limit", SqliteType.Integer, Math.Max(0, count));
|
||||||
|
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
|
|
||||||
while (reader.Read()) {
|
while (reader.Read()) {
|
||||||
list.Add(new DownloadItem {
|
found.Add(new DownloadItem {
|
||||||
NormalizedUrl = reader.GetString(0),
|
NormalizedUrl = reader.GetString(0),
|
||||||
DownloadUrl = reader.GetString(1),
|
DownloadUrl = reader.GetString(1),
|
||||||
Size = reader.GetUint64(2),
|
Size = reader.GetUint64(2),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return list;
|
if (found.Count != 0) {
|
||||||
|
using var cmd = conn.Command("UPDATE downloads SET status = :downloading WHERE normalized_url = :normalized_url AND status = :enqueued");
|
||||||
|
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||||
|
cmd.AddAndSet(":downloading", SqliteType.Integer, (int) DownloadStatus.Downloading);
|
||||||
|
cmd.Add(":normalized_url", SqliteType.Text);
|
||||||
|
|
||||||
|
foreach (var item in found) {
|
||||||
|
cmd.Set(":normalized_url", item.NormalizedUrl);
|
||||||
|
|
||||||
|
if (cmd.ExecuteNonQuery() == 1) {
|
||||||
|
pulled.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pulled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode) {
|
public void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode) {
|
||||||
@@ -562,15 +579,16 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
|||||||
static void LoadSuccessStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
|
static void LoadSuccessStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
|
||||||
using var cmd = conn.Command("""
|
using var cmd = conn.Command("""
|
||||||
SELECT
|
SELECT
|
||||||
IFNULL(SUM(CASE WHEN status = :enqueued THEN 1 ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status IN (:enqueued, :downloading) THEN 1 ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status = :enqueued THEN size ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status IN (:enqueued, :downloading) THEN size ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status = :success THEN 1 ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status = :success THEN 1 ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status = :success THEN size ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status = :success THEN size ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status != :enqueued AND status != :success THEN 1 ELSE 0 END), 0),
|
IFNULL(SUM(CASE WHEN status NOT IN (:enqueued, :downloading) AND status != :success THEN 1 ELSE 0 END), 0),
|
||||||
IFNULL(SUM(CASE WHEN status != :enqueued AND status != :success THEN size ELSE 0 END), 0)
|
IFNULL(SUM(CASE WHEN status NOT IN (:enqueued, :downloading) AND status != :success THEN size ELSE 0 END), 0)
|
||||||
FROM downloads
|
FROM downloads
|
||||||
""");
|
""");
|
||||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||||
|
cmd.AddAndSet(":downloading", SqliteType.Integer, (int) DownloadStatus.Downloading);
|
||||||
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
||||||
|
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
|
@@ -62,6 +62,10 @@ static class SqliteExtensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Add(this SqliteCommand cmd, string key, SqliteType type) {
|
||||||
|
cmd.Parameters.Add(key, type);
|
||||||
|
}
|
||||||
|
|
||||||
public static void AddAndSet(this SqliteCommand cmd, string key, SqliteType type, object? value) {
|
public static void AddAndSet(this SqliteCommand cmd, string key, SqliteType type, object? value) {
|
||||||
cmd.Parameters.Add(key, type).Value = value ?? DBNull.Value;
|
cmd.Parameters.Add(key, type).Value = value ?? DBNull.Value;
|
||||||
}
|
}
|
||||||
|
@@ -1,130 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Utils.Logging;
|
|
||||||
using DHT.Utils.Models;
|
|
||||||
|
|
||||||
namespace DHT.Server.Download;
|
|
||||||
|
|
||||||
public sealed class BackgroundDownloadThread : BaseModel {
|
|
||||||
private static readonly Log Log = Log.ForType<BackgroundDownloadThread>();
|
|
||||||
|
|
||||||
public event EventHandler<DownloadItem>? OnItemFinished {
|
|
||||||
add => parameters.OnItemFinished += value;
|
|
||||||
remove => parameters.OnItemFinished -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler? OnServerStopped {
|
|
||||||
add => parameters.OnServerStopped += value;
|
|
||||||
remove => parameters.OnServerStopped -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource;
|
|
||||||
private readonly ThreadInstance.Parameters parameters;
|
|
||||||
|
|
||||||
public BackgroundDownloadThread(IDatabaseFile db) {
|
|
||||||
this.cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
this.parameters = new ThreadInstance.Parameters(db, cancellationTokenSource);
|
|
||||||
|
|
||||||
var thread = new Thread(new ThreadInstance().Work) {
|
|
||||||
Name = "DHT download thread"
|
|
||||||
};
|
|
||||||
|
|
||||||
thread.Start(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopThread() {
|
|
||||||
try {
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
} catch (ObjectDisposedException) {
|
|
||||||
Log.Warn("Attempted to stop background download thread after the cancellation token has been disposed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ThreadInstance {
|
|
||||||
private const int QueueSize = 32;
|
|
||||||
|
|
||||||
public sealed class Parameters {
|
|
||||||
public event EventHandler<DownloadItem>? OnItemFinished;
|
|
||||||
public event EventHandler? OnServerStopped;
|
|
||||||
|
|
||||||
public IDatabaseFile Db { get; }
|
|
||||||
public CancellationTokenSource CancellationTokenSource { get; }
|
|
||||||
|
|
||||||
public Parameters(IDatabaseFile db, CancellationTokenSource cancellationTokenSource) {
|
|
||||||
Db = db;
|
|
||||||
CancellationTokenSource = cancellationTokenSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FireOnItemFinished(DownloadItem item) {
|
|
||||||
OnItemFinished?.Invoke(null, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FireOnServerStopped() {
|
|
||||||
OnServerStopped?.Invoke(null, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly HttpClient client = new ();
|
|
||||||
|
|
||||||
public ThreadInstance() {
|
|
||||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Work(object? obj) {
|
|
||||||
var parameters = (Parameters) obj!;
|
|
||||||
|
|
||||||
var cancellationTokenSource = parameters.CancellationTokenSource;
|
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
|
||||||
|
|
||||||
var db = parameters.Db;
|
|
||||||
var queue = new ConcurrentQueue<DownloadItem>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (!cancellationToken.IsCancellationRequested) {
|
|
||||||
FillQueue(db, queue, cancellationToken);
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested && queue.TryDequeue(out var item)) {
|
|
||||||
var downloadUrl = item.DownloadUrl;
|
|
||||||
Log.Debug("Downloading " + downloadUrl + "...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
db.AddDownload(Data.Download.NewSuccess(item, await client.GetByteArrayAsync(downloadUrl, cancellationToken)));
|
|
||||||
} catch (HttpRequestException e) {
|
|
||||||
db.AddDownload(Data.Download.NewFailure(item, e.StatusCode, item.Size));
|
|
||||||
Log.Error(e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
db.AddDownload(Data.Download.NewFailure(item, null, item.Size));
|
|
||||||
Log.Error(e);
|
|
||||||
} finally {
|
|
||||||
parameters.FireOnItemFinished(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
//
|
|
||||||
} catch (ObjectDisposedException) {
|
|
||||||
//
|
|
||||||
} finally {
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
parameters.FireOnServerStopped();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void FillQueue(IDatabaseFile db, ConcurrentQueue<DownloadItem> queue, CancellationToken cancellationToken) {
|
|
||||||
while (!cancellationToken.IsCancellationRequested && queue.IsEmpty) {
|
|
||||||
var newItems = db.GetEnqueuedDownloadItems(QueueSize);
|
|
||||||
if (newItems.Count == 0) {
|
|
||||||
Thread.Sleep(TimeSpan.FromMilliseconds(50));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
foreach (var item in newItems) {
|
|
||||||
queue.Enqueue(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
40
app/Server/Download/Downloader.cs
Normal file
40
app/Server/Download/Downloader.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
|
||||||
|
namespace DHT.Server.Download;
|
||||||
|
|
||||||
|
public sealed class Downloader {
|
||||||
|
private DownloaderTask? current;
|
||||||
|
public bool IsDownloading => current != null;
|
||||||
|
|
||||||
|
private readonly IDatabaseFile db;
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
internal Downloader(IDatabaseFile db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IObservable<DownloadItem>> Start() {
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
current ??= new DownloaderTask(db);
|
||||||
|
return current.FinishedItems;
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop() {
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
if (current != null) {
|
||||||
|
await current.DisposeAsync();
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
app/Server/Download/DownloaderTask.cs
Normal file
110
app/Server/Download/DownloaderTask.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using DHT.Utils.Tasks;
|
||||||
|
|
||||||
|
namespace DHT.Server.Download;
|
||||||
|
|
||||||
|
sealed class DownloaderTask : IAsyncDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<DownloaderTask>();
|
||||||
|
|
||||||
|
private const int DownloadTasks = 4;
|
||||||
|
private const int QueueSize = 25;
|
||||||
|
private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
private readonly Channel<DownloadItem> downloadQueue = Channel.CreateBounded<DownloadItem>(new BoundedChannelOptions(QueueSize) {
|
||||||
|
SingleReader = false,
|
||||||
|
SingleWriter = true,
|
||||||
|
AllowSynchronousContinuations = false,
|
||||||
|
FullMode = BoundedChannelFullMode.Wait
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||||
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
|
private readonly IDatabaseFile db;
|
||||||
|
private readonly Subject<DownloadItem> finishedItemPublisher = new ();
|
||||||
|
|
||||||
|
private readonly Task queueWriterTask;
|
||||||
|
private readonly Task[] downloadTasks;
|
||||||
|
|
||||||
|
public IObservable<DownloadItem> FinishedItems => finishedItemPublisher;
|
||||||
|
|
||||||
|
internal DownloaderTask(IDatabaseFile db) {
|
||||||
|
this.db = db;
|
||||||
|
this.cancellationToken = cancellationTokenSource.Token;
|
||||||
|
this.queueWriterTask = Task.Run(RunQueueWriterTask);
|
||||||
|
this.downloadTasks = Enumerable.Range(1, DownloadTasks).Select(taskIndex => Task.Run(() => RunDownloadTask(taskIndex))).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunQueueWriterTask() {
|
||||||
|
while (await downloadQueue.Writer.WaitToWriteAsync(cancellationToken)) {
|
||||||
|
var newItems = db.PullEnqueuedDownloadItems(QueueSize);
|
||||||
|
if (newItems.Count == 0) {
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var newItem in newItems) {
|
||||||
|
await downloadQueue.Writer.WriteAsync(newItem, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunDownloadTask(int taskIndex) {
|
||||||
|
var log = Log.ForType<DownloaderTask>("Task " + taskIndex);
|
||||||
|
|
||||||
|
var client = new HttpClient();
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested) {
|
||||||
|
var item = await downloadQueue.Reader.ReadAsync(cancellationToken);
|
||||||
|
log.Debug("Downloading " + item.DownloadUrl + "...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var downloadedBytes = await client.GetByteArrayAsync(item.DownloadUrl, cancellationToken);
|
||||||
|
db.AddDownload(Data.Download.NewSuccess(item, downloadedBytes));
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
// Ignore.
|
||||||
|
} catch (HttpRequestException e) {
|
||||||
|
db.AddDownload(Data.Download.NewFailure(item, e.StatusCode, item.Size));
|
||||||
|
log.Error(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
db.AddDownload(Data.Download.NewFailure(item, null, item.Size));
|
||||||
|
log.Error(e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
finishedItemPublisher.OnNext(item);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.Error("Caught exception in event handler: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
try {
|
||||||
|
await cancellationTokenSource.CancelAsync();
|
||||||
|
} catch (Exception) {
|
||||||
|
Log.Warn("Attempted to stop background download twice.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadQueue.Writer.Complete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queueWriterTask.WaitIgnoringCancellation();
|
||||||
|
await Task.WhenAll(downloadTasks).WaitIgnoringCancellation();
|
||||||
|
} finally {
|
||||||
|
cancellationTokenSource.Dispose();
|
||||||
|
finishedItemPublisher.OnCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -3,12 +3,9 @@ using System.Net;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Service;
|
|
||||||
using DHT.Utils.Http;
|
using DHT.Utils.Http;
|
||||||
using DHT.Utils.Logging;
|
using DHT.Utils.Logging;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
|
|
||||||
namespace DHT.Server.Endpoints;
|
namespace DHT.Server.Endpoints;
|
||||||
|
|
||||||
@@ -16,25 +13,14 @@ abstract class BaseEndpoint {
|
|||||||
private static readonly Log Log = Log.ForType<BaseEndpoint>();
|
private static readonly Log Log = Log.ForType<BaseEndpoint>();
|
||||||
|
|
||||||
protected IDatabaseFile Db { get; }
|
protected IDatabaseFile Db { get; }
|
||||||
protected ServerParameters Parameters { get; }
|
|
||||||
|
|
||||||
protected BaseEndpoint(IDatabaseFile db, ServerParameters parameters) {
|
protected BaseEndpoint(IDatabaseFile db) {
|
||||||
this.Db = db;
|
this.Db = db;
|
||||||
this.Parameters = parameters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Handle(HttpContext ctx, StringValues token) {
|
public async Task Handle(HttpContext ctx) {
|
||||||
var request = ctx.Request;
|
|
||||||
var response = ctx.Response;
|
var response = ctx.Response;
|
||||||
|
|
||||||
Log.Info("Request: " + request.GetDisplayUrl() + " (" + request.ContentLength + " B)");
|
|
||||||
|
|
||||||
if (token.Count != 1 || token[0] != Parameters.Token) {
|
|
||||||
Log.Error("Token: " + (token.Count == 1 ? token[0] : "<missing>"));
|
|
||||||
response.StatusCode = (int) HttpStatusCode.Forbidden;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response.StatusCode = (int) HttpStatusCode.OK;
|
response.StatusCode = (int) HttpStatusCode.OK;
|
||||||
var output = await Respond(ctx);
|
var output = await Respond(ctx);
|
||||||
@@ -49,14 +35,6 @@ abstract class BaseEndpoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleGet(HttpContext ctx) {
|
|
||||||
await Handle(ctx, ctx.Request.Query["token"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandlePost(HttpContext ctx) {
|
|
||||||
await Handle(ctx, ctx.Request.Headers["X-DHT-Token"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract Task<IHttpOutput> Respond(HttpContext ctx);
|
protected abstract Task<IHttpOutput> Respond(HttpContext ctx);
|
||||||
|
|
||||||
protected static async Task<JsonElement> ReadJson(HttpContext ctx) {
|
protected static async Task<JsonElement> ReadJson(HttpContext ctx) {
|
||||||
|
@@ -2,14 +2,13 @@ using System.Net;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Service;
|
|
||||||
using DHT.Utils.Http;
|
using DHT.Utils.Http;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace DHT.Server.Endpoints;
|
namespace DHT.Server.Endpoints;
|
||||||
|
|
||||||
sealed class GetAttachmentEndpoint : BaseEndpoint {
|
sealed class GetAttachmentEndpoint : BaseEndpoint {
|
||||||
public GetAttachmentEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
public GetAttachmentEndpoint(IDatabaseFile db) : base(db) {}
|
||||||
|
|
||||||
protected override Task<IHttpOutput> Respond(HttpContext ctx) {
|
protected override Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||||
string attachmentUrl = WebUtility.UrlDecode((string) ctx.Request.RouteValues["url"]!);
|
string attachmentUrl = WebUtility.UrlDecode((string) ctx.Request.RouteValues["url"]!);
|
||||||
|
@@ -13,12 +13,16 @@ namespace DHT.Server.Endpoints;
|
|||||||
sealed class GetTrackingScriptEndpoint : BaseEndpoint {
|
sealed class GetTrackingScriptEndpoint : BaseEndpoint {
|
||||||
private static ResourceLoader Resources { get; } = new (Assembly.GetExecutingAssembly());
|
private static ResourceLoader Resources { get; } = new (Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
public GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
private readonly ServerParameters serverParameters;
|
||||||
|
|
||||||
|
public GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db) {
|
||||||
|
serverParameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||||
string bootstrap = await Resources.ReadTextAsync("Tracker/bootstrap.js");
|
string bootstrap = await Resources.ReadTextAsync("Tracker/bootstrap.js");
|
||||||
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + Parameters.Port + ";")
|
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + serverParameters.Port + ";")
|
||||||
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(Parameters.Token))
|
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(serverParameters.Token))
|
||||||
.Replace("/*[IMPORTS]*/", await Resources.ReadJoinedAsync("Tracker/scripts/", '\n'))
|
.Replace("/*[IMPORTS]*/", await Resources.ReadJoinedAsync("Tracker/scripts/", '\n'))
|
||||||
.Replace("/*[CSS-CONTROLLER]*/", await Resources.ReadTextAsync("Tracker/styles/controller.css"))
|
.Replace("/*[CSS-CONTROLLER]*/", await Resources.ReadTextAsync("Tracker/styles/controller.css"))
|
||||||
.Replace("/*[CSS-SETTINGS]*/", await Resources.ReadTextAsync("Tracker/styles/settings.css"))
|
.Replace("/*[CSS-SETTINGS]*/", await Resources.ReadTextAsync("Tracker/styles/settings.css"))
|
||||||
|
@@ -3,14 +3,13 @@ using System.Text.Json;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Service;
|
|
||||||
using DHT.Utils.Http;
|
using DHT.Utils.Http;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace DHT.Server.Endpoints;
|
namespace DHT.Server.Endpoints;
|
||||||
|
|
||||||
sealed class TrackChannelEndpoint : BaseEndpoint {
|
sealed class TrackChannelEndpoint : BaseEndpoint {
|
||||||
public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
public TrackChannelEndpoint(IDatabaseFile db) : base(db) {}
|
||||||
|
|
||||||
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||||
var root = await ReadJson(ctx);
|
var root = await ReadJson(ctx);
|
||||||
|
@@ -9,7 +9,6 @@ using DHT.Server.Data;
|
|||||||
using DHT.Server.Data.Filters;
|
using DHT.Server.Data.Filters;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Download;
|
using DHT.Server.Download;
|
||||||
using DHT.Server.Service;
|
|
||||||
using DHT.Utils.Collections;
|
using DHT.Utils.Collections;
|
||||||
using DHT.Utils.Http;
|
using DHT.Utils.Http;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -20,7 +19,7 @@ sealed class TrackMessagesEndpoint : BaseEndpoint {
|
|||||||
private const string HasNewMessages = "1";
|
private const string HasNewMessages = "1";
|
||||||
private const string NoNewMessages = "0";
|
private const string NoNewMessages = "0";
|
||||||
|
|
||||||
public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
public TrackMessagesEndpoint(IDatabaseFile db) : base(db) {}
|
||||||
|
|
||||||
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||||
var root = await ReadJson(ctx);
|
var root = await ReadJson(ctx);
|
||||||
|
@@ -3,14 +3,13 @@ using System.Text.Json;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Service;
|
|
||||||
using DHT.Utils.Http;
|
using DHT.Utils.Http;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace DHT.Server.Endpoints;
|
namespace DHT.Server.Endpoints;
|
||||||
|
|
||||||
sealed class TrackUsersEndpoint : BaseEndpoint {
|
sealed class TrackUsersEndpoint : BaseEndpoint {
|
||||||
public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
public TrackUsersEndpoint(IDatabaseFile db) : base(db) {}
|
||||||
|
|
||||||
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||||
var root = await ReadJson(ctx);
|
var root = await ReadJson(ctx);
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace DHT.Server.Service.Middlewares;
|
||||||
|
|
||||||
|
sealed class ServerAuthorizationMiddleware {
|
||||||
|
private static readonly Log Log = Log.ForType<ServerAuthorizationMiddleware>();
|
||||||
|
|
||||||
|
private readonly RequestDelegate next;
|
||||||
|
private readonly ServerParameters serverParameters;
|
||||||
|
|
||||||
|
public ServerAuthorizationMiddleware(RequestDelegate next, ServerParameters serverParameters) {
|
||||||
|
this.next = next;
|
||||||
|
this.serverParameters = serverParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context) {
|
||||||
|
var request = context.Request;
|
||||||
|
|
||||||
|
bool success = HttpMethods.IsGet(request.Method)
|
||||||
|
? CheckToken(request.Query["token"])
|
||||||
|
: CheckToken(request.Headers["X-DHT-Token"]);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
context.Response.StatusCode = (int) HttpStatusCode.Forbidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckToken(StringValues token) {
|
||||||
|
if (token.Count == 1 && token[0] == serverParameters.Token) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.Error("Invalid token: " + (token.Count == 1 ? token[0] : "<missing>"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
app/Server/Service/Middlewares/ServerLoggingMiddleware.cs
Normal file
29
app/Server/Service/Middlewares/ServerLoggingMiddleware.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
|
||||||
|
namespace DHT.Server.Service.Middlewares;
|
||||||
|
|
||||||
|
sealed class ServerLoggingMiddleware {
|
||||||
|
private static readonly Log Log = Log.ForType<ServerLoggingMiddleware>();
|
||||||
|
|
||||||
|
private readonly RequestDelegate next;
|
||||||
|
|
||||||
|
public ServerLoggingMiddleware(RequestDelegate next) {
|
||||||
|
this.next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context) {
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
await next(context);
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
var request = context.Request;
|
||||||
|
var requestLength = request.ContentLength ?? 0L;
|
||||||
|
var responseStatus = context.Response.StatusCode;
|
||||||
|
var elapsedMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,127 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Threading;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Utils.Logging;
|
|
||||||
using Microsoft.AspNetCore;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace DHT.Server.Service;
|
|
||||||
|
|
||||||
public static class ServerLauncher {
|
|
||||||
private static readonly Log Log = Log.ForType(typeof(ServerLauncher));
|
|
||||||
|
|
||||||
private static IWebHost? Server { get; set; } = null;
|
|
||||||
|
|
||||||
public static bool IsRunning { get; private set; }
|
|
||||||
public static event EventHandler? ServerStatusChanged;
|
|
||||||
public static event EventHandler<Exception>? ServerManagementExceptionCaught;
|
|
||||||
|
|
||||||
private static Thread? ManagementThread { get; set; } = null;
|
|
||||||
private static readonly Mutex ManagementThreadLock = new();
|
|
||||||
private static readonly BlockingCollection<IMessage> Messages = new(new ConcurrentQueue<IMessage>());
|
|
||||||
|
|
||||||
private static void EnqueueMessage(IMessage message) {
|
|
||||||
ManagementThreadLock.WaitOne();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (ManagementThread == null) {
|
|
||||||
ManagementThread = new Thread(RunManagementThread) {
|
|
||||||
Name = "DHT server management thread",
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
ManagementThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
Messages.Add(message);
|
|
||||||
} finally {
|
|
||||||
ManagementThreadLock.ReleaseMutex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "FunctionNeverReturns")]
|
|
||||||
private static void RunManagementThread() {
|
|
||||||
foreach (IMessage message in Messages.GetConsumingEnumerable()) {
|
|
||||||
try {
|
|
||||||
switch (message) {
|
|
||||||
case IMessage.StartServer start:
|
|
||||||
StopServerFromManagementThread();
|
|
||||||
StartServerFromManagementThread(start.Port, start.Token, start.Db);
|
|
||||||
break;
|
|
||||||
case IMessage.StopServer:
|
|
||||||
StopServerFromManagementThread();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
ServerManagementExceptionCaught?.Invoke(null, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void StartServerFromManagementThread(ushort port, string token, IDatabaseFile db) {
|
|
||||||
Log.Info("Starting server on port " + port + "...");
|
|
||||||
|
|
||||||
void AddServices(IServiceCollection services) {
|
|
||||||
services.AddSingleton(typeof(IDatabaseFile), db);
|
|
||||||
services.AddSingleton(typeof(ServerParameters), new ServerParameters(port, token));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetKestrelOptions(KestrelServerOptions options) {
|
|
||||||
options.Limits.MaxRequestBodySize = null;
|
|
||||||
options.Limits.MinResponseDataRate = null;
|
|
||||||
options.ListenLocalhost(port, static listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Server = WebHost.CreateDefaultBuilder()
|
|
||||||
.ConfigureServices(AddServices)
|
|
||||||
.UseKestrel(SetKestrelOptions)
|
|
||||||
.UseStartup<Startup>()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
Server.Start();
|
|
||||||
|
|
||||||
Log.Info("Server started");
|
|
||||||
IsRunning = true;
|
|
||||||
ServerStatusChanged?.Invoke(null, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void StopServerFromManagementThread() {
|
|
||||||
if (Server != null) {
|
|
||||||
Log.Info("Stopping server...");
|
|
||||||
Server.StopAsync().Wait();
|
|
||||||
Server.Dispose();
|
|
||||||
Server = null;
|
|
||||||
|
|
||||||
Log.Info("Server stopped");
|
|
||||||
IsRunning = false;
|
|
||||||
ServerStatusChanged?.Invoke(null, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Relaunch(ushort port, string token, IDatabaseFile db) {
|
|
||||||
EnqueueMessage(new IMessage.StartServer(port, token, db));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Stop() {
|
|
||||||
EnqueueMessage(new IMessage.StopServer());
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface IMessage {
|
|
||||||
public sealed class StartServer : IMessage {
|
|
||||||
public ushort Port { get; }
|
|
||||||
public string Token { get; }
|
|
||||||
public IDatabaseFile Db { get; }
|
|
||||||
|
|
||||||
public StartServer(ushort port, string token, IDatabaseFile db) {
|
|
||||||
this.Port = port;
|
|
||||||
this.Token = token;
|
|
||||||
this.Db = db;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class StopServer : IMessage {}
|
|
||||||
}
|
|
||||||
}
|
|
107
app/Server/Service/ServerManager.cs
Normal file
107
app/Server/Service/ServerManager.cs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace DHT.Server.Service;
|
||||||
|
|
||||||
|
public sealed class ServerManager {
|
||||||
|
private static readonly Log Log = Log.ForType(typeof(ServerManager));
|
||||||
|
|
||||||
|
private IWebHost? server;
|
||||||
|
public bool IsRunning => server != null;
|
||||||
|
|
||||||
|
public event EventHandler<Status>? StatusChanged;
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
Starting,
|
||||||
|
Started,
|
||||||
|
Stopping,
|
||||||
|
Stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IDatabaseFile db;
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
internal ServerManager(IDatabaseFile db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Start(ushort port, string token) {
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
await StartInternal(port, token);
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop() {
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
await StopInternal();
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartInternal(ushort port, string token) {
|
||||||
|
await StopInternal();
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Starting);
|
||||||
|
|
||||||
|
void AddServices(IServiceCollection services) {
|
||||||
|
services.AddSingleton(typeof(IDatabaseFile), db);
|
||||||
|
services.AddSingleton(typeof(ServerParameters), new ServerParameters(port, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetKestrelOptions(KestrelServerOptions options) {
|
||||||
|
options.Limits.MaxRequestBodySize = null;
|
||||||
|
options.Limits.MinResponseDataRate = null;
|
||||||
|
options.ListenLocalhost(port, static listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newServer = new WebHostBuilder()
|
||||||
|
.ConfigureServices(AddServices)
|
||||||
|
.UseKestrel(SetKestrelOptions)
|
||||||
|
.UseStartup<Startup>()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Log.Info("Starting server on port " + port + "...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await newServer.StartAsync();
|
||||||
|
} catch (Exception) {
|
||||||
|
Log.Error("Server could not start");
|
||||||
|
StatusChanged?.Invoke(this, Status.Stopped);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Info("Server started");
|
||||||
|
|
||||||
|
server = newServer;
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Started);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopInternal() {
|
||||||
|
if (server == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Stopping);
|
||||||
|
|
||||||
|
Log.Info("Stopping server...");
|
||||||
|
await server.StopAsync();
|
||||||
|
Log.Info("Server stopped");
|
||||||
|
|
||||||
|
server.Dispose();
|
||||||
|
server = null;
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Stopped);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Endpoints;
|
using DHT.Server.Endpoints;
|
||||||
|
using DHT.Server.Service.Middlewares;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http.Json;
|
using Microsoft.AspNetCore.Http.Json;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -27,27 +28,23 @@ sealed class Startup {
|
|||||||
builder.WithOrigins(AllowedOrigins).AllowCredentials().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("X-DHT");
|
builder.WithOrigins(AllowedOrigins).AllowCredentials().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("X-DHT");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddRoutingCore();
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime, IDatabaseFile db, ServerParameters parameters) {
|
public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime, IDatabaseFile db, ServerParameters parameters) {
|
||||||
app.UseRouting();
|
app.UseMiddleware<ServerLoggingMiddleware>();
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
|
app.UseMiddleware<ServerAuthorizationMiddleware>();
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
app.UseEndpoints(endpoints => {
|
app.UseEndpoints(endpoints => {
|
||||||
GetTrackingScriptEndpoint getTrackingScript = new (db, parameters);
|
endpoints.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(db, parameters).Handle);
|
||||||
endpoints.MapGet("/get-tracking-script", context => getTrackingScript.HandleGet(context));
|
endpoints.MapGet("/get-attachment/{url}", new GetAttachmentEndpoint(db).Handle);
|
||||||
|
endpoints.MapPost("/track-channel", new TrackChannelEndpoint(db).Handle);
|
||||||
TrackChannelEndpoint trackChannel = new (db, parameters);
|
endpoints.MapPost("/track-users", new TrackUsersEndpoint(db).Handle);
|
||||||
endpoints.MapPost("/track-channel", context => trackChannel.HandlePost(context));
|
endpoints.MapPost("/track-messages", new TrackMessagesEndpoint(db).Handle);
|
||||||
|
|
||||||
TrackUsersEndpoint trackUsers = new (db, parameters);
|
|
||||||
endpoints.MapPost("/track-users", context => trackUsers.HandlePost(context));
|
|
||||||
|
|
||||||
TrackMessagesEndpoint trackMessages = new (db, parameters);
|
|
||||||
endpoints.MapPost("/track-messages", context => trackMessages.HandlePost(context));
|
|
||||||
|
|
||||||
GetAttachmentEndpoint getAttachment = new (db, parameters);
|
|
||||||
endpoints.MapGet("/get-attachment/{url}", context => getAttachment.HandleGet(context));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
app/Server/State.cs
Normal file
27
app/Server/State.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Download;
|
||||||
|
using DHT.Server.Service;
|
||||||
|
|
||||||
|
namespace DHT.Server;
|
||||||
|
|
||||||
|
public sealed class State : IAsyncDisposable {
|
||||||
|
public static State Dummy { get; } = new (DummyDatabaseFile.Instance);
|
||||||
|
|
||||||
|
public IDatabaseFile Db { get; }
|
||||||
|
public Downloader Downloader { get; }
|
||||||
|
public ServerManager Server { get; }
|
||||||
|
|
||||||
|
public State(IDatabaseFile db) {
|
||||||
|
Db = db;
|
||||||
|
Downloader = new Downloader(db);
|
||||||
|
Server = new ServerManager(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
await Downloader.Stop();
|
||||||
|
await Server.Stop();
|
||||||
|
Db.Dispose();
|
||||||
|
}
|
||||||
|
}
|
13
app/Utils/Logging/WindowsConsole.cs
Normal file
13
app/Utils/Logging/WindowsConsole.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
|
namespace DHT.Utils.Logging;
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public static partial class WindowsConsole {
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static partial void AllocConsole();
|
||||||
|
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static partial void FreeConsole();
|
||||||
|
}
|
12
app/Utils/Tasks/TaskExtensions.cs
Normal file
12
app/Utils/Tasks/TaskExtensions.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DHT.Utils.Tasks;
|
||||||
|
|
||||||
|
public static class TaskExtensions {
|
||||||
|
public static async Task WaitIgnoringCancellation(this Task task) {
|
||||||
|
try {
|
||||||
|
await task;
|
||||||
|
} catch (OperationCanceledException) {}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,6 +6,10 @@
|
|||||||
<PackageId>DiscordHistoryTrackerUtils</PackageId>
|
<PackageId>DiscordHistoryTrackerUtils</PackageId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
Reference in New Issue
Block a user