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

Compare commits

..

No commits in common. "d4d14cab97c68604e6690a2322787ff3ca2f2b5c" and "d01f9ed2186b83ed5ad58da1376059138600b946" have entirely different histories.

17 changed files with 161 additions and 312 deletions

View File

@ -29,6 +29,9 @@
<H2CodeStyleSettings version="6"> <H2CodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" /> <option name="USE_GENERIC_STYLE" value="true" />
</H2CodeStyleSettings> </H2CodeStyleSettings>
<H2CodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</H2CodeStyleSettings>
<HSQLCodeStyleSettings version="6"> <HSQLCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" /> <option name="USE_GENERIC_STYLE" value="true" />
</HSQLCodeStyleSettings> </HSQLCodeStyleSettings>

View File

@ -1,8 +1,8 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Desktop" type="DotNetProject" factoryName=".NET Project"> <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="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="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
@ -12,7 +12,7 @@
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" /> <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" /> <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" /> <option name="PROJECT_TFM" value="net5.0" />
<method v="2"> <method v="2">
<option name="Build" /> <option name="Build" />
</method> </method>

View File

@ -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>

View File

@ -4,8 +4,7 @@ public readonly struct Attachment {
public ulong Id { get; internal init; } public ulong Id { get; internal init; }
public string Name { get; internal init; } public string Name { get; internal init; }
public string? Type { get; internal init; } public string? Type { get; internal init; }
public string NormalizedUrl { get; internal init; } public string Url { get; internal init; }
public string DownloadUrl { get; internal init; }
public ulong Size { get; internal init; } public ulong Size { get; internal init; }
public int? Width { get; internal init; } public int? Width { get; internal init; }
public int? Height { get; internal init; } public int? Height { get; internal init; }

View File

@ -1,33 +1,30 @@
using System; using System;
using System.Net; using System.Net;
using DHT.Server.Download;
namespace DHT.Server.Data; namespace DHT.Server.Data;
public readonly struct Download { public readonly struct Download {
internal static Download NewSuccess(DownloadItem item, byte[] data) { internal static Download NewSuccess(string url, byte[] data) {
return new Download(item.NormalizedUrl, item.DownloadUrl, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data); return new Download(url, DownloadStatus.Success, (ulong) Math.Max(data.LongLength, 0), data);
} }
internal static Download NewFailure(DownloadItem item, HttpStatusCode? statusCode, ulong size) { internal static Download NewFailure(string url, HttpStatusCode? statusCode, ulong size) {
return new Download(item.NormalizedUrl, item.DownloadUrl, statusCode.HasValue ? (DownloadStatus) (int) statusCode : DownloadStatus.GenericError, size); return new Download(url, statusCode.HasValue ? (DownloadStatus) (int) statusCode : DownloadStatus.GenericError, size);
} }
public string NormalizedUrl { get; } public string Url { get; }
public string DownloadUrl { get; }
public DownloadStatus Status { get; } public DownloadStatus Status { get; }
public ulong Size { get; } public ulong Size { get; }
public byte[]? Data { get; } public byte[]? Data { get; }
internal Download(string normalizedUrl, string downloadUrl, DownloadStatus status, ulong size, byte[]? data = null) { internal Download(string url, DownloadStatus status, ulong size, byte[]? data = null) {
NormalizedUrl = normalizedUrl; Url = url;
DownloadUrl = downloadUrl;
Status = status; Status = status;
Size = size; Size = size;
Data = data; Data = data;
} }
internal Download WithData(byte[] data) { internal Download WithData(byte[] data) {
return new Download(NormalizedUrl, DownloadUrl, Status, Size, data); return new Download(Url, Status, Size, data);
} }
} }

View File

@ -13,6 +13,6 @@ public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
} }
public string GetAttachmentUrl(Attachment attachment) { 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;
} }
} }

View File

@ -8,11 +8,6 @@ public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
private StandaloneViewerExportStrategy() {} private StandaloneViewerExportStrategy() {}
public string GetAttachmentUrl(Attachment attachment) { public string GetAttachmentUrl(Attachment attachment) {
// The normalized URL will not load files from Discord CDN once the time limit is enforced. return attachment.Url;
// 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;
} }
} }

View File

@ -170,7 +170,7 @@ public static class ViewerJsonExport {
obj["a"] = message.Attachments.Select(attachment => { obj["a"] = message.Attachments.Select(attachment => {
var a = new Dictionary<string, object> { var a = new Dictionary<string, object> {
{ "url", strategy.GetAttachmentUrl(attachment) }, { "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 }) { if (attachment is { Width: not null, Height: not null }) {

View File

@ -197,8 +197,7 @@ public static class LegacyArchiveImport {
Id = fakeSnowflake.Next(), Id = fakeSnowflake.Next(),
Name = name, Name = name,
Type = type, Type = type,
NormalizedUrl = url, Url = url,
DownloadUrl = url,
Size = 0, // unknown size Size = 0, // unknown size
}; };
}).DistinctByKeyStable(static attachment => { }).DistinctByKeyStable(static attachment => {

View File

@ -1,16 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using DHT.Server.Database.Exceptions; using DHT.Server.Database.Exceptions;
using DHT.Server.Database.Sqlite.Utils; using DHT.Server.Database.Sqlite.Utils;
using DHT.Server.Download;
using DHT.Utils.Logging; using DHT.Utils.Logging;
using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite; namespace DHT.Server.Database.Sqlite;
sealed class Schema { sealed class Schema {
internal const int Version = 6; internal const int Version = 5;
private static readonly Log Log = Log.ForType<Schema>(); private static readonly Log Log = Log.ForType<Schema>();
@ -50,88 +47,57 @@ sealed class Schema {
} }
private void InitializeSchemas() { private void InitializeSchemas() {
Execute(""" Execute(@"CREATE TABLE users (
CREATE TABLE users (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
avatar_url TEXT, avatar_url TEXT,
discriminator TEXT discriminator TEXT)");
)
""");
Execute(""" Execute(@"CREATE TABLE servers (
CREATE TABLE servers (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT NOT NULL type TEXT NOT NULL)");
)
""");
Execute(""" Execute(@"CREATE TABLE channels (
CREATE TABLE channels (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
server INTEGER NOT NULL, server INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
parent_id INTEGER, parent_id INTEGER,
position INTEGER, position INTEGER,
topic TEXT, topic TEXT,
nsfw INTEGER nsfw INTEGER)");
)
""");
Execute(""" Execute(@"CREATE TABLE messages (
CREATE TABLE messages (
message_id INTEGER PRIMARY KEY NOT NULL, message_id INTEGER PRIMARY KEY NOT NULL,
sender_id INTEGER NOT NULL, sender_id INTEGER NOT NULL,
channel_id INTEGER NOT NULL, channel_id INTEGER NOT NULL,
text TEXT NOT NULL, text TEXT NOT NULL,
timestamp INTEGER NOT NULL timestamp INTEGER NOT NULL)");
)
""");
Execute(""" Execute(@"CREATE TABLE attachments (
CREATE TABLE attachments (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
attachment_id INTEGER NOT NULL PRIMARY KEY NOT NULL, attachment_id INTEGER NOT NULL PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT, type TEXT,
normalized_url TEXT NOT NULL, url TEXT NOT NULL,
download_url TEXT,
size INTEGER NOT NULL, size INTEGER NOT NULL,
width INTEGER, width INTEGER,
height INTEGER height INTEGER)");
)
""");
Execute(""" Execute(@"CREATE TABLE embeds (
CREATE TABLE embeds (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
json TEXT NOT NULL json TEXT NOT NULL)");
)
""");
Execute(""" Execute(@"CREATE TABLE reactions (
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 (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
emoji_id INTEGER, emoji_id INTEGER,
emoji_name TEXT, emoji_name TEXT,
emoji_flags INTEGER NOT NULL, emoji_flags INTEGER NOT NULL,
count INTEGER NOT NULL count INTEGER NOT NULL)");
)
""");
CreateMessageEditTimestampTable(); CreateMessageEditTimestampTable();
CreateMessageRepliedToTable(); CreateMessageRepliedToTable();
CreateDownloadsTable();
Execute("CREATE INDEX attachments_message_ix ON attachments(message_id)"); Execute("CREATE INDEX attachments_message_ix ON attachments(message_id)");
Execute("CREATE INDEX embeds_message_ix ON embeds(message_id)"); Execute("CREATE INDEX embeds_message_ix ON embeds(message_id)");
@ -141,90 +107,23 @@ sealed class Schema {
} }
private void CreateMessageEditTimestampTable() { private void CreateMessageEditTimestampTable() {
Execute(""" Execute(@"CREATE TABLE edit_timestamps (
CREATE TABLE edit_timestamps (
message_id INTEGER PRIMARY KEY NOT NULL, message_id INTEGER PRIMARY KEY NOT NULL,
edit_timestamp INTEGER NOT NULL edit_timestamp INTEGER NOT NULL)");
)
""");
} }
private void CreateMessageRepliedToTable() { private void CreateMessageRepliedToTable() {
Execute(""" Execute(@"CREATE TABLE replied_to (
CREATE TABLE replied_to (
message_id INTEGER PRIMARY KEY NOT NULL, message_id INTEGER PRIMARY KEY NOT NULL,
replied_to_id INTEGER NOT NULL replied_to_id INTEGER NOT NULL)");
)
""");
} }
private void NormalizeAttachmentUrls() { private void CreateDownloadsTable() {
var normalizedUrls = new Dictionary<long, string>(); Execute(@"CREATE TABLE downloads (
url TEXT NOT NULL PRIMARY KEY,
using (var selectCmd = conn.Command("SELECT attachment_id, url FROM attachments")) { status INTEGER NOT NULL,
using var reader = selectCmd.ExecuteReader(); size INTEGER NOT NULL,
blob BLOB)");
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 UpgradeSchemas(int dbVersion) { private void UpgradeSchemas(int dbVersion) {
@ -241,19 +140,13 @@ sealed class Schema {
CreateMessageEditTimestampTable(); CreateMessageEditTimestampTable();
CreateMessageRepliedToTable(); CreateMessageRepliedToTable();
Execute(""" Execute(@"INSERT INTO edit_timestamps (message_id, edit_timestamp)
INSERT INTO edit_timestamps (message_id, edit_timestamp) SELECT message_id, edit_timestamp FROM messages
SELECT message_id, edit_timestamp WHERE edit_timestamp IS NOT NULL");
FROM messages
WHERE edit_timestamp IS NOT NULL
""");
Execute(""" Execute(@"INSERT INTO replied_to (message_id, replied_to_id)
INSERT INTO replied_to (message_id, replied_to_id) SELECT message_id, replied_to_id FROM messages
SELECT message_id, replied_to_id WHERE replied_to_id IS NOT NULL");
FROM messages
WHERE replied_to_id IS NOT NULL
""");
Execute("ALTER TABLE messages DROP COLUMN replied_to_id"); Execute("ALTER TABLE messages DROP COLUMN replied_to_id");
Execute("ALTER TABLE messages DROP COLUMN edit_timestamp"); Execute("ALTER TABLE messages DROP COLUMN edit_timestamp");
@ -265,15 +158,7 @@ sealed class Schema {
} }
if (dbVersion <= 3) { if (dbVersion <= 3) {
Execute(""" CreateDownloadsTable();
CREATE TABLE downloads (
url TEXT NOT NULL PRIMARY KEY,
status INTEGER NOT NULL,
size INTEGER NOT NULL,
blob BLOB
)
""");
perf.Step("Upgrade to version 4"); perf.Step("Upgrade to version 4");
} }
@ -283,19 +168,6 @@ sealed class Schema {
perf.Step("Upgrade to version 5"); 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(); perf.End();
} }
} }

View File

@ -252,8 +252,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
("attachment_id", SqliteType.Integer), ("attachment_id", SqliteType.Integer),
("name", SqliteType.Text), ("name", SqliteType.Text),
("type", SqliteType.Text), ("type", SqliteType.Text),
("normalized_url", SqliteType.Text), ("url", SqliteType.Text),
("download_url", SqliteType.Text),
("size", SqliteType.Integer), ("size", SqliteType.Integer),
("width", SqliteType.Integer), ("width", SqliteType.Integer),
("height", SqliteType.Integer), ("height", SqliteType.Integer),
@ -309,8 +308,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
attachmentCmd.Set(":attachment_id", attachment.Id); attachmentCmd.Set(":attachment_id", attachment.Id);
attachmentCmd.Set(":name", attachment.Name); attachmentCmd.Set(":name", attachment.Name);
attachmentCmd.Set(":type", attachment.Type); attachmentCmd.Set(":type", attachment.Type);
attachmentCmd.Set(":normalized_url", attachment.NormalizedUrl); attachmentCmd.Set(":url", attachment.Url);
attachmentCmd.Set(":download_url", attachment.DownloadUrl);
attachmentCmd.Set(":size", attachment.Size); attachmentCmd.Set(":size", attachment.Size);
attachmentCmd.Set(":width", attachment.Width); attachmentCmd.Set(":width", attachment.Width);
attachmentCmd.Set(":height", attachment.Height); attachmentCmd.Set(":height", attachment.Height);
@ -365,13 +363,11 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
var reactions = GetAllReactions(); var reactions = GetAllReactions();
using var conn = pool.Take(); 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 SELECT m.message_id, m.sender_id, m.channel_id, m.text, m.timestamp, et.edit_timestamp, rt.replied_to_id
FROM messages m FROM messages m
LEFT JOIN edit_timestamps et ON m.message_id = et.message_id LEFT JOIN edit_timestamps et ON m.message_id = et.message_id
LEFT JOIN replied_to rt ON m.message_id = rt.message_id LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereClause("m"));
{filter.GenerateWhereClause("m")}
""");
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
while (reader.Read()) { while (reader.Read()) {
@ -422,7 +418,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
public int CountAttachments(AttachmentFilter? filter = null) { public int CountAttachments(AttachmentFilter? filter = null) {
using var conn = pool.Take(); 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(); using var reader = cmd.ExecuteReader();
return reader.Read() ? reader.GetInt32(0) : 0; return reader.Read() ? reader.GetInt32(0) : 0;
@ -431,15 +427,13 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
public void AddDownload(Data.Download download) { public void AddDownload(Data.Download download) {
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Upsert("downloads", new[] { using var cmd = conn.Upsert("downloads", new[] {
("normalized_url", SqliteType.Text), ("url", SqliteType.Text),
("download_url", SqliteType.Text),
("status", SqliteType.Integer), ("status", SqliteType.Integer),
("size", SqliteType.Integer), ("size", SqliteType.Integer),
("blob", SqliteType.Blob), ("blob", SqliteType.Blob),
}); });
cmd.Set(":normalized_url", download.NormalizedUrl); cmd.Set(":url", download.Url);
cmd.Set(":download_url", download.DownloadUrl);
cmd.Set(":status", (int) download.Status); cmd.Set(":status", (int) download.Status);
cmd.Set(":size", download.Size); cmd.Set(":size", download.Size);
cmd.Set(":blob", download.Data); cmd.Set(":blob", download.Data);
@ -452,16 +446,15 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
var list = new List<Data.Download>(); var list = new List<Data.Download>();
using var conn = pool.Take(); 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(); using var reader = cmd.ExecuteReader();
while (reader.Read()) { while (reader.Read()) {
string normalizedUrl = reader.GetString(0); string url = reader.GetString(0);
string downloadUrl = reader.GetString(1); var status = (DownloadStatus) reader.GetInt32(1);
var status = (DownloadStatus) reader.GetInt32(2); ulong size = reader.GetUint64(2);
ulong size = reader.GetUint64(3);
list.Add(new Data.Download(normalizedUrl, downloadUrl, status, size)); list.Add(new Data.Download(url, status, size));
} }
return list; return list;
@ -469,8 +462,8 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
public Data.Download GetDownloadWithData(Data.Download download) { public Data.Download GetDownloadWithData(Data.Download download) {
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Command("SELECT blob FROM downloads WHERE normalized_url = :url"); using var cmd = conn.Command("SELECT blob FROM downloads WHERE url = :url");
cmd.AddAndSet(":url", SqliteType.Text, download.NormalizedUrl); cmd.AddAndSet(":url", SqliteType.Text, download.Url);
using var reader = cmd.ExecuteReader(); 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 conn = pool.Take();
using var cmd = conn.Command(""" using var cmd = conn.Command(@"
SELECT a.type, d.blob FROM downloads d SELECT a.type, d.blob FROM downloads d
LEFT JOIN attachments a ON d.normalized_url = a.normalized_url LEFT JOIN attachments a ON d.url = a.url
WHERE d.normalized_url = :normalized_url AND d.status = :success AND d.blob IS NOT NULL 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); cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
@ -507,13 +499,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
public void EnqueueDownloadItems(AttachmentFilter? filter = null) { public void EnqueueDownloadItems(AttachmentFilter? filter = null) {
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Command($""" 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");
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
""");
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued); cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@ -522,7 +508,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
var list = new List<DownloadItem>(); var list = 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 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));
@ -530,9 +516,8 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
while (reader.Read()) { while (reader.Read()) {
list.Add(new DownloadItem { list.Add(new DownloadItem {
NormalizedUrl = reader.GetString(0), Url = reader.GetString(0),
DownloadUrl = reader.GetString(1), Size = reader.GetUint64(1),
Size = reader.GetUint64(2),
}); });
} }
@ -546,7 +531,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
public DownloadStatusStatistics GetDownloadStatusStatistics() { public DownloadStatusStatistics GetDownloadStatusStatistics() {
static void LoadUndownloadedStatistics(ISqliteConnection conn, DownloadStatusStatistics result) { 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(); using var reader = cmd.ExecuteReader();
if (reader.Read()) { if (reader.Read()) {
@ -556,16 +541,14 @@ 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 = :enqueued THEN 1 ELSE 0 END), 0), IFNULL(SUM(CASE WHEN status = :enqueued THEN size 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 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 != :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)
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(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success); cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
@ -593,7 +576,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
var dict = new MultiDictionary<ulong, Attachment>(); var dict = new MultiDictionary<ulong, Attachment>();
using var conn = pool.Take(); 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(); using var reader = cmd.ExecuteReader();
while (reader.Read()) { while (reader.Read()) {
@ -603,11 +586,10 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
Id = reader.GetUint64(1), Id = reader.GetUint64(1),
Name = reader.GetString(2), Name = reader.GetString(2),
Type = reader.IsDBNull(3) ? null : reader.GetString(3), Type = reader.IsDBNull(3) ? null : reader.GetString(3),
NormalizedUrl = reader.GetString(4), Url = reader.GetString(4),
DownloadUrl = reader.GetString(5), Size = reader.GetUint64(5),
Size = reader.GetUint64(6), Width = reader.IsDBNull(6) ? null : reader.GetInt32(6),
Width = reader.IsDBNull(7) ? null : reader.GetInt32(7), Height = reader.IsDBNull(7) ? null : reader.GetInt32(7),
Height = reader.IsDBNull(8) ? null : reader.GetInt32(8),
}); });
} }
@ -695,7 +677,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
private long ComputeAttachmentStatistics() { private long ComputeAttachmentStatistics() {
using var conn = pool.Take(); 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) { private void UpdateAttachmentStatistics(long totalAttachments) {

View File

@ -50,10 +50,10 @@ static class SqliteFilters {
} }
if (filter.DownloadItemRule == AttachmentFilter.DownloadItemRules.OnlyNotPresent) { 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) { 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(); return where.Generate();

View File

@ -87,16 +87,16 @@ public sealed class BackgroundDownloadThread : BaseModel {
FillQueue(db, queue, cancellationToken); FillQueue(db, queue, cancellationToken);
while (!cancellationToken.IsCancellationRequested && queue.TryDequeue(out var item)) { while (!cancellationToken.IsCancellationRequested && queue.TryDequeue(out var item)) {
var downloadUrl = item.DownloadUrl; var url = item.Url;
Log.Debug("Downloading " + downloadUrl + "..."); Log.Debug("Downloading " + url + "...");
try { 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) { } 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); Log.Error(e);
} catch (Exception 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); Log.Error(e);
} finally { } finally {
parameters.FireOnItemFinished(item); parameters.FireOnItemFinished(item);

View File

@ -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;
}
}

View File

@ -1,7 +1,6 @@
namespace DHT.Server.Download; namespace DHT.Server.Download;
public readonly struct DownloadItem { public readonly struct DownloadItem {
public string NormalizedUrl { get; init; } public string Url { get; init; }
public string DownloadUrl { get; init; }
public ulong Size { get; init; } public ulong Size { get; init; }
} }

View File

@ -8,7 +8,6 @@ using System.Threading.Tasks;
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;
using DHT.Server.Download;
using DHT.Server.Service; using DHT.Server.Service;
using DHT.Utils.Collections; using DHT.Utils.Collections;
using DHT.Utils.Http; using DHT.Utils.Http;
@ -58,18 +57,14 @@ sealed class TrackMessagesEndpoint : BaseEndpoint {
}; };
[SuppressMessage("ReSharper", "ConvertToLambdaExpression")] [SuppressMessage("ReSharper", "ConvertToLambdaExpression")]
private static IEnumerable<Attachment> ReadAttachments(JsonElement.ArrayEnumerator array, string path) => array.Select(ele => { private static IEnumerable<Attachment> ReadAttachments(JsonElement.ArrayEnumerator array, string path) => array.Select(ele => new Attachment {
var downloadUrl = ele.RequireString("url", path);
return new Attachment {
Id = ele.RequireSnowflake("id", path), Id = ele.RequireSnowflake("id", path),
Name = ele.RequireString("name", path), Name = ele.RequireString("name", path),
Type = ele.HasKey("type") ? ele.RequireString("type", path) : null, Type = ele.HasKey("type") ? ele.RequireString("type", path) : null,
NormalizedUrl = DiscordCdn.NormalizeUrl(downloadUrl), Url = ele.RequireString("url", path),
DownloadUrl = downloadUrl,
Size = (ulong) ele.RequireLong("size", path), Size = (ulong) ele.RequireLong("size", path),
Width = ele.HasKey("width") ? ele.RequireInt("width", path) : null, Width = ele.HasKey("width") ? ele.RequireInt("width", path) : null,
Height = ele.HasKey("height") ? ele.RequireInt("height", path) : null, Height = ele.HasKey("height") ? ele.RequireInt("height", path) : null,
};
}).DistinctByKeyStable(static attachment => { }).DistinctByKeyStable(static attachment => {
// Some Discord messages have duplicate attachments with the same id for unknown reasons. // Some Discord messages have duplicate attachments with the same id for unknown reasons.
return attachment.Id; return attachment.Id;

Binary file not shown.