From 1ae03ad2069f34ee7dac0ef76515e5525773fddc Mon Sep 17 00:00:00 2001 From: Kornelius Rohrschneider Date: Wed, 29 Oct 2025 00:01:39 +0100 Subject: [PATCH] Fixed appending empty channel exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several bugs related to appending empty channel exports have been fixed: - Previously, the partition index hadn’t been adapted if the existing channel export had been empty. This had resulted in an uncaught exception as the target file had already existed. - Previously, the HTML last message detection hadn’t worked if the export had been empty, resulting in an uncaught exception. - Previously, empty TXT exports had been detected as message containing because the footer hadn’t been taken into account. This had resulted in an uncaught exception as no valid timestamp could have been retrieved. - Previously, empty TXT & CSV exports would have been detected as message containing if they had contained enough empty lines. This has been fixed by testing whether the respective file has enough non-empty lines in a functional and lazy way. Additionally, there have been multiple other improvements: - The CSV last message detection has been improved to now use the last non-empty line. Previously, it wouldn’t have worked (and thrown an exception) if there had been any trailing empty lines. - The info messages that a channel export is being appended had previously not been logged if the export had been empty. Therefore, new specific info logs have been added for this situation. - If an empty channel had previously been exported and is still empty, the export will now be aborted (and a respective new info message will be logged). - A comment has been added to the HTML template to prevent it from being changed without corresponding changes in the HTML last message detection. --- .../Exporting/ChannelExporter.cs | 22 +++++++++++++------ .../Exporting/CsvMessageWriter.cs | 14 +++++++----- .../Exporting/HtmlMessageWriter.cs | 2 ++ .../Exporting/MessageGroupTemplate.cshtml | 1 + .../Exporting/PlainTextMessageWriter.cs | 9 ++++++-- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs index 4a434a97..9a8aa37c 100644 --- a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs @@ -55,24 +55,32 @@ public class ChannelExporter(DiscordClient discord) ); if (lastMessageSnowflake != null) { - request.LastPriorMessage = lastMessageSnowflake.Value; - - if (!request.Channel.MayHaveMessagesAfter(request.LastPriorMessage.Value)) + if (!request.Channel.MayHaveMessagesAfter(lastMessageSnowflake.Value)) { logger.IncrementCounter(ExportResult.UpdateExportSkip); logger.LogInfo(request, "Existing export already up to date"); return; } - + request.LastPriorMessage = lastMessageSnowflake.Value; logger.LogInfo( request, "Appending existing export starting at " + lastMessageSnowflake.Value.ToDate() ); - currentPartitionIndex = MessageExporter.GetPartitionCount( - request.OutputFilePath - ); } + else + { + if (request.Channel.IsEmpty) + { + logger.IncrementCounter(ExportResult.UpdateExportSkip); + logger.LogInfo(request, "Existing empty export already up to date"); + return; + } + logger.LogInfo(request, "Appending existing empty export."); + } + currentPartitionIndex = MessageExporter.GetPartitionCount( + request.OutputFilePath + ); break; default: throw new InvalidOperationException( diff --git a/DiscordChatExporter.Core/Exporting/CsvMessageWriter.cs b/DiscordChatExporter.Core/Exporting/CsvMessageWriter.cs index 9df2bba3..ede7052c 100644 --- a/DiscordChatExporter.Core/Exporting/CsvMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/CsvMessageWriter.cs @@ -142,10 +142,12 @@ internal partial class CsvMessageWriter(Stream stream, ExportContext context) { try { - var fileLines = File.ReadAllLines(filePath) - .SkipWhile(string.IsNullOrWhiteSpace) - .ToArray(); - if (fileLines.Length <= HeaderSize) + var fileLines = File.ReadAllLines(filePath); + var fileContainsMessages = fileLines + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Skip(HeaderSize) + .Any(); + if (!fileContainsMessages) return null; const string columnPattern = "(?:[^\"]?(?:\"\")?)*"; @@ -155,7 +157,9 @@ internal partial class CsvMessageWriter(Stream stream, ExportContext context) ); var messageDateRegex = new Regex(messageDatePattern); - var timestampMatch = messageDateRegex.Match(fileLines[^1]); + var timestampMatch = messageDateRegex.Match( + fileLines.Last(line => !string.IsNullOrWhiteSpace(line)) + ); var timestampString = timestampMatch.Groups[1].Value; var timestamp = DateTimeOffset.Parse(timestampString); return Snowflake.FromDate(timestamp, true); diff --git a/DiscordChatExporter.Core/Exporting/HtmlMessageWriter.cs b/DiscordChatExporter.Core/Exporting/HtmlMessageWriter.cs index e6916fd2..f8357a99 100644 --- a/DiscordChatExporter.Core/Exporting/HtmlMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/HtmlMessageWriter.cs @@ -179,6 +179,8 @@ internal partial class HtmlMessageWriter(Stream stream, ExportContext context, s var messageDateRegex = MessageDateRegex(); var timestampMatches = messageDateRegex.Matches(fileContent); + if (timestampMatches.Count == 0) + return null; var timestampString = timestampMatches[^1].Groups[1].Value; var timestamp = DateTimeOffset.Parse(timestampString); return Snowflake.FromDate(timestamp, true); diff --git a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml index e0730553..4a7d2558 100644 --- a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml @@ -240,6 +240,7 @@ } @* Timestamp *@ + @* Don't change this without changing the corresponding detection in the HtmlMessageWriter *@ @FormatDate(message.Timestamp) } diff --git a/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs b/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs index f96248ad..a2dc5976 100644 --- a/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs @@ -16,6 +16,7 @@ internal partial class PlainTextMessageWriter(Stream stream, ExportContext conte : MessageWriter(stream, context) { private const int HeaderSize = 4; + private const int FooterSize = 3; private readonly TextWriter _writer = new StreamWriter(stream); @@ -306,13 +307,17 @@ internal partial class PlainTextMessageWriter(Stream stream, ExportContext conte public static Snowflake? GetLastMessageDate(string filePath) { var fileLines = File.ReadAllLines(filePath); - if (fileLines.SkipWhile(string.IsNullOrWhiteSpace).ToArray().Length <= HeaderSize) + var fileContainsMessages = fileLines + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Skip(HeaderSize + FooterSize) + .Any(); + if (!fileContainsMessages) return null; var messageDateRegex = MessageDateRegex(); // Find the last line with a message timestamp - for (var i = fileLines.Length - 1; i >= HeaderSize; i--) + for (var i = fileLines.Length - 1 - FooterSize; i >= HeaderSize; i--) { var timestampMatch = messageDateRegex.Match(fileLines[i]); if (!timestampMatch.Success)