diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index 9476acbb..57b4ee49 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -165,6 +165,37 @@ public abstract class ExportCommandBase : DiscordCommandBase 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(channels); // Unwrap threads @@ -205,28 +236,6 @@ public abstract class ExportCommandBase : DiscordCommandBase 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 var errorsByChannel = new ConcurrentDictionary(); var warningsByChannel = new ConcurrentDictionary(); diff --git a/DiscordChatExporter.Core/Exporting/ExportRequest.cs b/DiscordChatExporter.Core/Exporting/ExportRequest.cs index db83d98a..1285b246 100644 --- a/DiscordChatExporter.Core/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Core/Exporting/ExportRequest.cs @@ -206,10 +206,15 @@ public partial class ExportRequest { var actualOutputPath = FormatPath(outputPath, guild, channel, after, before); - // Output is a directory + // Determine whether the output path refers to a directory or a file. + // The extension-based heuristic is evaluated on the original, unsubstituted path, + // because the value of a template token (e.g. a guild or channel name) may contain + // a period that would otherwise be mistaken for a file extension, incorrectly causing + // a directory path to be treated as a file. + // https://github.com/Tyrrrz/DiscordChatExporter/issues/1502 if ( Directory.Exists(actualOutputPath) - || string.IsNullOrWhiteSpace(Path.GetExtension(actualOutputPath)) + || string.IsNullOrWhiteSpace(Path.GetExtension(outputPath)) ) { var fileName = GetDefaultOutputFileName(guild, channel, format, after, before);