diff --git a/src/BootstrapBlazor/Services/ArchiveEntry.cs b/src/BootstrapBlazor/Services/ArchiveEntry.cs new file mode 100644 index 00000000000..efd21c63717 --- /dev/null +++ b/src/BootstrapBlazor/Services/ArchiveEntry.cs @@ -0,0 +1,27 @@ +// 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 System.IO.Compression; + +/// +/// 归档项实体类 +/// +public readonly record struct ArchiveEntry +{ + /// + /// 获得 物理文件 + /// + public string SourceFileName { get; init; } + + /// + /// 获得 归档项 + /// + public string EntryName { get; init; } + + /// + /// 获得 压缩配置 + /// + public CompressionLevel? CompressionLevel { get; init; } +} diff --git a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs index 9be54392501..7316bcf7c05 100644 --- a/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs +++ b/src/BootstrapBlazor/Services/DefaultZipArchiveService.cs @@ -13,15 +13,12 @@ class DefaultZipArchiveService : IZipArchiveService /// /// /// - /// 要归档的文件集合 - /// 归档配置 - /// 归档数据流 - public async Task ArchiveAsync(IEnumerable files, ArchiveOptions? options = null) + public async Task ArchiveAsync(IEnumerable entries, ArchiveOptions? options = null) { var stream = new MemoryStream(); options ??= new ArchiveOptions(); options.LeaveOpen = true; - await ArchiveFilesAsync(stream, files, options); + await ArchiveFilesAsync(stream, entries, options); stream.Position = 0; return stream; } @@ -29,31 +26,40 @@ public async Task ArchiveAsync(IEnumerable files, ArchiveOptions /// /// /// - /// 归档文件 - /// 要归档的文件集合 - /// 归档配置 - public async Task ArchiveAsync(string archiveFile, IEnumerable files, ArchiveOptions? options = null) + public async Task ArchiveAsync(string archiveFile, IEnumerable entries, ArchiveOptions? options = null) { using var stream = File.OpenWrite(archiveFile); - await ArchiveFilesAsync(stream, files, options); + await ArchiveFilesAsync(stream, entries, options); } - private static async Task ArchiveFilesAsync(Stream stream, IEnumerable files, ArchiveOptions? options = null) + private static async Task ArchiveFilesAsync(Stream stream, IEnumerable entries, ArchiveOptions? options = null) { options ??= new ArchiveOptions(); using var archive = new ZipArchive(stream, options.Mode, options.LeaveOpen, options.Encoding); - foreach (var f in files) + foreach (var f in entries) { if (options.ReadStreamAsync != null) { - var entry = archive.CreateEntry(Path.GetFileName(f), options.CompressionLevel); - using var entryStream = entry.Open(); - await using var content = await options.ReadStreamAsync(f); + var entry = archive.CreateEntry(f.EntryName, options.CompressionLevel); + await using var content = await options.ReadStreamAsync(f.SourceFileName); + await using var entryStream = entry.Open(); await content.CopyToAsync(entryStream); } - else + else if (Directory.Exists(f.SourceFileName)) { - archive.CreateEntryFromFile(f, Path.GetFileName(f), options.CompressionLevel); + var entryName = f.EntryName; + if (!string.IsNullOrEmpty(entryName)) + { + if (!entryName.EndsWith('/')) + { + entryName = $"{entryName}/"; + } + archive.CreateEntry(entryName, f.CompressionLevel ?? options.CompressionLevel); + } + } + else if (File.Exists(f.SourceFileName)) + { + archive.CreateEntryFromFile(f.SourceFileName, f.EntryName, f.CompressionLevel ?? options.CompressionLevel); } } } @@ -61,15 +67,7 @@ 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, CancellationToken token = default) + public async Task ArchiveDirectoryAsync(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null, CancellationToken token = default) { if (Directory.Exists(directoryName)) { @@ -93,77 +91,6 @@ await Task.Run(() => /// /// /// - /// 归档文件 - /// - /// - /// - /// - /// - /// - /// - 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); - - 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) - { - archive.CreateEntry($"{relativePath}/", compressionLevel); - - // 添加当前文件夹中的所有文件 - 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); - } - } - - /// - /// - /// - /// 归档文件 - /// 解压缩文件夹 - /// 是否覆盖文件 默认 false 不覆盖 - /// 编码方式 默认 null 内部使用 UTF-8 - /// - public bool ExtractToDirectory(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null) - { - if (!Directory.Exists(destinationDirectoryName)) - { - Directory.CreateDirectory(destinationDirectoryName); - } - ZipFile.ExtractToDirectory(archiveFile, destinationDirectoryName, encoding, overwriteFiles); - return true; - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// public async Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null, CancellationToken token = default) { if (!Directory.Exists(destinationDirectoryName)) @@ -186,11 +113,6 @@ await Task.Run(() => /// /// /// - /// 归档文件 - /// 解压缩文件 - /// 是否覆盖文件 默认 false 不覆盖 - /// 编码方式 默认 null 内部使用 UTF-8 - /// public ZipArchiveEntry? GetEntry(string archiveFile, string entryFile, bool overwriteFiles = false, Encoding? encoding = null) { using var archive = ZipFile.Open(archiveFile, ZipArchiveMode.Read, encoding); diff --git a/src/BootstrapBlazor/Services/IZipArchiveService.cs b/src/BootstrapBlazor/Services/IZipArchiveService.cs index e75593ffd26..1c07398afbe 100644 --- a/src/BootstrapBlazor/Services/IZipArchiveService.cs +++ b/src/BootstrapBlazor/Services/IZipArchiveService.cs @@ -16,18 +16,18 @@ public interface IZipArchiveService /// /// 将文件归档方法 /// - /// 要归档的文件集合 + /// 要归档项集合 /// 归档配置 /// 归档数据流 - Task ArchiveAsync(IEnumerable files, ArchiveOptions? options = null); + Task ArchiveAsync(IEnumerable entries, ArchiveOptions? options = null); /// /// 将文件归档方法 /// /// 归档文件 - /// 要归档的文件集合 + /// 要归档项集合 /// 归档配置 - Task ArchiveAsync(string archiveFile, IEnumerable files, ArchiveOptions? options = null); + Task ArchiveAsync(string archiveFile, IEnumerable entries, ArchiveOptions? options = null); /// /// 将指定目录归档方法 @@ -38,30 +38,7 @@ public interface IZipArchiveService /// 是否包含本目录 默认 false /// 编码方式 默认 null 内部使用 UTF-8 /// - /// - 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, bool skipEmptyFolder = false, CancellationToken token = default); - - /// - /// 解压缩归档文件到指定文件夹 - /// - /// 归档文件 - /// 解压缩文件夹 - /// 是否覆盖文件 默认 false 不覆盖 - /// 编码方式 默认 null 内部使用 UTF-8 - /// - bool ExtractToDirectory(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null); + Task ArchiveDirectoryAsync(string archiveFile, string directoryName, CompressionLevel compressionLevel = CompressionLevel.Optimal, bool includeBaseDirectory = false, Encoding? encoding = null, CancellationToken token = default); /// /// 解压缩归档文件到指定文件夹异步方法 @@ -71,7 +48,6 @@ public interface IZipArchiveService /// 是否覆盖文件 默认 false 不覆盖 /// 编码方式 默认 null 内部使用 UTF-8 /// - /// Task ExtractToDirectoryAsync(string archiveFile, string destinationDirectoryName, bool overwriteFiles = false, Encoding? encoding = null, CancellationToken token = default); /// @@ -81,6 +57,5 @@ public interface IZipArchiveService /// 解压缩文件 /// 是否覆盖文件 默认 false 不覆盖 /// 编码方式 默认 null 内部使用 UTF-8 - /// ZipArchiveEntry? GetEntry(string archiveFile, string entryFile, bool overwriteFiles = false, Encoding? encoding = null); } diff --git a/test/UnitTest/Services/ZipArchiveServiceTest.cs b/test/UnitTest/Services/ZipArchiveServiceTest.cs index 142bc991f5b..0e5bd852e74 100644 --- a/test/UnitTest/Services/ZipArchiveServiceTest.cs +++ b/test/UnitTest/Services/ZipArchiveServiceTest.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone +using System.IO.Compression; + namespace UnitTest.Services; public class ZipArchiveServiceTest : BootstrapBlazorTestBase @@ -22,10 +24,15 @@ public async Task Archive_Ok() using var fs = File.OpenWrite(f); fs.WriteByte(65); }); - var stream = await archService.ArchiveAsync(files); + var items = files.Select(i => new ArchiveEntry() + { + SourceFileName = i, + EntryName = Path.GetFileName(i) + }); + var stream = await archService.ArchiveAsync(items); Assert.NotNull(stream); - stream = await archService.ArchiveAsync(files, new ArchiveOptions() + stream = await archService.ArchiveAsync(items, new ArchiveOptions() { CompressionLevel = System.IO.Compression.CompressionLevel.Optimal, Encoding = System.Text.Encoding.UTF8, @@ -35,7 +42,7 @@ public async Task Archive_Ok() Assert.NotNull(stream); var archiveFile = Path.Combine(root, "test.zip"); - await archService.ArchiveAsync(archiveFile, files); + await archService.ArchiveAsync(archiveFile, items); Assert.True(File.Exists(archiveFile)); // GetEntry @@ -48,7 +55,7 @@ public async Task Archive_Ok() { Directory.Delete(destFolder, true); } - archService.ExtractToDirectory(archiveFile, destFolder); + await archService.ExtractToDirectoryAsync(archiveFile, destFolder); Assert.True(Directory.Exists(destFolder)); // 删除文件夹 @@ -70,32 +77,83 @@ public async Task Archive_Ok() { File.Delete(destFile); } - await archService.ArchiveDirectory(destFile, destFolder, includeBaseDirectory: true); + await archService.ArchiveDirectoryAsync(destFile, destFolder, includeBaseDirectory: true); Assert.True(File.Exists(destFile)); File.Delete(destFile); - await Assert.ThrowsAsync(() => archService.ArchiveDirectory(null!, destFolder, includeBaseDirectory: true)); + await Assert.ThrowsAsync(() => archService.ArchiveDirectoryAsync(null!, destFolder, includeBaseDirectory: true)); + } - // 测试压缩多个文件夹 - var entries = new List() + [Fact] + public async Task ZipArchive_Ok() + { + var fileName = Path.Combine(AppContext.BaseDirectory, "test", "3.zip"); + if (File.Exists(fileName)) { - destFolder, - tempFolder, - }; - entries.AddRange(files); + File.Delete(fileName); + } - destFile = Path.Combine(root, "folder.zip"); - if (File.Exists(destFile)) + using var fs = File.OpenWrite(fileName); + using var zip = new ZipArchive(fs, ZipArchiveMode.Create); + + var item = Path.Combine(AppContext.BaseDirectory, "test", "1.txt"); + zip.CreateEntry("text/"); + await zip.CreateEntryFromFileAsync(item, "text/1.txt"); + } + + [Fact] + public async Task ArchiveAsync_Ok() + { + var fileName = Path.Combine(AppContext.BaseDirectory, "archive_test", "test.zip"); + if (File.Exists(fileName)) { - File.Delete(destFile); + File.Delete(fileName); } - Assert.False(File.Exists(destFile)); - var subFolder = Path.Combine(tempFolder, "sub"); - if (!Directory.Exists(subFolder)) + + var root = AppContext.BaseDirectory; + var files = new string[] { - Directory.CreateDirectory(subFolder); - } - await archService.ArchiveDirectory(destFile, entries); - Assert.True(File.Exists(destFile)); + Path.Combine(root, "archive_test", "test1", "1.txt"), + Path.Combine(root, "archive_test", "test2", "2.txt") + }; + files.ToList().ForEach(f => + { + var folder = Path.GetDirectoryName(f); + if (!string.IsNullOrEmpty(folder) && !Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + using var fs = File.OpenWrite(f); + fs.WriteByte(65); + }); + + var archService = Context.Services.GetRequiredService(); + await archService.ArchiveAsync(fileName, new List() + { + new ArchiveEntry() + { + SourceFileName = files[0], + EntryName = "test1/test.log" + }, + new ArchiveEntry() + { + SourceFileName = files[1], + EntryName = "test2/test.log", + CompressionLevel = CompressionLevel.Optimal + }, + new ArchiveEntry() + { + SourceFileName = Path.Combine(AppContext.BaseDirectory, "archive_test", "test1"), + EntryName = "test1", + }, + new ArchiveEntry() + { + SourceFileName = Path.Combine(AppContext.BaseDirectory, "archive_test", "test1"), + EntryName = "test2", + CompressionLevel = CompressionLevel.Optimal + } + }); + + Assert.True(File.Exists(fileName)); } }