Compare commits

...

2 Commits

Author SHA1 Message Date
Jacob Pfundstein 5a57c4202d Validate output path early to prevent errors when exporting multiple channels (#1555)
Co-authored-by: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
2026-06-18 15:37:58 +03:00
tyrrrz acac8f7bbb Update packages 2026-06-08 15:52:10 +03:00
6 changed files with 40 additions and 31 deletions
+2 -2
View File
@@ -14,7 +14,7 @@
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" /> <PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="CSharpier.MsBuild" Version="1.2.6" /> <PackageVersion Include="CSharpier.MsBuild" Version="1.2.6" />
<PackageVersion Include="Deorcify" Version="1.1.0" /> <PackageVersion Include="Deorcify" Version="2.0.1" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.2" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.12.2" />
<PackageVersion Include="FluentAssertions" Version="8.10.0" /> <PackageVersion Include="FluentAssertions" Version="8.10.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.4" /> <PackageVersion Include="GitHubActionsTestLogger" Version="3.0.4" />
@@ -33,7 +33,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Onova" Version="2.6.13" /> <PackageVersion Include="Onova" Version="2.6.13" />
<PackageVersion Include="Polly" Version="8.6.6" /> <PackageVersion Include="Polly" Version="8.6.6" />
<PackageVersion Include="PowerKit" Version="1.2.0" /> <PackageVersion Include="PowerKit" Version="2.0.1" />
<PackageVersion Include="RazorBlade" Version="1.0.0" /> <PackageVersion Include="RazorBlade" Version="1.0.0" />
<PackageVersion Include="Spectre.Console" Version="0.55.2" /> <PackageVersion Include="Spectre.Console" Version="0.55.2" />
<PackageVersion Include="Superpower" Version="3.1.0" /> <PackageVersion Include="Superpower" Version="3.1.0" />
@@ -14,13 +14,13 @@
<PackageReference Include="coverlet.collector" PrivateAssets="all" /> <PackageReference Include="coverlet.collector" PrivateAssets="all" />
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" /> <PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" /> <PackageReference Include="FluentAssertions" />
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="all" /> <PackageReference Include="GitHubActionsTestLogger" />
<PackageReference Include="JsonExtensions" /> <PackageReference Include="JsonExtensions" />
<PackageReference Include="Microsoft.Extensions.Configuration" /> <PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="PowerKit" PrivateAssets="all" /> <PackageReference Include="PowerKit" />
<PackageReference Include="xunit" /> <PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" /> <PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
@@ -165,6 +165,37 @@ public abstract class ExportCommandBase : DiscordCommandBase
throw new CommandException("Option --media-dir cannot be used without --media."); throw new CommandException("Option --media-dir cannot be used without --media.");
} }
// Make sure the user does not try to export multiple channels into one file.
// Output path must either be a directory or contain template tokens for this to work.
// Validate this up-front, before fetching threads, because thread fetching can take a
// long time and it's frustrating to fail only after it completes.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
// https://github.com/Tyrrrz/DiscordChatExporter/issues/917
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1549
var mayExportMultipleChannels =
// Multiple channels were provided explicitly
channels.Count > 1
// Thread inclusion can add more channels to the export
|| ThreadInclusionMode != ThreadInclusionMode.None;
var isValidOutputPath =
// Anything is valid when exporting a single channel
!mayExportMultipleChannels
// When using template tokens, assume the user knows what they're doing
|| OutputPath.Contains('%')
// Otherwise, require an existing directory or an unambiguous directory path
|| Directory.Exists(OutputPath)
|| Path.EndsInDirectorySeparator(OutputPath);
if (!isValidOutputPath)
{
throw new CommandException(
"Attempted to export multiple channels, but the output path is neither a directory nor a template. "
+ "If the provided output path is meant to be treated as a directory, make sure it ends with a slash. "
+ $"Provided output path: '{OutputPath}'."
);
}
var unwrappedChannels = new List<Channel>(channels); var unwrappedChannels = new List<Channel>(channels);
// Unwrap threads // Unwrap threads
@@ -205,28 +236,6 @@ public abstract class ExportCommandBase : DiscordCommandBase
await console.Output.WriteLineAsync($"Fetched {fetchedThreadsCount} thread(s)."); await console.Output.WriteLineAsync($"Fetched {fetchedThreadsCount} thread(s).");
} }
// Make sure the user does not try to export multiple channels into one file.
// Output path must either be a directory or contain template tokens for this to work.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
// https://github.com/Tyrrrz/DiscordChatExporter/issues/917
var isValidOutputPath =
// Anything is valid when exporting a single channel
unwrappedChannels.Count <= 1
// When using template tokens, assume the user knows what they're doing
|| OutputPath.Contains('%')
// Otherwise, require an existing directory or an unambiguous directory path
|| Directory.Exists(OutputPath)
|| Path.EndsInDirectorySeparator(OutputPath);
if (!isValidOutputPath)
{
throw new CommandException(
"Attempted to export multiple channels, but the output path is neither a directory nor a template. "
+ "If the provided output path is meant to be treated as a directory, make sure it ends with a slash. "
+ $"Provided output path: '{OutputPath}'."
);
}
// Export // Export
var errorsByChannel = new ConcurrentDictionary<Channel, string>(); var errorsByChannel = new ConcurrentDictionary<Channel, string>();
var warningsByChannel = new ConcurrentDictionary<Channel, string>(); var warningsByChannel = new ConcurrentDictionary<Channel, string>();
@@ -9,9 +9,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CliFx" /> <PackageReference Include="CliFx" />
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" /> <PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
<PackageReference Include="Deorcify" PrivateAssets="all" /> <PackageReference Include="Deorcify" />
<PackageReference Include="Gress" /> <PackageReference Include="Gress" />
<PackageReference Include="PowerKit" PrivateAssets="all" /> <PackageReference Include="PowerKit" />
<PackageReference Include="Spectre.Console" /> <PackageReference Include="Spectre.Console" />
</ItemGroup> </ItemGroup>
@@ -6,7 +6,7 @@
<PackageReference Include="Gress" /> <PackageReference Include="Gress" />
<PackageReference Include="JsonExtensions" /> <PackageReference Include="JsonExtensions" />
<PackageReference Include="Polly" /> <PackageReference Include="Polly" />
<PackageReference Include="PowerKit" PrivateAssets="all" /> <PackageReference Include="PowerKit" />
<PackageReference Include="RazorBlade" /> <PackageReference Include="RazorBlade" />
<PackageReference Include="Superpower" /> <PackageReference Include="Superpower" />
<PackageReference Include="WebMarkupMin.Core" /> <PackageReference Include="WebMarkupMin.Core" />
@@ -31,7 +31,7 @@
<PackageReference Include="Cogwheel" /> <PackageReference Include="Cogwheel" />
<PackageReference Include="CommunityToolkit.Mvvm" /> <PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" /> <PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
<PackageReference Include="Deorcify" PrivateAssets="all" /> <PackageReference Include="Deorcify" />
<PackageReference Include="DialogHost.Avalonia" /> <PackageReference Include="DialogHost.Avalonia" />
<PackageReference Include="Gress" /> <PackageReference Include="Gress" />
<PackageReference Include="Markdig" /> <PackageReference Include="Markdig" />
@@ -39,7 +39,7 @@
<PackageReference Include="Material.Icons.Avalonia" /> <PackageReference Include="Material.Icons.Avalonia" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Onova" /> <PackageReference Include="Onova" />
<PackageReference Include="PowerKit" PrivateAssets="all" /> <PackageReference Include="PowerKit" />
<PackageReference Include="ThisAssembly.Project" PrivateAssets="all" /> <PackageReference Include="ThisAssembly.Project" PrivateAssets="all" />
</ItemGroup> </ItemGroup>