From 008bb2f5918a9734156835896715d86fb118893d Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:02:11 +0200 Subject: [PATCH] Use .NET 6's `ParallelForEachAsync(...)` --- .../Commands/Base/ExportCommandBase.cs | 64 ++++++++++-------- .../Exporting/ExportRequest.cs | 8 +-- .../Exporting/MediaDownloader.cs | 2 +- .../Utils/Extensions/AsyncExtensions.cs | 30 +-------- DiscordChatExporter.Core/Utils/PathEx.cs | 17 +++-- .../ViewModels/RootViewModel.cs | 67 ++++++++++--------- 6 files changed, 88 insertions(+), 100 deletions(-) diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index f0e68bb..07e70fa 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -14,7 +14,6 @@ using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Core.Exporting.Filtering; using DiscordChatExporter.Core.Exporting.Partitioning; -using DiscordChatExporter.Core.Utils.Extensions; namespace DiscordChatExporter.Cli.Commands.Base; @@ -68,36 +67,47 @@ public abstract class ExportCommandBase : TokenCommandBase await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)..."); await console.CreateProgressTicker().StartAsync(async progressContext => { - await channels.ParallelForEachAsync(async channel => - { - try + await Parallel.ForEachAsync( + channels, + new ParallelOptions + { + MaxDegreeOfParallelism = Math.Max(1, ParallelLimit), + CancellationToken = cancellationToken + }, + async (channel, innerCancellationToken) => { - await progressContext.StartTaskAsync($"{channel.Category.Name} / {channel.Name}", async progress => + try { - var guild = await Discord.GetGuildAsync(channel.GuildId, cancellationToken); - - var request = new ExportRequest( - guild, - channel, - OutputPath, - ExportFormat, - After, - Before, - PartitionLimit, - MessageFilter, - ShouldDownloadMedia, - ShouldReuseMedia, - DateFormat + await progressContext.StartTaskAsync( + $"{channel.Category.Name} / {channel.Name}", + async progress => + { + var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken); + + var request = new ExportRequest( + guild, + channel, + OutputPath, + ExportFormat, + After, + Before, + PartitionLimit, + MessageFilter, + ShouldDownloadMedia, + ShouldReuseMedia, + DateFormat + ); + + await Exporter.ExportChannelAsync(request, progress, innerCancellationToken); + } ); - - await Exporter.ExportChannelAsync(request, progress, cancellationToken); - }); - } - catch (DiscordChatExporterException ex) when (!ex.IsFatal) - { - errors[channel] = ex.Message; + } + catch (DiscordChatExporterException ex) when (!ex.IsFatal) + { + errors[channel] = ex.Message; + } } - }, Math.Max(ParallelLimit, 1), cancellationToken); + ); }); // Print result diff --git a/DiscordChatExporter.Core/Exporting/ExportRequest.cs b/DiscordChatExporter.Core/Exporting/ExportRequest.cs index db61514..960cf85 100644 --- a/DiscordChatExporter.Core/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Core/Exporting/ExportRequest.cs @@ -48,10 +48,9 @@ public partial record ExportRequest Snowflake? after = null, Snowflake? before = null) { - // Formats path outputPath = Regex.Replace(outputPath, "%.", m => - PathEx.EscapePath(m.Value switch + PathEx.EscapeFileName(m.Value switch { "%g" => guild.Id.ToString(), "%G" => guild.Name, @@ -118,9 +117,6 @@ public partial record ExportRequest // File extension buffer.Append($".{format.GetFileExtension()}"); - // Replace invalid chars - PathEx.EscapePath(buffer); - - return buffer.ToString(); + return PathEx.EscapeFileName(buffer.ToString()); } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/MediaDownloader.cs b/DiscordChatExporter.Core/Exporting/MediaDownloader.cs index 38a5b0b..4765de5 100644 --- a/DiscordChatExporter.Core/Exporting/MediaDownloader.cs +++ b/DiscordChatExporter.Core/Exporting/MediaDownloader.cs @@ -104,6 +104,6 @@ internal partial class MediaDownloader var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); var fileExtension = Path.GetExtension(fileName); - return PathEx.EscapePath(fileNameWithoutExtension.Truncate(42) + '-' + urlHash + fileExtension); + return PathEx.EscapeFileName(fileNameWithoutExtension.Truncate(42) + '-' + urlHash + fileExtension); } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Utils/Extensions/AsyncExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/AsyncExtensions.cs index b5d2707..5583ec2 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/AsyncExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/AsyncExtensions.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; namespace DiscordChatExporter.Core.Utils.Extensions; @@ -23,29 +20,4 @@ public static class AsyncExtensions public static ValueTaskAwaiter> GetAwaiter( this IAsyncEnumerable asyncEnumerable) => asyncEnumerable.AggregateAsync().GetAwaiter(); - - public static async ValueTask ParallelForEachAsync( - this IEnumerable source, - Func handleAsync, - int degreeOfParallelism, - CancellationToken cancellationToken = default) - { - using var semaphore = new SemaphoreSlim(degreeOfParallelism); - - await Task.WhenAll(source.Select(async item => - { - // ReSharper disable once AccessToDisposedClosure - await semaphore.WaitAsync(cancellationToken); - - try - { - await handleAsync(item); - } - finally - { - // ReSharper disable once AccessToDisposedClosure - semaphore.Release(); - } - })); - } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Utils/PathEx.cs b/DiscordChatExporter.Core/Utils/PathEx.cs index 06b5709..39f5b2b 100644 --- a/DiscordChatExporter.Core/Utils/PathEx.cs +++ b/DiscordChatExporter.Core/Utils/PathEx.cs @@ -1,17 +1,20 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Text; namespace DiscordChatExporter.Core.Utils; public static class PathEx { - public static StringBuilder EscapePath(StringBuilder pathBuffer) + private static readonly HashSet InvalidFileNameChars = new(Path.GetInvalidFileNameChars()); + + public static string EscapeFileName(string path) { - foreach (var invalidChar in Path.GetInvalidFileNameChars()) - pathBuffer.Replace(invalidChar, '_'); + var buffer = new StringBuilder(path.Length); - return pathBuffer; - } + foreach (var c in path) + buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_'); - public static string EscapePath(string path) => EscapePath(new StringBuilder(path)).ToString(); + return buffer.ToString(); + } } \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs index 7252984..d611836 100644 --- a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -210,39 +210,46 @@ public class RootViewModel : Screen var operations = ProgressManager.CreateOperations(dialog.Channels!.Count); var successfulExportCount = 0; - await dialog.Channels.Zip(operations).ParallelForEachAsync(async tuple => - { - var (channel, operation) = tuple; - - try - { - var request = new ExportRequest( - dialog.Guild!, - channel!, - dialog.OutputPath!, - dialog.SelectedFormat, - dialog.After?.Pipe(Snowflake.FromDate), - dialog.Before?.Pipe(Snowflake.FromDate), - dialog.PartitionLimit, - dialog.MessageFilter, - dialog.ShouldDownloadMedia, - _settingsService.ShouldReuseMedia, - _settingsService.DateFormat - ); - - await exporter.ExportChannelAsync(request, operation); - - Interlocked.Increment(ref successfulExportCount); - } - catch (DiscordChatExporterException ex) when (!ex.IsFatal) + await Parallel.ForEachAsync( + dialog.Channels.Zip(operations), + new ParallelOptions { - Notifications.Enqueue(ex.Message.TrimEnd('.')); - } - finally + MaxDegreeOfParallelism = Math.Max(1, _settingsService.ParallelLimit) + }, + async (tuple, cancellationToken) => { - operation.Dispose(); + var (channel, operation) = tuple; + + try + { + var request = new ExportRequest( + dialog.Guild!, + channel, + dialog.OutputPath!, + dialog.SelectedFormat, + dialog.After?.Pipe(Snowflake.FromDate), + dialog.Before?.Pipe(Snowflake.FromDate), + dialog.PartitionLimit, + dialog.MessageFilter, + dialog.ShouldDownloadMedia, + _settingsService.ShouldReuseMedia, + _settingsService.DateFormat + ); + + await exporter.ExportChannelAsync(request, operation, cancellationToken); + + Interlocked.Increment(ref successfulExportCount); + } + catch (DiscordChatExporterException ex) when (!ex.IsFatal) + { + Notifications.Enqueue(ex.Message.TrimEnd('.')); + } + finally + { + operation.Dispose(); + } } - }, Math.Max(1, _settingsService.ParallelLimit)); + ); // Notify of overall completion if (successfulExportCount > 0)