This commit is contained in:
Tyrrrz 2025-04-02 00:34:52 +03:00
parent 7add81a472
commit 6fb197cf0b
6 changed files with 32 additions and 39 deletions
DiscordChatExporter.Cli.Tests/Specs
DiscordChatExporter.Cli/Commands/Base
DiscordChatExporter.Core
DiscordChatExporter.Gui/ViewModels/Components

View file

@ -148,12 +148,10 @@ public class DateRangeSpecs
} }
[Fact] [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 // Arrange
var before = long_in_the_past; var before = new DateTimeOffset(2020, 08, 01, 0, 0, 0, TimeSpan.Zero);
using var file = TempFile.Create(); using var file = TempFile.Create();
// Act // Act

View file

@ -265,7 +265,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
using (console.WithForegroundColor(ConsoleColor.Yellow)) using (console.WithForegroundColor(ConsoleColor.Yellow))
{ {
await console.Error.WriteLineAsync( 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)) 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) foreach (var (channel, message) in errorsByChannel)

View file

@ -1,8 +1,3 @@
using System;
namespace DiscordChatExporter.Core.Exceptions; namespace DiscordChatExporter.Core.Exceptions;
// Thrown when there is circumstancially no message to export with given parameters, public class ChannelEmptyException(string message) : DiscordChatExporterException(message);
// though it should not be treated as a runtime error; simply warn instead
public class ChannelEmptyException(string message)
: DiscordChatExporterException(message, false, null) { }

View file

@ -31,7 +31,8 @@ public class ChannelExporter(DiscordClient discord)
var context = new ExportContext(discord, request); var context = new ExportContext(discord, request);
await context.PopulateChannelsAndRolesAsync(cancellationToken); 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); await using var messageExporter = new MessageExporter(context);
// Check if the channel is empty // Check if the channel is empty

View file

@ -13,24 +13,7 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
public long MessagesExported { get; private set; } public long MessagesExported { get; private set; }
private async ValueTask ResetWriterAsync(CancellationToken cancellationToken = default) private async ValueTask<MessageWriter> InitializeWriterAsync(
{
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<MessageWriter> GetWriterAsync(
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
@ -43,7 +26,7 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
) )
) )
{ {
await ResetWriterAsync(cancellationToken); await UninitializeWriterAsync(cancellationToken);
_partitionIndex++; _partitionIndex++;
} }
@ -60,21 +43,40 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
return _writer = writer; 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( public async ValueTask ExportMessageAsync(
Message message, Message message,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
var writer = await GetWriterAsync(cancellationToken); var writer = await InitializeWriterAsync(cancellationToken);
await writer.WriteMessageAsync(message, cancellationToken); await writer.WriteMessageAsync(message, cancellationToken);
MessagesExported++; MessagesExported++;
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
// causes the file to be created whether there were messages written or not // If not messages were written, force the creation of an empty file
await GetWriterAsync(); if (MessagesExported <= 0)
await ResetWriterAsync(); _ = await InitializeWriterAsync();
await UninitializeWriterAsync();
} }
} }

View file

@ -286,9 +286,6 @@ public partial class DashboardViewModel : ViewModelBase
catch (ChannelEmptyException ex) catch (ChannelEmptyException ex)
{ {
_snackbarManager.Notify(ex.Message.TrimEnd('.')); _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) catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{ {