From 51c23dc763bade09e417acdccc042cc6eaab6278 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 8 Dec 2025 13:25:31 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20ExtractToDirec?= =?UTF-8?q?toryAsync=20=E5=BC=82=E6=AD=A5=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultZipArchiveService.cs | 98 +++++++++++++++++-- .../Services/IZipArchiveService.cs | 30 +++++- 2 files changed, 114 insertions(+), 14 deletions(-) diff --git a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs index e872ab3eeda..f438298afa0 100644 --- a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs +++ b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs @@ -1,14 +1,15 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone +using Microsoft.Extensions.Logging; using System.IO.Compression; using System.Text; namespace BootstrapBlazor.Components; -class DefaultZipArchiveService : IZipArchiveService +class DefaultZipArchiveService(ILogger logger) : IZipArchiveService { /// /// @@ -29,12 +30,12 @@ public async Task ArchiveAsync(IEnumerable files, ArchiveOptions /// /// /// - /// 归档文件 + /// 归档文件 /// 要归档的文件集合 /// 归档配置 - public async Task ArchiveAsync(string archiveFileName, IEnumerable files, ArchiveOptions? options = null) + public async Task ArchiveAsync(string archiveFile, IEnumerable files, ArchiveOptions? options = null) { - using var stream = File.OpenWrite(archiveFileName); + using var stream = File.OpenWrite(archiveFile); await ArchiveFilesAsync(stream, files, options); } @@ -61,23 +62,70 @@ private static async Task ArchiveFilesAsync(Stream stream, IEnumerable f /// /// /// - /// 归档文件 + /// 归档文件 /// 要归档文件夹 /// /// /// /// /// - public async Task ArchiveDirectory(string archiveFileName, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null) + public async Task ArchiveDirectory(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null) { if (Directory.Exists(directoryName)) { - var folder = Path.GetDirectoryName(archiveFileName); + var folder = Path.GetDirectoryName(archiveFile); if (!string.IsNullOrEmpty(folder) && !Directory.Exists(folder)) { Directory.CreateDirectory(folder); } - await Task.Run(() => ZipFile.CreateFromDirectory(directoryName, archiveFileName, compressionLevel, includeBaseDirectory, encoding)); +#if NET10_0_OR_GREATER + await ZipFile.CreateFromDirectoryAsync(directoryName, archiveFile, compressionLevel, includeBaseDirectory, encoding); +#else + await Task.Run(() => ZipFile.CreateFromDirectory(directoryName, archiveFile, compressionLevel, includeBaseDirectory, encoding)); +#endif + } + } + + /// + /// + /// + /// 归档文件 + /// + /// + /// + /// + /// + public async Task ArchiveDirectory(string archiveFile, IEnumerable entries, CompressionLevel compressionLevel = CompressionLevel.Optimal, Encoding? encoding = null) + { + using var archive = ZipFile.Open(archiveFile, ZipArchiveMode.Create, encoding); + + foreach (var entry in entries) + { + if (Directory.Exists(entry)) + { + AddFolderToZip(archive, entry, Path.GetFileName(entry), compressionLevel); + } + else if (File.Exists(entry)) + { + archive.CreateEntryFromFile(entry, Path.GetFileName(entry), compressionLevel); + } + } + } + + private static void AddFolderToZip(ZipArchive archive, string folderPath, string relativePath, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + // 添加当前文件夹中的所有文件 + foreach (string filePath in Directory.GetFiles(folderPath)) + { + string entryName = Path.Combine(relativePath, Path.GetFileName(filePath)); + archive.CreateEntryFromFile(filePath, entryName, compressionLevel); + } + + // 递归添加所有子文件夹 + foreach (string subfolderPath in Directory.GetDirectories(folderPath)) + { + string newRelativePath = Path.Combine(relativePath, Path.GetFileName(subfolderPath)); + AddFolderToZip(archive, subfolderPath, newRelativePath, compressionLevel); } } @@ -99,6 +147,38 @@ public bool ExtractToDirectory(string archiveFile, string destinationDirectoryNa return true; } + /// + /// + /// + /// 归档文件 + /// 解压缩文件夹 + /// 是否覆盖文件 默认 false 不覆盖 + /// 编码方式 默认 null 内部使用 UTF-8 + /// + public async Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null) + { + var ret = false; + try + { + if (!Directory.Exists(destinationDirectoryName)) + { + Directory.CreateDirectory(destinationDirectoryName); + } + +#if NET10_0_OR_GREATER + await ZipFile.ExtractToDirectoryAsync(archiveFile, destinationDirectoryName, encoding, overwriteFiles); +#else + await Task.Run(() => ZipFile.ExtractToDirectory(archiveFile, destinationDirectoryName, encoding, overwriteFiles)); +#endif + ret = true; + } + catch (Exception ex) + { + logger.LogError(ex, "ExtractToDirectoryAsync"); + } + return ret; + } + /// /// /// diff --git a/src/BootstrapBlazor/Services/IZipArchiveService.cs b/src/BootstrapBlazor/Services/IZipArchiveService.cs index 2514f9b6c17..5a7361a5b8d 100644 --- a/src/BootstrapBlazor/Services/IZipArchiveService.cs +++ b/src/BootstrapBlazor/Services/IZipArchiveService.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone @@ -24,21 +24,31 @@ public interface IZipArchiveService /// /// 将文件归档方法 /// - /// 归档文件 + /// 归档文件 /// 要归档的文件集合 /// 归档配置 - Task ArchiveAsync(string archiveFileName, IEnumerable files, ArchiveOptions? options = null); + Task ArchiveAsync(string archiveFile, IEnumerable files, ArchiveOptions? options = null); /// /// 将指定目录归档方法 /// - /// 归档文件 + /// 归档文件 /// 要归档文件夹 /// /// /// /// - Task ArchiveDirectory(string archiveFileName, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null); + Task ArchiveDirectory(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null); + + /// + /// 将指定目录归档方法 + /// + /// 归档文件 + /// 要归档条目 + /// + /// + /// + Task ArchiveDirectory(string archiveFile, IEnumerable entries, CompressionLevel compressionLevel = CompressionLevel.Optimal, Encoding? encoding = null); /// /// 解压缩归档文件到指定文件夹 @@ -50,6 +60,16 @@ public interface IZipArchiveService /// bool ExtractToDirectory(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null); + /// + /// 解压缩归档文件到指定文件夹异步方法 + /// + /// 归档文件 + /// 解压缩文件夹 + /// 是否覆盖文件 默认 false 不覆盖 + /// 编码方式 默认 null 内部使用 UTF-8 + /// + Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null); + /// /// 获得归档压缩文件中指定归档文件 /// From ea5f21516d98a21cd53f78a55f8fa49a461927d0 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 8 Dec 2025 13:26:47 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultZipArchiveService.cs | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs index f438298afa0..a6fe84353fd 100644 --- a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs +++ b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs @@ -3,13 +3,12 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using Microsoft.Extensions.Logging; using System.IO.Compression; using System.Text; namespace BootstrapBlazor.Components; -class DefaultZipArchiveService(ILogger logger) : IZipArchiveService +class DefaultZipArchiveService : IZipArchiveService { /// /// @@ -157,26 +156,17 @@ public bool ExtractToDirectory(string archiveFile, string destinationDirectoryNa /// public async Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null) { - var ret = false; - try + if (!Directory.Exists(destinationDirectoryName)) { - if (!Directory.Exists(destinationDirectoryName)) - { - Directory.CreateDirectory(destinationDirectoryName); - } + Directory.CreateDirectory(destinationDirectoryName); + } #if NET10_0_OR_GREATER - await ZipFile.ExtractToDirectoryAsync(archiveFile, destinationDirectoryName, encoding, overwriteFiles); + await ZipFile.ExtractToDirectoryAsync(archiveFile, destinationDirectoryName, encoding, overwriteFiles); #else await Task.Run(() => ZipFile.ExtractToDirectory(archiveFile, destinationDirectoryName, encoding, overwriteFiles)); #endif - ret = true; - } - catch (Exception ex) - { - logger.LogError(ex, "ExtractToDirectoryAsync"); - } - return ret; + return true; } /// From 0ffea9b041ebe16166baedb1578b07b0cc6890fd Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 8 Dec 2025 13:26:58 +0800 Subject: [PATCH 3/6] chore: bump version 10.1.2 --- src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 0309e7a6e8d..1c2fc60513f 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 10.1.2-beta01 + 10.1.2 From 33035177499d17f4b3bb126bef4d57cdab5d4cc8 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 8 Dec 2025 14:04:30 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E4=BB=A4=E7=89=8C=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DefaultZipArchiveService.cs | 40 +++++++++++++------ .../Services/IZipArchiveService.cs | 20 ++++++---- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs index a6fe84353fd..9be54392501 100644 --- a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs +++ b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs @@ -61,14 +61,15 @@ private static async Task ArchiveFilesAsync(Stream stream, IEnumerable f /// /// /// - /// 归档文件 - /// 要归档文件夹 + /// + /// /// /// /// + /// /// /// - public async Task ArchiveDirectory(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null) + public async Task ArchiveDirectory(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null, CancellationToken token = default) { if (Directory.Exists(directoryName)) { @@ -78,9 +79,13 @@ public async Task ArchiveDirectory(string archiveFile, string directoryName, Com Directory.CreateDirectory(folder); } #if NET10_0_OR_GREATER - await ZipFile.CreateFromDirectoryAsync(directoryName, archiveFile, compressionLevel, includeBaseDirectory, encoding); + await ZipFile.CreateFromDirectoryAsync(directoryName, archiveFile, compressionLevel, includeBaseDirectory, encoding, token); #else - await Task.Run(() => ZipFile.CreateFromDirectory(directoryName, archiveFile, compressionLevel, includeBaseDirectory, encoding)); + await Task.Run(() => + { + token.ThrowIfCancellationRequested(); + ZipFile.CreateFromDirectory(directoryName, archiveFile, compressionLevel, includeBaseDirectory, encoding); + }, token); #endif } } @@ -92,9 +97,11 @@ public async Task ArchiveDirectory(string archiveFile, string directoryName, Com /// /// /// + /// + /// /// /// - public async Task ArchiveDirectory(string archiveFile, IEnumerable entries, CompressionLevel compressionLevel = CompressionLevel.Optimal, Encoding? encoding = null) + public async Task ArchiveDirectory(string archiveFile, IEnumerable entries, CompressionLevel compressionLevel = CompressionLevel.Optimal, Encoding? encoding = null, bool skipEmptyFolder = false, CancellationToken token = default) { using var archive = ZipFile.Open(archiveFile, ZipArchiveMode.Create, encoding); @@ -113,6 +120,8 @@ public async Task ArchiveDirectory(string archiveFile, IEnumerable entri private static void AddFolderToZip(ZipArchive archive, string folderPath, string relativePath, CompressionLevel compressionLevel = CompressionLevel.Optimal) { + archive.CreateEntry($"{relativePath}/", compressionLevel); + // 添加当前文件夹中的所有文件 foreach (string filePath in Directory.GetFiles(folderPath)) { @@ -149,12 +158,13 @@ public bool ExtractToDirectory(string archiveFile, string destinationDirectoryNa /// /// /// - /// 归档文件 - /// 解压缩文件夹 - /// 是否覆盖文件 默认 false 不覆盖 - /// 编码方式 默认 null 内部使用 UTF-8 + /// + /// + /// + /// + /// /// - public async Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null) + public async Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null, CancellationToken token = default) { if (!Directory.Exists(destinationDirectoryName)) { @@ -162,9 +172,13 @@ public async Task ExtractToDirectoryAsync(string archiveFile, string desti } #if NET10_0_OR_GREATER - await ZipFile.ExtractToDirectoryAsync(archiveFile, destinationDirectoryName, encoding, overwriteFiles); + await ZipFile.ExtractToDirectoryAsync(archiveFile, destinationDirectoryName, encoding, overwriteFiles, token); #else - await Task.Run(() => ZipFile.ExtractToDirectory(archiveFile, destinationDirectoryName, encoding, overwriteFiles)); + await Task.Run(() => + { + token.ThrowIfCancellationRequested(); + ZipFile.ExtractToDirectory(archiveFile, destinationDirectoryName, encoding, overwriteFiles); + }, token); #endif return true; } diff --git a/src/BootstrapBlazor/Services/IZipArchiveService.cs b/src/BootstrapBlazor/Services/IZipArchiveService.cs index 5a7361a5b8d..e75593ffd26 100644 --- a/src/BootstrapBlazor/Services/IZipArchiveService.cs +++ b/src/BootstrapBlazor/Services/IZipArchiveService.cs @@ -34,21 +34,24 @@ public interface IZipArchiveService /// /// 归档文件 /// 要归档文件夹 - /// - /// - /// + /// 压缩率 + /// 是否包含本目录 默认 false + /// 编码方式 默认 null 内部使用 UTF-8 + /// /// - Task ArchiveDirectory(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null); + Task ArchiveDirectory(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null, CancellationToken token = default); /// /// 将指定目录归档方法 /// /// 归档文件 /// 要归档条目 - /// - /// + /// 压缩率 + /// 编码方式 默认 null 内部使用 UTF-8 + /// 是否跳过空文件夹 + /// /// - Task ArchiveDirectory(string archiveFile, IEnumerable entries, CompressionLevel compressionLevel = CompressionLevel.Optimal, Encoding? encoding = null); + Task ArchiveDirectory(string archiveFile, IEnumerable entries, CompressionLevel compressionLevel = CompressionLevel.Optimal, Encoding? encoding = null, bool skipEmptyFolder = false, CancellationToken token = default); /// /// 解压缩归档文件到指定文件夹 @@ -67,8 +70,9 @@ public interface IZipArchiveService /// 解压缩文件夹 /// 是否覆盖文件 默认 false 不覆盖 /// 编码方式 默认 null 内部使用 UTF-8 + /// /// - Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null); + Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null, CancellationToken token = default); /// /// 获得归档压缩文件中指定归档文件 From 435cec3b6787d17c86aee7993c5303016bd79e16 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 8 Dec 2025 14:04:39 +0800 Subject: [PATCH 5/6] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ZipArchiveServiceTest.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/UnitTest/Services/ZipArchiveServiceTest.cs b/test/UnitTest/Services/ZipArchiveServiceTest.cs index c4b7c2e0ab3..2514965de66 100644 --- a/test/UnitTest/Services/ZipArchiveServiceTest.cs +++ b/test/UnitTest/Services/ZipArchiveServiceTest.cs @@ -51,6 +51,14 @@ public async Task Archive_Ok() archService.ExtractToDirectory(archiveFile, destFolder); Assert.True(Directory.Exists(destFolder)); + // 删除文件夹 + Directory.Delete(destFolder, true); + Assert.False(Directory.Exists(destFolder)); + + // 异步解压缩单元测试 + await archService.ExtractToDirectoryAsync(archiveFile, destFolder); + Assert.True(Directory.Exists(destFolder)); + // 打包文件夹单元测试 var tempFolder = Path.Combine(root, "test_temp"); if (Directory.Exists(tempFolder)) @@ -67,5 +75,22 @@ public async Task Archive_Ok() File.Delete(destFile); await Assert.ThrowsAsync(() => archService.ArchiveDirectory(null!, destFolder, includeBaseDirectory: true)); + + // 测试压缩多个文件夹 + var entries = new List() + { + destFolder, + tempFolder, + }; + entries.AddRange(files); + + destFile = Path.Combine(root, "folder.zip"); + if (File.Exists(destFile)) + { + File.Delete(destFile); + } + Assert.False(File.Exists(destFile)); + await archService.ArchiveDirectory(destFile, entries); + Assert.True(File.Exists(destFile)); } } From 1cb3175516a379349423d7104f6da7de77c81db5 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 8 Dec 2025 14:08:10 +0800 Subject: [PATCH 6/6] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Services/ZipArchiveServiceTest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/UnitTest/Services/ZipArchiveServiceTest.cs b/test/UnitTest/Services/ZipArchiveServiceTest.cs index 2514965de66..142bc991f5b 100644 --- a/test/UnitTest/Services/ZipArchiveServiceTest.cs +++ b/test/UnitTest/Services/ZipArchiveServiceTest.cs @@ -90,6 +90,11 @@ public async Task Archive_Ok() File.Delete(destFile); } Assert.False(File.Exists(destFile)); + var subFolder = Path.Combine(tempFolder, "sub"); + if (!Directory.Exists(subFolder)) + { + Directory.CreateDirectory(subFolder); + } await archService.ArchiveDirectory(destFile, entries); Assert.True(File.Exists(destFile)); }