mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2024-11-25 14:42:44 +01:00
Compare commits
No commits in common. "d4d14cab97c68604e6690a2322787ff3ca2f2b5c" and "d01f9ed2186b83ed5ad58da1376059138600b946" have entirely different histories.
d4d14cab97
...
d01f9ed218
@ -29,6 +29,9 @@
|
||||
<H2CodeStyleSettings version="6">
|
||||
<option name="USE_GENERIC_STYLE" value="true" />
|
||||
</H2CodeStyleSettings>
|
||||
<H2CodeStyleSettings version="6">
|
||||
<option name="USE_GENERIC_STYLE" value="true" />
|
||||
</H2CodeStyleSettings>
|
||||
<HSQLCodeStyleSettings version="6">
|
||||
<option name="USE_GENERIC_STYLE" value="true" />
|
||||
</HSQLCodeStyleSettings>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Desktop" type="DotNetProject" factoryName=".NET Project">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Desktop/debug/DiscordHistoryTracker.exe" />
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/Desktop/bin/Debug/net5.0/DiscordHistoryTracker.exe" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.artifacts/bin/Desktop/debug" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Desktop/bin/Debug/net5.0" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
@ -12,7 +12,7 @@
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net5.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@ -0,0 +1,23 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Minify" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="rider.module" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="false" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="false" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/minify.py" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
@ -4,8 +4,7 @@ public readonly struct Attachment {
|
||||
public ulong Id { get; internal init; }
|
||||
public string Name { get; internal init; }
|
||||
public string? Type { get; internal init; }
|
||||
public string NormalizedUrl { get; internal init; }
|
||||
public string DownloadUrl { get; internal init; }
|
||||
public string Url { get; internal init; }
|
||||
public ulong Size { get; internal init; }
|
||||
public int? Width { get; internal init; }
|
||||
public int? Height { get; internal init; }
|
||||
|
@ -1,33 +1,30 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using DHT.Server.Download;
|
||||
|
||||
namespace DHT.Server.Data;
|
||||
|
||||
public readonly struct Download {
|
||||
internal static Download NewSuccess(DownloadItem item, byte[] data) {
|
||||
return new Download(item.NormalizedUrl, item.DownloadUrl, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data);
|
||||
internal static Download NewSuccess(string url, byte[] data) {
|
||||
return new Download(url, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data);
|
||||
}
|
||||
|
||||
internal static Download NewFailure(DownloadItem item, HttpStatusCode? statusCode, ulong size) {
|
||||
return new Download(item.NormalizedUrl, item.DownloadUrl, statusCode.HasValue ? (DownloadStatus) (int) statusCode : DownloadStatus.GenericError, size);
|
||||
internal static Download NewFailure(string url, HttpStatusCode? statusCode, ulong size) {
|
||||
return new Download(url, statusCode.HasValue ? (DownloadStatus) (int) statusCode : DownloadStatus.GenericError, size);
|
||||
}
|
||||
|
||||
public string NormalizedUrl { get; }
|
||||
public string DownloadUrl { get; }
|
||||
public string Url { get; }
|
||||
public DownloadStatus Status { get; }
|
||||
public ulong Size { get; }
|
||||
public byte[]? Data { get; }
|
||||
|
||||
internal Download(string normalizedUrl, string downloadUrl, DownloadStatus status, ulong size, byte[]? data = null) {
|
||||
NormalizedUrl = normalizedUrl;
|
||||
DownloadUrl = downloadUrl;
|
||||
internal Download(string url, DownloadStatus status, ulong size, byte[]? data = null) {
|
||||
Url = url;
|
||||
Status = status;
|
||||
Size = size;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
internal Download WithData(byte[] data) {
|
||||
return new Download(NormalizedUrl, DownloadUrl, Status, Size, data);
|
||||
return new Download(Url, Status, Size, data);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,6 @@ public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
|
||||
}
|
||||
|
||||
public string GetAttachmentUrl(Attachment attachment) {
|
||||
return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.NormalizedUrl) + "?token=" + safeToken;
|
||||
return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.Url) + "?token=" + safeToken;
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,6 @@ public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
|
||||
private StandaloneViewerExportStrategy() {}
|
||||
|
||||
public string GetAttachmentUrl(Attachment attachment) {
|
||||
// The normalized URL will not load files from Discord CDN once the time limit is enforced.
|
||||
|
||||
// The downloaded URL would work, but only for a limited time, so it is better for the links to not work
|
||||
// rather than give users a false sense of security.
|
||||
|
||||
return attachment.NormalizedUrl;
|
||||
return attachment.Url;
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ public static class ViewerJsonExport {
|
||||
obj["a"] = message.Attachments.Select(attachment => {
|
||||
var a = new Dictionary<string, object> {
|
||||
{ "url", strategy.GetAttachmentUrl(attachment) },
|
||||
{ "name", Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl },
|
||||
{ "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url },
|
||||
};
|
||||
|
||||
if (attachment is { Width: not null, Height: not null }) {
|
||||
|
@ -197,8 +197,7 @@ public static class LegacyArchiveImport {
|
||||
Id = fakeSnowflake.Next(),
|
||||
Name = name,
|
||||
Type = type,
|
||||
NormalizedUrl = url,
|
||||
DownloadUrl = url,
|
||||
Url = url,
|
||||
Size = 0, // unknown size
|
||||
};
|
||||
}).DistinctByKeyStable(static attachment => {
|
||||
|
@ -1,16 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database.Exceptions;
|
||||
using DHT.Server.Database.Sqlite.Utils;
|
||||
using DHT.Server.Download;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite;
|
||||
|
||||
sealed class Schema {
|
||||
internal const int Version = 6;
|
||||
internal const int Version = 5;
|
||||
|
||||
private static readonly Log Log = Log.ForType<Schema>();
|
||||
|
||||
@ -50,88 +47,57 @@ sealed class Schema {
|
||||
}
|
||||
|
||||
private void InitializeSchemas() {
|
||||
Execute("""
|
||||
CREATE TABLE users (
|
||||
Execute(@"CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
avatar_url TEXT,
|
||||
discriminator TEXT
|
||||
)
|
||||
""");
|
||||
discriminator TEXT)");
|
||||
|
||||
Execute("""
|
||||
CREATE TABLE servers (
|
||||
Execute(@"CREATE TABLE servers (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
)
|
||||
""");
|
||||
type TEXT NOT NULL)");
|
||||
|
||||
Execute("""
|
||||
CREATE TABLE channels (
|
||||
Execute(@"CREATE TABLE channels (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
server INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
parent_id INTEGER,
|
||||
position INTEGER,
|
||||
topic TEXT,
|
||||
nsfw INTEGER
|
||||
)
|
||||
""");
|
||||
nsfw INTEGER)");
|
||||
|
||||
Execute("""
|
||||
CREATE TABLE messages (
|
||||
Execute(@"CREATE TABLE messages (
|
||||
message_id INTEGER PRIMARY KEY NOT NULL,
|
||||
sender_id INTEGER NOT NULL,
|
||||
channel_id INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)
|
||||
""");
|
||||
timestamp INTEGER NOT NULL)");
|
||||
|
||||
Execute("""
|
||||
CREATE TABLE attachments (
|
||||
Execute(@"CREATE TABLE attachments (
|
||||
message_id INTEGER NOT NULL,
|
||||
attachment_id INTEGER NOT NULL PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT,
|
||||
normalized_url TEXT NOT NULL,
|
||||
download_url TEXT,
|
||||
url TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
width INTEGER,
|
||||
height INTEGER
|
||||
)
|
||||
""");
|
||||
height INTEGER)");
|
||||
|
||||
Execute("""
|
||||
CREATE TABLE embeds (
|
||||
Execute(@"CREATE TABLE embeds (
|
||||
message_id INTEGER NOT NULL,
|
||||
json TEXT NOT NULL
|
||||
)
|
||||
""");
|
||||
json TEXT NOT NULL)");
|
||||
|
||||
Execute("""
|
||||
CREATE TABLE downloads (
|
||||
normalized_url TEXT NOT NULL PRIMARY KEY,
|
||||
download_url TEXT,
|
||||
status INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
blob BLOB
|
||||
)
|
||||
""");
|
||||
|
||||
Execute("""
|
||||
CREATE TABLE reactions (
|
||||
Execute(@"CREATE TABLE reactions (
|
||||
message_id INTEGER NOT NULL,
|
||||
emoji_id INTEGER,
|
||||
emoji_name TEXT,
|
||||
emoji_flags INTEGER NOT NULL,
|
||||
count INTEGER NOT NULL
|
||||
)
|
||||
""");
|
||||
count INTEGER NOT NULL)");
|
||||
|
||||
CreateMessageEditTimestampTable();
|
||||
CreateMessageRepliedToTable();
|
||||
CreateDownloadsTable();
|
||||
|
||||
Execute("CREATE INDEX attachments_message_ix ON attachments(message_id)");
|
||||
Execute("CREATE INDEX embeds_message_ix ON embeds(message_id)");
|
||||
@ -141,90 +107,23 @@ sealed class Schema {
|
||||
}
|
||||
|
||||
private void CreateMessageEditTimestampTable() {
|
||||
Execute("""
|
||||
CREATE TABLE edit_timestamps (
|
||||
Execute(@"CREATE TABLE edit_timestamps (
|
||||
message_id INTEGER PRIMARY KEY NOT NULL,
|
||||
edit_timestamp INTEGER NOT NULL
|
||||
)
|
||||
""");
|
||||
edit_timestamp INTEGER NOT NULL)");
|
||||
}
|
||||
|
||||
private void CreateMessageRepliedToTable() {
|
||||
Execute("""
|
||||
CREATE TABLE replied_to (
|
||||
Execute(@"CREATE TABLE replied_to (
|
||||
message_id INTEGER PRIMARY KEY NOT NULL,
|
||||
replied_to_id INTEGER NOT NULL
|
||||
)
|
||||
""");
|
||||
replied_to_id INTEGER NOT NULL)");
|
||||
}
|
||||
|
||||
private void NormalizeAttachmentUrls() {
|
||||
var normalizedUrls = new Dictionary<long, string>();
|
||||
|
||||
using (var selectCmd = conn.Command("SELECT attachment_id, url FROM attachments")) {
|
||||
using var reader = selectCmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
var attachmentId = reader.GetInt64(0);
|
||||
var originalUrl = reader.GetString(1);
|
||||
normalizedUrls[attachmentId] = DiscordCdn.NormalizeUrl(originalUrl);
|
||||
}
|
||||
}
|
||||
|
||||
using var tx = conn.BeginTransaction();
|
||||
|
||||
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.Parameters.Add(":normalized_url", SqliteType.Text);
|
||||
|
||||
foreach (var (attachmentId, normalizedUrl) in normalizedUrls) {
|
||||
updateCmd.Set(":attachment_id", attachmentId);
|
||||
updateCmd.Set(":normalized_url", normalizedUrl);
|
||||
updateCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
private void NormalizeDownloadUrls() {
|
||||
var normalizedUrlsToOriginalUrls = new Dictionary<string, string>();
|
||||
var duplicateUrlsToDelete = new HashSet<string>();
|
||||
|
||||
using (var selectCmd = conn.Command("SELECT url FROM downloads ORDER BY CASE WHEN status = 200 THEN 0 ELSE 1 END")) {
|
||||
using var reader = selectCmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
var originalUrl = reader.GetString(0);
|
||||
var normalizedUrl = DiscordCdn.NormalizeUrl(originalUrl);
|
||||
|
||||
if (!normalizedUrlsToOriginalUrls.TryAdd(normalizedUrl, originalUrl)) {
|
||||
duplicateUrlsToDelete.Add(originalUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using var tx = conn.BeginTransaction();
|
||||
|
||||
using (var deleteCmd = conn.Delete("downloads", ("url", SqliteType.Text))) {
|
||||
foreach (var duplicateUrl in duplicateUrlsToDelete) {
|
||||
deleteCmd.Set(":url", duplicateUrl);
|
||||
deleteCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
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.Parameters.Add(":download_url", SqliteType.Text);
|
||||
|
||||
foreach (var (normalizedUrl, downloadUrl) in normalizedUrlsToOriginalUrls) {
|
||||
updateCmd.Set(":normalized_url", normalizedUrl);
|
||||
updateCmd.Set(":download_url", downloadUrl);
|
||||
updateCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
private void CreateDownloadsTable() {
|
||||
Execute(@"CREATE TABLE downloads (
|
||||
url TEXT NOT NULL PRIMARY KEY,
|
||||
status INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
blob BLOB)");
|
||||
}
|
||||
|
||||
private void UpgradeSchemas(int dbVersion) {
|
||||
@ -241,19 +140,13 @@ sealed class Schema {
|
||||
CreateMessageEditTimestampTable();
|
||||
CreateMessageRepliedToTable();
|
||||
|
||||
Execute("""
|
||||
INSERT INTO edit_timestamps (message_id, edit_timestamp)
|
||||
SELECT message_id, edit_timestamp
|
||||
FROM messages
|
||||
WHERE edit_timestamp IS NOT NULL
|
||||
""");
|
||||
Execute(@"INSERT INTO edit_timestamps (message_id, edit_timestamp)
|
||||
SELECT message_id, edit_timestamp FROM messages
|
||||
WHERE edit_timestamp IS NOT NULL");
|
||||
|
||||
Execute("""
|
||||
INSERT INTO replied_to (message_id, replied_to_id)
|
||||
SELECT message_id, replied_to_id
|
||||
FROM messages
|
||||
WHERE replied_to_id IS NOT NULL
|
||||
""");
|
||||
Execute(@"INSERT INTO replied_to (message_id, replied_to_id)
|
||||
SELECT message_id, replied_to_id FROM messages
|
||||
WHERE replied_to_id IS NOT NULL");
|
||||
|
||||
Execute("ALTER TABLE messages DROP COLUMN replied_to_id");
|
||||
Execute("ALTER TABLE messages DROP COLUMN edit_timestamp");
|
||||
@ -265,15 +158,7 @@ sealed class Schema {
|
||||
}
|
||||
|
||||
if (dbVersion <= 3) {
|
||||
Execute("""
|
||||
CREATE TABLE downloads (
|
||||
url TEXT NOT NULL PRIMARY KEY,
|
||||
status INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
blob BLOB
|
||||
)
|
||||
""");
|
||||
|
||||
CreateDownloadsTable();
|
||||
perf.Step("Upgrade to version 4");
|
||||
}
|
||||
|
||||
@ -283,19 +168,6 @@ sealed class Schema {
|
||||
perf.Step("Upgrade to version 5");
|
||||
}
|
||||
|
||||
if (dbVersion <= 5) {
|
||||
Execute("ALTER TABLE attachments ADD download_url TEXT");
|
||||
Execute("ALTER TABLE downloads ADD download_url TEXT");
|
||||
|
||||
NormalizeAttachmentUrls();
|
||||
NormalizeDownloadUrls();
|
||||
|
||||
Execute("ALTER TABLE attachments RENAME COLUMN url TO normalized_url");
|
||||
Execute("ALTER TABLE downloads RENAME COLUMN url TO normalized_url");
|
||||
|
||||
perf.Step("Upgrade to version 6");
|
||||
}
|
||||
|
||||
perf.End();
|
||||
}
|
||||
}
|
||||
|
@ -252,8 +252,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
("attachment_id", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
("type", SqliteType.Text),
|
||||
("normalized_url", SqliteType.Text),
|
||||
("download_url", SqliteType.Text),
|
||||
("url", SqliteType.Text),
|
||||
("size", SqliteType.Integer),
|
||||
("width", SqliteType.Integer),
|
||||
("height", SqliteType.Integer),
|
||||
@ -309,8 +308,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
attachmentCmd.Set(":attachment_id", attachment.Id);
|
||||
attachmentCmd.Set(":name", attachment.Name);
|
||||
attachmentCmd.Set(":type", attachment.Type);
|
||||
attachmentCmd.Set(":normalized_url", attachment.NormalizedUrl);
|
||||
attachmentCmd.Set(":download_url", attachment.DownloadUrl);
|
||||
attachmentCmd.Set(":url", attachment.Url);
|
||||
attachmentCmd.Set(":size", attachment.Size);
|
||||
attachmentCmd.Set(":width", attachment.Width);
|
||||
attachmentCmd.Set(":height", attachment.Height);
|
||||
@ -365,13 +363,11 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
var reactions = GetAllReactions();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command($"""
|
||||
using var cmd = conn.Command(@"
|
||||
SELECT m.message_id, m.sender_id, m.channel_id, m.text, m.timestamp, et.edit_timestamp, rt.replied_to_id
|
||||
FROM messages m
|
||||
LEFT JOIN edit_timestamps et ON m.message_id = et.message_id
|
||||
LEFT JOIN replied_to rt ON m.message_id = rt.message_id
|
||||
{filter.GenerateWhereClause("m")}
|
||||
""");
|
||||
LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereClause("m"));
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
@ -422,7 +418,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
public int CountAttachments(AttachmentFilter? filter = null) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT COUNT(DISTINCT normalized_url) FROM attachments a" + filter.GenerateWhereClause("a"));
|
||||
using var cmd = conn.Command("SELECT COUNT(DISTINCT url) FROM attachments a" + filter.GenerateWhereClause("a"));
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
return reader.Read() ? reader.GetInt32(0) : 0;
|
||||
@ -431,15 +427,13 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
public void AddDownload(Data.Download download) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Upsert("downloads", new[] {
|
||||
("normalized_url", SqliteType.Text),
|
||||
("download_url", SqliteType.Text),
|
||||
("url", SqliteType.Text),
|
||||
("status", SqliteType.Integer),
|
||||
("size", SqliteType.Integer),
|
||||
("blob", SqliteType.Blob),
|
||||
});
|
||||
|
||||
cmd.Set(":normalized_url", download.NormalizedUrl);
|
||||
cmd.Set(":download_url", download.DownloadUrl);
|
||||
cmd.Set(":url", download.Url);
|
||||
cmd.Set(":status", (int) download.Status);
|
||||
cmd.Set(":size", download.Size);
|
||||
cmd.Set(":blob", download.Data);
|
||||
@ -452,16 +446,15 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
var list = new List<Data.Download>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT normalized_url, download_url, status, size FROM downloads");
|
||||
using var cmd = conn.Command("SELECT url, status, size FROM downloads");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
string normalizedUrl = reader.GetString(0);
|
||||
string downloadUrl = reader.GetString(1);
|
||||
var status = (DownloadStatus) reader.GetInt32(2);
|
||||
ulong size = reader.GetUint64(3);
|
||||
string url = reader.GetString(0);
|
||||
var status = (DownloadStatus) reader.GetInt32(1);
|
||||
ulong size = reader.GetUint64(2);
|
||||
|
||||
list.Add(new Data.Download(normalizedUrl, downloadUrl, status, size));
|
||||
list.Add(new Data.Download(url, status, size));
|
||||
}
|
||||
|
||||
return list;
|
||||
@ -469,8 +462,8 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
public Data.Download GetDownloadWithData(Data.Download download) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT blob FROM downloads WHERE normalized_url = :url");
|
||||
cmd.AddAndSet(":url", SqliteType.Text, download.NormalizedUrl);
|
||||
using var cmd = conn.Command("SELECT blob FROM downloads WHERE url = :url");
|
||||
cmd.AddAndSet(":url", SqliteType.Text, download.Url);
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -482,15 +475,14 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadedAttachment? GetDownloadedAttachment(string normalizedUrl) {
|
||||
public DownloadedAttachment? GetDownloadedAttachment(string url) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("""
|
||||
using var cmd = conn.Command(@"
|
||||
SELECT a.type, d.blob FROM downloads d
|
||||
LEFT JOIN attachments a ON d.normalized_url = a.normalized_url
|
||||
WHERE d.normalized_url = :normalized_url AND d.status = :success AND d.blob IS NOT NULL
|
||||
""");
|
||||
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(":normalized_url", SqliteType.Text, normalizedUrl);
|
||||
cmd.AddAndSet(":url", SqliteType.Text, url);
|
||||
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
@ -507,13 +499,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command($"""
|
||||
INSERT INTO downloads (normalized_url, download_url, status, size)
|
||||
SELECT a.normalized_url, a.download_url, :enqueued, MAX(a.size)
|
||||
FROM attachments a
|
||||
{filter.GenerateWhereClause("a")}
|
||||
GROUP BY a.normalized_url
|
||||
""");
|
||||
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");
|
||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
@ -522,7 +508,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
var list = new List<DownloadItem>();
|
||||
|
||||
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 url, size FROM downloads WHERE status = :enqueued LIMIT :limit");
|
||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||
cmd.AddAndSet(":limit", SqliteType.Integer, Math.Max(0, count));
|
||||
|
||||
@ -530,9 +516,8 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
while (reader.Read()) {
|
||||
list.Add(new DownloadItem {
|
||||
NormalizedUrl = reader.GetString(0),
|
||||
DownloadUrl = reader.GetString(1),
|
||||
Size = reader.GetUint64(2),
|
||||
Url = reader.GetString(0),
|
||||
Size = reader.GetUint64(1),
|
||||
});
|
||||
}
|
||||
|
||||
@ -546,7 +531,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
public DownloadStatusStatistics GetDownloadStatusStatistics() {
|
||||
static void LoadUndownloadedStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
|
||||
using var cmd = conn.Command("SELECT IFNULL(COUNT(size), 0), IFNULL(SUM(size), 0) FROM (SELECT MAX(a.size) size FROM attachments a WHERE a.normalized_url NOT IN (SELECT d.normalized_url FROM downloads d) GROUP BY a.normalized_url)");
|
||||
using var cmd = conn.Command("SELECT IFNULL(COUNT(size), 0), IFNULL(SUM(size), 0) FROM (SELECT MAX(a.size) size FROM attachments a WHERE a.url NOT IN (SELECT d.url FROM downloads d) GROUP BY a.url)");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (reader.Read()) {
|
||||
@ -556,16 +541,14 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
}
|
||||
|
||||
static void LoadSuccessStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
|
||||
using var cmd = conn.Command("""
|
||||
SELECT
|
||||
using var cmd = conn.Command(@"SELECT
|
||||
IFNULL(SUM(CASE WHEN status = :enqueued THEN 1 ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status = :enqueued 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 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 != :enqueued AND status != :success THEN size ELSE 0 END), 0)
|
||||
FROM downloads
|
||||
""");
|
||||
FROM downloads");
|
||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
||||
|
||||
@ -593,7 +576,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
var dict = new MultiDictionary<ulong, Attachment>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, normalized_url, download_url, size, width, height FROM attachments");
|
||||
using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size, width, height FROM attachments");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
@ -603,11 +586,10 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
Id = reader.GetUint64(1),
|
||||
Name = reader.GetString(2),
|
||||
Type = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
NormalizedUrl = reader.GetString(4),
|
||||
DownloadUrl = reader.GetString(5),
|
||||
Size = reader.GetUint64(6),
|
||||
Width = reader.IsDBNull(7) ? null : reader.GetInt32(7),
|
||||
Height = reader.IsDBNull(8) ? null : reader.GetInt32(8),
|
||||
Url = reader.GetString(4),
|
||||
Size = reader.GetUint64(5),
|
||||
Width = reader.IsDBNull(6) ? null : reader.GetInt32(6),
|
||||
Height = reader.IsDBNull(7) ? null : reader.GetInt32(7),
|
||||
});
|
||||
}
|
||||
|
||||
@ -695,7 +677,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
private long ComputeAttachmentStatistics() {
|
||||
using var conn = pool.Take();
|
||||
return conn.SelectScalar("SELECT COUNT(DISTINCT normalized_url) FROM attachments") as long? ?? 0L;
|
||||
return conn.SelectScalar("SELECT COUNT(DISTINCT url) FROM attachments") as long? ?? 0L;
|
||||
}
|
||||
|
||||
private void UpdateAttachmentStatistics(long totalAttachments) {
|
||||
|
@ -50,10 +50,10 @@ static class SqliteFilters {
|
||||
}
|
||||
|
||||
if (filter.DownloadItemRule == AttachmentFilter.DownloadItemRules.OnlyNotPresent) {
|
||||
where.AddCondition("normalized_url NOT IN (SELECT normalized_url FROM downloads)");
|
||||
where.AddCondition("url NOT IN (SELECT url FROM downloads)");
|
||||
}
|
||||
else if (filter.DownloadItemRule == AttachmentFilter.DownloadItemRules.OnlyPresent) {
|
||||
where.AddCondition("normalized_url IN (SELECT normalized_url FROM downloads)");
|
||||
where.AddCondition("url IN (SELECT url FROM downloads)");
|
||||
}
|
||||
|
||||
return where.Generate();
|
||||
|
@ -87,16 +87,16 @@ public sealed class BackgroundDownloadThread : BaseModel {
|
||||
FillQueue(db, queue, cancellationToken);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested && queue.TryDequeue(out var item)) {
|
||||
var downloadUrl = item.DownloadUrl;
|
||||
Log.Debug("Downloading " + downloadUrl + "...");
|
||||
var url = item.Url;
|
||||
Log.Debug("Downloading " + url + "...");
|
||||
|
||||
try {
|
||||
db.AddDownload(Data.Download.NewSuccess(item, await client.GetByteArrayAsync(downloadUrl, cancellationToken)));
|
||||
db.AddDownload(Data.Download.NewSuccess(url, await client.GetByteArrayAsync(url, cancellationToken)));
|
||||
} catch (HttpRequestException e) {
|
||||
db.AddDownload(Data.Download.NewFailure(item, e.StatusCode, item.Size));
|
||||
db.AddDownload(Data.Download.NewFailure(url, e.StatusCode, item.Size));
|
||||
Log.Error(e);
|
||||
} catch (Exception e) {
|
||||
db.AddDownload(Data.Download.NewFailure(item, null, item.Size));
|
||||
db.AddDownload(Data.Download.NewFailure(url, null, item.Size));
|
||||
Log.Error(e);
|
||||
} finally {
|
||||
parameters.FireOnItemFinished(item);
|
||||
|
@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
|
||||
namespace DHT.Server.Download;
|
||||
|
||||
static class DiscordCdn {
|
||||
private static FrozenSet<string> CdnHosts { get; } = new [] {
|
||||
"cdn.discordapp.com",
|
||||
"cdn.discord.com",
|
||||
}.ToFrozenSet();
|
||||
|
||||
public static string NormalizeUrl(string originalUrl) {
|
||||
return Uri.TryCreate(originalUrl, UriKind.Absolute, out var uri) && CdnHosts.Contains(uri.Host) ? uri.GetLeftPart(UriPartial.Path) : originalUrl;
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
namespace DHT.Server.Download;
|
||||
|
||||
public readonly struct DownloadItem {
|
||||
public string NormalizedUrl { get; init; }
|
||||
public string DownloadUrl { get; init; }
|
||||
public string Url { get; init; }
|
||||
public ulong Size { get; init; }
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Download;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Utils.Collections;
|
||||
using DHT.Utils.Http;
|
||||
@ -58,18 +57,14 @@ sealed class TrackMessagesEndpoint : BaseEndpoint {
|
||||
};
|
||||
|
||||
[SuppressMessage("ReSharper", "ConvertToLambdaExpression")]
|
||||
private static IEnumerable<Attachment> ReadAttachments(JsonElement.ArrayEnumerator array, string path) => array.Select(ele => {
|
||||
var downloadUrl = ele.RequireString("url", path);
|
||||
return new Attachment {
|
||||
private static IEnumerable<Attachment> ReadAttachments(JsonElement.ArrayEnumerator array, string path) => array.Select(ele => new Attachment {
|
||||
Id = ele.RequireSnowflake("id", path),
|
||||
Name = ele.RequireString("name", path),
|
||||
Type = ele.HasKey("type") ? ele.RequireString("type", path) : null,
|
||||
NormalizedUrl = DiscordCdn.NormalizeUrl(downloadUrl),
|
||||
DownloadUrl = downloadUrl,
|
||||
Url = ele.RequireString("url", path),
|
||||
Size = (ulong) ele.RequireLong("size", path),
|
||||
Width = ele.HasKey("width") ? ele.RequireInt("width", path) : null,
|
||||
Height = ele.HasKey("height") ? ele.RequireInt("height", path) : null,
|
||||
};
|
||||
}).DistinctByKeyStable(static attachment => {
|
||||
// Some Discord messages have duplicate attachments with the same id for unknown reasons.
|
||||
return attachment.Id;
|
||||
|
BIN
app/empty.dht
BIN
app/empty.dht
Binary file not shown.
Loading…
Reference in New Issue
Block a user