diff --git a/app/Desktop/Main/Pages/ViewerPageModel.cs b/app/Desktop/Main/Pages/ViewerPageModel.cs
index 421380d..b022f88 100644
--- a/app/Desktop/Main/Pages/ViewerPageModel.cs
+++ b/app/Desktop/Main/Pages/ViewerPageModel.cs
@@ -11,9 +11,11 @@ using Avalonia.Controls;
using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Main.Controls;
+using DHT.Desktop.Server;
using DHT.Server.Data.Filters;
using DHT.Server.Database;
using DHT.Server.Database.Export;
+using DHT.Server.Database.Export.Strategy;
using DHT.Utils.Models;
using static DHT.Desktop.Program;
@@ -55,7 +57,7 @@ namespace DHT.Desktop.Main.Pages {
HasFilters = FilterModel.HasAnyFilters;
}
- private async Task WriteViewerFile(string path) {
+ private async Task WriteViewerFile(string path, IViewerExportStrategy strategy) {
const string ArchiveTag = "/*[ARCHIVE]*/";
string indexFile = await Resources.ReadTextAsync("Viewer/index.html");
@@ -68,7 +70,7 @@ namespace DHT.Desktop.Main.Pages {
string jsonTempFile = path + ".tmp";
await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) {
- await ViewerJsonExport.Generate(jsonStream, db, FilterModel.CreateFilter());
+ await ViewerJsonExport.Generate(jsonStream, strategy, db, FilterModel.CreateFilter());
char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)];
jsonStream.Position = 0;
@@ -106,7 +108,7 @@ namespace DHT.Desktop.Main.Pages {
TemporaryFiles.Add(fullPath);
Directory.CreateDirectory(rootPath);
- await WriteViewerFile(fullPath);
+ await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerManager.Port, ServerManager.Token));
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
}
@@ -126,7 +128,7 @@ namespace DHT.Desktop.Main.Pages {
string? path = await dialog;
if (!string.IsNullOrEmpty(path)) {
- await WriteViewerFile(path);
+ await WriteViewerFile(path, StandaloneViewerExportStrategy.Instance);
}
}
diff --git a/app/Resources/Viewer/scripts/discord.js b/app/Resources/Viewer/scripts/discord.js
index fe05813..9952d0d 100644
--- a/app/Resources/Viewer/scripts/discord.js
+++ b/app/Resources/Viewer/scripts/discord.js
@@ -129,7 +129,7 @@ const DISCORD = (function() {
// noinspection HtmlUnknownTarget
templateAttachmentDownload = new TEMPLATE([
- "Download {filename}"
+ "Download {name}"
].join(""));
// noinspection HtmlUnknownTarget
@@ -240,19 +240,11 @@ const DISCORD = (function() {
}
return value.map(attachment => {
- const url = DOM.tryParseUrl(attachment.url);
-
- if (url != null && isImageUrl(url) && SETTINGS.enableImagePreviews) {
+ if (DISCORD.isImageAttachment(attachment) && SETTINGS.enableImagePreviews) {
return templateEmbedImage.apply({ url: attachment.url, src: attachment.url });
}
else {
- const path = url == null ? attachment.url : url.pathname;
- const sliced = path.split("/");
-
- return templateAttachmentDownload.apply({
- "url": attachment.url,
- "filename": sliced[sliced.length - 1]
- });
+ return templateAttachmentDownload.apply(attachment);
}
}).join("");
}
diff --git a/app/Server/Data/DownloadedAttachment.cs b/app/Server/Data/DownloadedAttachment.cs
new file mode 100644
index 0000000..97fe56f
--- /dev/null
+++ b/app/Server/Data/DownloadedAttachment.cs
@@ -0,0 +1,6 @@
+namespace DHT.Server.Data {
+ public readonly struct DownloadedAttachment {
+ public string? Type { get; internal init; }
+ public byte[] Data { get; internal init; }
+ }
+}
diff --git a/app/Server/Database/DummyDatabaseFile.cs b/app/Server/Database/DummyDatabaseFile.cs
index ca5affd..17602a4 100644
--- a/app/Server/Database/DummyDatabaseFile.cs
+++ b/app/Server/Database/DummyDatabaseFile.cs
@@ -63,6 +63,10 @@ namespace DHT.Server.Database {
return download;
}
+ public DownloadedAttachment? GetDownloadedAttachment(string url) {
+ return null;
+ }
+
public void AddDownload(Data.Download download) {}
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}
diff --git a/app/Server/Database/Export/Strategy/IViewerExportStrategy.cs b/app/Server/Database/Export/Strategy/IViewerExportStrategy.cs
new file mode 100644
index 0000000..30f93f1
--- /dev/null
+++ b/app/Server/Database/Export/Strategy/IViewerExportStrategy.cs
@@ -0,0 +1,7 @@
+using DHT.Server.Data;
+
+namespace DHT.Server.Database.Export.Strategy {
+ public interface IViewerExportStrategy {
+ string GetAttachmentUrl(Attachment attachment);
+ }
+}
diff --git a/app/Server/Database/Export/Strategy/LiveViewerExportStrategy.cs b/app/Server/Database/Export/Strategy/LiveViewerExportStrategy.cs
new file mode 100644
index 0000000..6f1d2d9
--- /dev/null
+++ b/app/Server/Database/Export/Strategy/LiveViewerExportStrategy.cs
@@ -0,0 +1,18 @@
+using System.Net;
+using DHT.Server.Data;
+
+namespace DHT.Server.Database.Export.Strategy {
+ public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
+ private readonly string safePort;
+ private readonly string safeToken;
+
+ public LiveViewerExportStrategy(ushort port, string token) {
+ this.safePort = port.ToString();
+ this.safeToken = WebUtility.UrlEncode(token);
+ }
+
+ public string GetAttachmentUrl(Attachment attachment) {
+ return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.Url) + "?token=" + safeToken;
+ }
+ }
+}
diff --git a/app/Server/Database/Export/Strategy/StandaloneViewerExportStrategy.cs b/app/Server/Database/Export/Strategy/StandaloneViewerExportStrategy.cs
new file mode 100644
index 0000000..a34ac84
--- /dev/null
+++ b/app/Server/Database/Export/Strategy/StandaloneViewerExportStrategy.cs
@@ -0,0 +1,13 @@
+using DHT.Server.Data;
+
+namespace DHT.Server.Database.Export.Strategy {
+ public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
+ public static StandaloneViewerExportStrategy Instance { get; } = new ();
+
+ private StandaloneViewerExportStrategy() {}
+
+ public string GetAttachmentUrl(Attachment attachment) {
+ return attachment.Url;
+ }
+ }
+}
diff --git a/app/Server/Database/Export/ViewerJsonExport.cs b/app/Server/Database/Export/ViewerJsonExport.cs
index 69d556c..ce0b525 100644
--- a/app/Server/Database/Export/ViewerJsonExport.cs
+++ b/app/Server/Database/Export/ViewerJsonExport.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -5,13 +6,14 @@ using System.Text.Json;
using System.Threading.Tasks;
using DHT.Server.Data;
using DHT.Server.Data.Filters;
+using DHT.Server.Database.Export.Strategy;
using DHT.Utils.Logging;
namespace DHT.Server.Database.Export {
public static class ViewerJsonExport {
private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport));
- public static async Task Generate(Stream stream, IDatabaseFile db, MessageFilter? filter = null) {
+ public static async Task Generate(Stream stream, IViewerExportStrategy strategy, IDatabaseFile db, MessageFilter? filter = null) {
var perf = Log.Start();
var includedUserIds = new HashSet();
@@ -41,7 +43,7 @@ namespace DHT.Server.Database.Export {
var value = new {
meta = new { users, userindex, servers, channels },
- data = GenerateMessageList(includedMessages, userIndices)
+ data = GenerateMessageList(includedMessages, userIndices, strategy)
};
perf.Step("Generate value object");
@@ -138,7 +140,7 @@ namespace DHT.Server.Database.Export {
return channels;
}
- private static object GenerateMessageList(List includedMessages, Dictionary userIndices) {
+ private static object GenerateMessageList( List includedMessages, Dictionary userIndices, IViewerExportStrategy strategy) {
var data = new Dictionary>();
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
@@ -164,8 +166,9 @@ namespace DHT.Server.Database.Export {
}
if (!message.Attachments.IsEmpty) {
- obj["a"] = message.Attachments.Select(static attachment => new Dictionary {
- { "url", attachment.Url }
+ obj["a"] = message.Attachments.Select(attachment => new Dictionary {
+ { "url", strategy.GetAttachmentUrl(attachment) },
+ { "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url }
}).ToArray();
}
diff --git a/app/Server/Database/IDatabaseFile.cs b/app/Server/Database/IDatabaseFile.cs
index 1fb3ed8..dba7658 100644
--- a/app/Server/Database/IDatabaseFile.cs
+++ b/app/Server/Database/IDatabaseFile.cs
@@ -31,7 +31,8 @@ namespace DHT.Server.Database {
void AddDownload(Data.Download download);
List GetDownloadsWithoutData();
Data.Download GetDownloadWithData(Data.Download download);
-
+ DownloadedAttachment? GetDownloadedAttachment(string url);
+
void EnqueueDownloadItems(AttachmentFilter? filter = null);
List GetEnqueuedDownloadItems(int count);
void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode);
diff --git a/app/Server/Database/Sqlite/SqliteDatabaseFile.cs b/app/Server/Database/Sqlite/SqliteDatabaseFile.cs
index 87d25ea..d64869f 100644
--- a/app/Server/Database/Sqlite/SqliteDatabaseFile.cs
+++ b/app/Server/Database/Sqlite/SqliteDatabaseFile.cs
@@ -470,6 +470,28 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
}
}
+ public DownloadedAttachment? GetDownloadedAttachment(string url) {
+ using var conn = pool.Take();
+ using var cmd = conn.Command(@"
+SELECT a.type, d.blob FROM downloads d
+LEFT JOIN attachments a ON d.url = a.url
+WHERE d.url = :url AND d.status = :success AND d.blob IS NOT NULL");
+
+ cmd.AddAndSet(":url", SqliteType.Text, url);
+ cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
+
+ using var reader = cmd.ExecuteReader();
+
+ if (!reader.Read()) {
+ return null;
+ }
+
+ return new DownloadedAttachment {
+ Type = reader.IsDBNull(0) ? null : reader.GetString(0),
+ Data = (byte[]) reader["blob"]
+ };
+ }
+
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {
using var conn = pool.Take();
using var cmd = conn.Command("INSERT INTO downloads (url, status, size) SELECT a.url, :enqueued, MAX(a.size) FROM attachments a" + filter.GenerateWhereClause("a") + " GROUP BY a.url");
diff --git a/app/Server/Endpoints/BaseEndpoint.cs b/app/Server/Endpoints/BaseEndpoint.cs
index f0dacd3..115ca60 100644
--- a/app/Server/Endpoints/BaseEndpoint.cs
+++ b/app/Server/Endpoints/BaseEndpoint.cs
@@ -8,6 +8,7 @@ using DHT.Utils.Http;
using DHT.Utils.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Primitives;
namespace DHT.Server.Endpoints {
abstract class BaseEndpoint {
@@ -21,26 +22,22 @@ namespace DHT.Server.Endpoints {
this.parameters = parameters;
}
- public async Task Handle(HttpContext ctx) {
+ private async Task Handle(HttpContext ctx, StringValues token) {
var request = ctx.Request;
var response = ctx.Response;
Log.Info("Request: " + request.GetDisplayUrl() + " (" + request.ContentLength + " B)");
-
- var requestToken = request.Headers["X-DHT-Token"];
- if (requestToken.Count != 1 || requestToken[0] != parameters.Token) {
- Log.Error("Token: " + (requestToken.Count == 1 ? requestToken[0] : ""));
+
+ if (token.Count != 1 || token[0] != parameters.Token) {
+ Log.Error("Token: " + (token.Count == 1 ? token[0] : ""));
response.StatusCode = (int) HttpStatusCode.Forbidden;
return;
}
try {
- var (statusCode, output) = await Respond(ctx);
- response.StatusCode = (int) statusCode;
-
- if (output != null) {
- await response.WriteAsJsonAsync(output);
- }
+ response.StatusCode = (int) HttpStatusCode.OK;
+ var output = await Respond(ctx);
+ await output.WriteTo(response);
} catch (HttpException e) {
Log.Error(e);
response.StatusCode = (int) e.StatusCode;
@@ -51,7 +48,15 @@ namespace DHT.Server.Endpoints {
}
}
- protected abstract Task<(HttpStatusCode, object?)> Respond(HttpContext ctx);
+ 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 Respond(HttpContext ctx);
protected static async Task ReadJson(HttpContext ctx) {
return await ctx.Request.ReadFromJsonAsync() ?? throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON.");
diff --git a/app/Server/Endpoints/GetAttachmentEndpoint.cs b/app/Server/Endpoints/GetAttachmentEndpoint.cs
new file mode 100644
index 0000000..16d6763
--- /dev/null
+++ b/app/Server/Endpoints/GetAttachmentEndpoint.cs
@@ -0,0 +1,25 @@
+using System.Net;
+using System.Threading.Tasks;
+using DHT.Server.Data;
+using DHT.Server.Database;
+using DHT.Server.Service;
+using DHT.Utils.Http;
+using Microsoft.AspNetCore.Http;
+
+namespace DHT.Server.Endpoints {
+ sealed class GetAttachmentEndpoint : BaseEndpoint {
+ public GetAttachmentEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
+
+ protected override Task Respond(HttpContext ctx) {
+ string attachmentUrl = WebUtility.UrlDecode((string) ctx.Request.RouteValues["url"]!);
+ DownloadedAttachment? maybeDownloadedAttachment = Db.GetDownloadedAttachment(attachmentUrl);
+
+ if (maybeDownloadedAttachment is {} downloadedAttachment) {
+ return Task.FromResult(new HttpOutput.File(downloadedAttachment.Type, downloadedAttachment.Data));
+ }
+ else {
+ return Task.FromResult(new HttpOutput.Redirect(attachmentUrl, permanent: false));
+ }
+ }
+ }
+}
diff --git a/app/Server/Endpoints/TrackChannelEndpoint.cs b/app/Server/Endpoints/TrackChannelEndpoint.cs
index b44dac2..c01bd7e 100644
--- a/app/Server/Endpoints/TrackChannelEndpoint.cs
+++ b/app/Server/Endpoints/TrackChannelEndpoint.cs
@@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints {
sealed class TrackChannelEndpoint : BaseEndpoint {
public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
- protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
+ protected override async Task Respond(HttpContext ctx) {
var root = await ReadJson(ctx);
var server = ReadServer(root.RequireObject("server"), "server");
var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id);
@@ -19,7 +19,7 @@ namespace DHT.Server.Endpoints {
Db.AddServer(server);
Db.AddChannel(channel);
- return (HttpStatusCode.OK, null);
+ return HttpOutput.None;
}
private static Data.Server ReadServer(JsonElement json, string path) => new() {
diff --git a/app/Server/Endpoints/TrackMessagesEndpoint.cs b/app/Server/Endpoints/TrackMessagesEndpoint.cs
index 91b1cd3..6bc2d63 100644
--- a/app/Server/Endpoints/TrackMessagesEndpoint.cs
+++ b/app/Server/Endpoints/TrackMessagesEndpoint.cs
@@ -17,7 +17,7 @@ namespace DHT.Server.Endpoints {
sealed class TrackMessagesEndpoint : BaseEndpoint {
public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
- protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
+ protected override async Task Respond(HttpContext ctx) {
var root = await ReadJson(ctx);
if (root.ValueKind != JsonValueKind.Array) {
@@ -39,7 +39,7 @@ namespace DHT.Server.Endpoints {
Db.AddMessages(messages);
- return (HttpStatusCode.OK, anyNewMessages ? 1 : 0);
+ return new HttpOutput.Json(anyNewMessages ? 1 : 0);
}
private static Message ReadMessage(JsonElement json, string path) => new() {
diff --git a/app/Server/Endpoints/TrackUsersEndpoint.cs b/app/Server/Endpoints/TrackUsersEndpoint.cs
index 2e8070e..02afb76 100644
--- a/app/Server/Endpoints/TrackUsersEndpoint.cs
+++ b/app/Server/Endpoints/TrackUsersEndpoint.cs
@@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints {
sealed class TrackUsersEndpoint : BaseEndpoint {
public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
- protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
+ protected override async Task Respond(HttpContext ctx) {
var root = await ReadJson(ctx);
if (root.ValueKind != JsonValueKind.Array) {
@@ -27,7 +27,7 @@ namespace DHT.Server.Endpoints {
Db.AddUsers(users);
- return (HttpStatusCode.OK, null);
+ return HttpOutput.None;
}
private static User ReadUser(JsonElement json, string path) => new() {
diff --git a/app/Server/Service/ServerStartup.cs b/app/Server/Service/ServerStartup.cs
index 3a1b1f2..73f6f26 100644
--- a/app/Server/Service/ServerStartup.cs
+++ b/app/Server/Service/ServerStartup.cs
@@ -34,13 +34,16 @@ namespace DHT.Server.Service {
app.UseCors();
app.UseEndpoints(endpoints => {
TrackChannelEndpoint trackChannel = new(db, parameters);
- endpoints.MapPost("/track-channel", async context => await trackChannel.Handle(context));
+ endpoints.MapPost("/track-channel", async context => await trackChannel.HandlePost(context));
TrackUsersEndpoint trackUsers = new(db, parameters);
- endpoints.MapPost("/track-users", async context => await trackUsers.Handle(context));
+ endpoints.MapPost("/track-users", async context => await trackUsers.HandlePost(context));
TrackMessagesEndpoint trackMessages = new(db, parameters);
- endpoints.MapPost("/track-messages", async context => await trackMessages.Handle(context));
+ endpoints.MapPost("/track-messages", async context => await trackMessages.HandlePost(context));
+
+ GetAttachmentEndpoint getAttachment = new(db, parameters);
+ endpoints.MapGet("/get-attachment/{url}", async context => await getAttachment.HandleGet(context));
});
}
}
diff --git a/app/Utils/Http/HttpOutput.cs b/app/Utils/Http/HttpOutput.cs
new file mode 100644
index 0000000..9f403f4
--- /dev/null
+++ b/app/Utils/Http/HttpOutput.cs
@@ -0,0 +1,56 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace DHT.Utils.Http {
+ public static class HttpOutput {
+ public static IHttpOutput None { get; } = new NoneImpl();
+
+ private sealed class NoneImpl : IHttpOutput {
+ public Task WriteTo(HttpResponse response) {
+ return Task.CompletedTask;
+ }
+ }
+
+ public sealed class Json : IHttpOutput {
+ private readonly object? obj;
+
+ public Json(object? obj) {
+ this.obj = obj;
+ }
+
+ public Task WriteTo(HttpResponse response) {
+ return response.WriteAsJsonAsync(obj);
+ }
+ }
+
+ public sealed class File : IHttpOutput {
+ private readonly string? contentType;
+ private readonly byte[] bytes;
+
+ public File(string? contentType, byte[] bytes) {
+ this.contentType = contentType;
+ this.bytes = bytes;
+ }
+
+ public async Task WriteTo(HttpResponse response) {
+ response.ContentType = contentType ?? string.Empty;
+ await response.Body.WriteAsync(bytes);
+ }
+ }
+
+ public sealed class Redirect : IHttpOutput {
+ private readonly string url;
+ private readonly bool permanent;
+
+ public Redirect(string url, bool permanent) {
+ this.url = url;
+ this.permanent = permanent;
+ }
+
+ public Task WriteTo(HttpResponse response) {
+ response.Redirect(url, permanent);
+ return Task.CompletedTask;
+ }
+ }
+ }
+}
diff --git a/app/Utils/Http/IHttpOutput.cs b/app/Utils/Http/IHttpOutput.cs
new file mode 100644
index 0000000..039980a
--- /dev/null
+++ b/app/Utils/Http/IHttpOutput.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace DHT.Utils.Http {
+ public interface IHttpOutput {
+ Task WriteTo(HttpResponse response);
+ }
+}
diff --git a/app/Utils/Utils.csproj b/app/Utils/Utils.csproj
index 60f260e..d55e716 100644
--- a/app/Utils/Utils.csproj
+++ b/app/Utils/Utils.csproj
@@ -16,6 +16,9 @@
true
none
+
+
+