diff --git a/DiscordChatExporter.Domain/Exporting/ExportContext.cs b/DiscordChatExporter.Domain/Exporting/ExportContext.cs index f2bd243b..397716c4 100644 --- a/DiscordChatExporter.Domain/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Domain/Exporting/ExportContext.cs @@ -8,6 +8,7 @@ using System.Net.Http; using System.Threading.Tasks; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Internal.Extensions; +using Tyrrrz.Extensions; namespace DiscordChatExporter.Domain.Exporting { @@ -72,7 +73,13 @@ namespace DiscordChatExporter.Domain.Exporting // We want relative path so that the output files can be copied around without breaking var relativeFilePath = Path.GetRelativePath(Request.OutputBaseDirPath, filePath); - return $"file:///./{Uri.EscapeDataString(relativeFilePath)}"; + // Need to properly escape each path segment while keeping the slashes + var escapedRelativeFilePath = relativeFilePath + .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .Select(Uri.EscapeDataString) + .JoinToString(Path.AltDirectorySeparatorChar.ToString()); + + return escapedRelativeFilePath; } catch (HttpRequestException) { diff --git a/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs b/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs index e1be5845..570284b6 100644 --- a/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs +++ b/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs @@ -9,7 +9,7 @@ using DiscordChatExporter.Domain.Internal.Extensions; namespace DiscordChatExporter.Domain.Exporting { - internal class MediaDownloader + internal partial class MediaDownloader { private readonly HttpClient _httpClient = Singleton.HttpClient; private readonly string _workingDirPath; @@ -21,33 +21,35 @@ namespace DiscordChatExporter.Domain.Exporting _workingDirPath = workingDirPath; } - private string GetRandomSuffix() => Guid.NewGuid().ToString().Replace("-", "").Substring(0, 8); - - private string GetFileNameFromUrl(string url) - { - var originalFileName = Regex.Match(url, @".+/([^?]*)").Groups[1].Value; - - var fileName = !string.IsNullOrWhiteSpace(originalFileName) ? - $"{Path.GetFileNameWithoutExtension(originalFileName)}-{GetRandomSuffix()}{Path.GetExtension(originalFileName)}" : - GetRandomSuffix(); - - return PathEx.EscapePath(fileName); - } - - // HACK: ConfigureAwait() is crucial here to enable sync-over-async in HtmlMessageWriter public async ValueTask DownloadAsync(string url) { if (_pathMap.TryGetValue(url, out var cachedFilePath)) return cachedFilePath; var fileName = GetFileNameFromUrl(url); - var filePath = Path.Combine(_workingDirPath, fileName); + var filePath = PathEx.MakeUniqueFilePath(Path.Combine(_workingDirPath, fileName)); Directory.CreateDirectory(_workingDirPath); - await _httpClient.DownloadAsync(url, filePath).ConfigureAwait(false); + await _httpClient.DownloadAsync(url, filePath); return _pathMap[url] = filePath; } } + + internal partial class MediaDownloader + { + private static string GetRandomFileName() => Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16); + + private static string GetFileNameFromUrl(string url) + { + var originalFileName = Regex.Match(url, @".+/([^?]*)").Groups[1].Value; + + var fileName = !string.IsNullOrWhiteSpace(originalFileName) + ? originalFileName + : GetRandomFileName(); + + return PathEx.EscapePath(fileName); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Internal/Extensions/HttpClientExtensions.cs b/DiscordChatExporter.Domain/Internal/Extensions/HttpClientExtensions.cs index 0c5335c9..3c88633a 100644 --- a/DiscordChatExporter.Domain/Internal/Extensions/HttpClientExtensions.cs +++ b/DiscordChatExporter.Domain/Internal/Extensions/HttpClientExtensions.cs @@ -7,14 +7,13 @@ namespace DiscordChatExporter.Domain.Internal.Extensions { internal static class HttpClientExtensions { - // HACK: ConfigureAwait() is crucial here to enable sync-over-async in HtmlMessageWriter public static async ValueTask DownloadAsync(this HttpClient httpClient, string uri, string outputFilePath) { - await using var input = await httpClient.GetStreamAsync(uri).ConfigureAwait(false); + await using var input = await httpClient.GetStreamAsync(uri); var output = File.Create(outputFilePath); - await input.CopyToAsync(output).ConfigureAwait(false); - await output.DisposeAsync().ConfigureAwait(false); + await input.CopyToAsync(output); + await output.DisposeAsync(); } public static async ValueTask ReadAsJsonAsync(this HttpContent content) diff --git a/DiscordChatExporter.Domain/Internal/PathEx.cs b/DiscordChatExporter.Domain/Internal/PathEx.cs index 83b5949a..d89c78a6 100644 --- a/DiscordChatExporter.Domain/Internal/PathEx.cs +++ b/DiscordChatExporter.Domain/Internal/PathEx.cs @@ -14,5 +14,27 @@ namespace DiscordChatExporter.Domain.Internal } public static string EscapePath(string path) => EscapePath(new StringBuilder(path)).ToString(); + + public static string MakeUniqueFilePath(string baseFilePath, int maxAttempts = 100) + { + if (!File.Exists(baseFilePath)) + return baseFilePath; + + var baseDirPath = Path.GetDirectoryName(baseFilePath); + var baseFileNameWithoutExtension = Path.GetFileNameWithoutExtension(baseFilePath); + var baseFileExtension = Path.GetExtension(baseFilePath); + + for (var i = 1; i <= maxAttempts; i++) + { + var filePath = $"{baseFileNameWithoutExtension} ({i}){baseFileExtension}"; + if (!string.IsNullOrWhiteSpace(baseDirPath)) + filePath = Path.Combine(baseDirPath, filePath); + + if (!File.Exists(filePath)) + return filePath; + } + + return baseFilePath; + } } } \ No newline at end of file