diff --git a/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs index ab8c0d40..25e0cec9 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs @@ -148,12 +148,10 @@ public class DateRangeSpecs } [Fact] - public async Task Export_file_is_created_even_when_nothing_to_export() + public async Task I_can_filter_the_export_to_not_include_any_messages() { - var long_in_the_past = new DateTimeOffset(1921, 08, 01, 0, 0, 0, TimeSpan.Zero); - // Arrange - var before = long_in_the_past; + var before = new DateTimeOffset(2020, 08, 01, 0, 0, 0, TimeSpan.Zero); using var file = TempFile.Create(); // Act diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index f2fd495c..d77cd74d 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -265,7 +265,7 @@ public abstract class ExportCommandBase : DiscordCommandBase using (console.WithForegroundColor(ConsoleColor.Yellow)) { await console.Error.WriteLineAsync( - $"Warnings reported for the following channel(s):" + "Warnings reported for the following channel(s):" ); } @@ -286,7 +286,7 @@ public abstract class ExportCommandBase : DiscordCommandBase using (console.WithForegroundColor(ConsoleColor.Red)) { - await console.Error.WriteLineAsync($"Failed to export the following channel(s):"); + await console.Error.WriteLineAsync("Failed to export the following channel(s):"); } foreach (var (channel, message) in errorsByChannel) diff --git a/DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs b/DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs index b2373cbb..42e6c787 100644 --- a/DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs +++ b/DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs @@ -1,8 +1,3 @@ -using System; - namespace DiscordChatExporter.Core.Exceptions; -// Thrown when there is circumstancially no message to export with given parameters, -// though it should not be treated as a runtime error; simply warn instead -public class ChannelEmptyException(string message) - : DiscordChatExporterException(message, false, null) { } +public class ChannelEmptyException(string message) : DiscordChatExporterException(message); diff --git a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs index 58830e7c..68ca75b4 100644 --- a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs @@ -31,7 +31,8 @@ public class ChannelExporter(DiscordClient discord) var context = new ExportContext(discord, request); await context.PopulateChannelsAndRolesAsync(cancellationToken); - // Export messages + // Initialize the exporter before further checks to ensure the file is created even if + // an exception is thrown after this point. await using var messageExporter = new MessageExporter(context); // Check if the channel is empty diff --git a/DiscordChatExporter.Core/Exporting/MessageExporter.cs b/DiscordChatExporter.Core/Exporting/MessageExporter.cs index 95497962..1f0f7ca4 100644 --- a/DiscordChatExporter.Core/Exporting/MessageExporter.cs +++ b/DiscordChatExporter.Core/Exporting/MessageExporter.cs @@ -13,24 +13,7 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable public long MessagesExported { get; private set; } - private async ValueTask ResetWriterAsync(CancellationToken cancellationToken = default) - { - if (_writer is not null) - { - try - { - await _writer.WritePostambleAsync(cancellationToken); - } - // Writer must be disposed, even if it fails to write the postamble - finally - { - await _writer.DisposeAsync(); - _writer = null; - } - } - } - - private async ValueTask GetWriterAsync( + private async ValueTask InitializeWriterAsync( CancellationToken cancellationToken = default ) { @@ -43,7 +26,7 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable ) ) { - await ResetWriterAsync(cancellationToken); + await UninitializeWriterAsync(cancellationToken); _partitionIndex++; } @@ -60,21 +43,40 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable return _writer = writer; } + private async ValueTask UninitializeWriterAsync(CancellationToken cancellationToken = default) + { + if (_writer is not null) + { + try + { + await _writer.WritePostambleAsync(cancellationToken); + } + // Writer must be disposed, even if it fails to write the postamble + finally + { + await _writer.DisposeAsync(); + _writer = null; + } + } + } + public async ValueTask ExportMessageAsync( Message message, CancellationToken cancellationToken = default ) { - var writer = await GetWriterAsync(cancellationToken); + var writer = await InitializeWriterAsync(cancellationToken); await writer.WriteMessageAsync(message, cancellationToken); MessagesExported++; } public async ValueTask DisposeAsync() { - // causes the file to be created whether there were messages written or not - await GetWriterAsync(); - await ResetWriterAsync(); + // If not messages were written, force the creation of an empty file + if (MessagesExported <= 0) + _ = await InitializeWriterAsync(); + + await UninitializeWriterAsync(); } } diff --git a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs index 4288723c..5d1bf649 100644 --- a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs @@ -286,9 +286,6 @@ public partial class DashboardViewModel : ViewModelBase catch (ChannelEmptyException ex) { _snackbarManager.Notify(ex.Message.TrimEnd('.')); - - // FIXME: not exactly successful, but not a failure either. Not ideal to duplicate the line - Interlocked.Increment(ref successfulExportCount); } catch (DiscordChatExporterException ex) when (!ex.IsFatal) {