Răsfoiți Sursa

更多压缩包相关方法

龙腾猫跃 5 luni în urmă
părinte
comite
d7c7aa1e49
1 a modificat fișierele cu 55 adăugiri și 29 ștergeri
  1. 55 29
      Shared/Utils/FileUtils.cs

+ 55 - 29
Shared/Utils/FileUtils.cs

@@ -1,4 +1,5 @@
 using System.IO.Compression;
+using System.Text;
 
 namespace MeloongCore;
 public static class FileUtils {
@@ -146,10 +147,33 @@ public static class FileUtils {
 
     #region 压缩包
 
+    /// <summary>
+    /// 以只读模式打开压缩文件。
+    /// 会先尝试 UTF8 编码,失败后换用 GB18030 编码。
+    /// </summary>
+    public static ZipArchive OpenZip(string zipFilePath) {
+        ZipArchive TryOpen(Encoding encoding) {
+            var result = ZipFile.Open(PathUtils.WithLongPath(zipFilePath), ZipArchiveMode.Read, encoding);
+            try {
+                _ = result.Entries; // 如果编码有误,会在这里抛出 DecoderFallbackException;如果文件异常,会在这里抛出 InvalidDataException
+                return result;
+            } catch {
+                result.Dispose();
+                throw;
+            }
+        }
+        try { // 尝试两种编码
+            return TryOpen(new UTF8Encoding(false, true));
+        } catch (DecoderFallbackException) {
+            return TryOpen(Encoding.GetEncoding("GB18030"));
+        } catch (InvalidDataException ex) {
+            throw new InvalidDataException($"文件不是压缩包,或者文件已损坏({zipFilePath})", ex);
+        }
+    }
+
     /// <summary>
     /// 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 jar 以 zip 方式解压。
     /// 会自动创建文件夹。会覆盖已有文件,但不会删除多余文件。
-    /// 解压时会先尝试 UTF8 编码,失败后换用 GB18030。
     /// </summary>
     public static void ExtractToDirectory(string compressionFile, string outputDirectory, Action<double>? progressIncrementHandler = null) {
         compressionFile = PathUtils.WithLongPath(compressionFile);
@@ -161,30 +185,23 @@ public static class FileUtils {
             return;
         }
         // 解压 zip
-        void ExtractWithEncoding(Encoding encoding) {
-            using var archive = ZipFile.Open(compressionFile, ZipArchiveMode.Read, encoding);
-            int entryCount = archive.Entries.Count;
-            foreach (var entry in archive.Entries) {
-                if (progressIncrementHandler != null && entryCount > 0) progressIncrementHandler(1.0 / entryCount);
-                if (string.IsNullOrEmpty(entry.Name)) continue; // 跳过文件夹条目(ZipArchive 会将文件夹也作为一个 entry,但它们的 Name 为空)
-                // ZipSlip 修复
-                string outputFilePath = Path.GetFullPath(Path.Combine(outputDirectory, entry.FullName));
-                if (!outputFilePath.StartsWithF(PathUtils.WithSeparator(Path.GetFullPath(outputDirectory)))) 
-                    throw new UnauthorizedAccessException($"Zip 文件项 {entry.FullName} 的路径在压缩包之外,这可能导致安全问题");
-                // 实际的解压
-                using var entryStream = entry.Open();
-                FileUtils.Write(outputFilePath, entryStream);
-            }
-        }
-        try { // 尝试两种编码
-            ExtractWithEncoding(new UTF8Encoding(false, true));
-        } catch (InvalidDataException) {
-            ExtractWithEncoding(Encoding.GetEncoding("GB18030"));
+        using var archive = FileUtils.OpenZip(compressionFile);
+        int entryCount = archive.Entries.Count;
+        foreach (var entry in archive.Entries) {
+            if (progressIncrementHandler != null && entryCount > 0) progressIncrementHandler(1.0 / entryCount);
+            if (string.IsNullOrEmpty(entry.Name)) continue; // 跳过文件夹条目(ZipArchive 会将文件夹也作为一个 entry,但它们的 Name 为空)
+            // ZipSlip 修复
+            string outputFilePath = PathUtils.WithLongPath(Path.GetFullPath(Path.Combine(outputDirectory, entry.FullName)));
+            if (!outputFilePath.StartsWithF(PathUtils.WithSeparator(PathUtils.WithLongPath(Path.GetFullPath(outputDirectory)))))
+                throw new UnauthorizedAccessException($"Zip 文件项 {entry.FullName} 的路径在压缩包之外,这可能导致安全问题");
+            // 实际的解压
+            using var entryStream = entry.Open();
+            FileUtils.Write(outputFilePath, entryStream);
         }
     }
 
     /// <summary>
-    /// 将指定文件夹的内容打包为一个 zip 压缩包。
+    /// 将指定文件夹的内容打包为 zip 文件。
     /// 会自动创建文件夹。会覆盖已有文件。
     /// </summary>
     public static void CreateZipFromDirectory(string outputFullPath, string sourceDirectory) {
@@ -196,21 +213,30 @@ public static class FileUtils {
     }
 
     /// <summary>
-    /// 将多个文件打包为一个压缩文件,所有文件都会被放在压缩文件的根目录。
+    /// 将多个文件打包为 zip 文件,所有文件都会被放在压缩文件的根目录。
     /// 会自动创建文件夹。会覆盖已有文件。
     /// </summary>
     public static void CreateZipFromFiles(string outputFullPath, params string[] sourceFiles) {
+        var sources = new Dictionary<string, string>();
+        foreach (var source in sourceFiles) {
+            string fileName = Path.GetFileName(source);
+            if (sources.ContainsKey(fileName)) throw new ArgumentException($"尝试将多个同文件名的文件放进压缩包中({fileName})", nameof(sourceFiles));
+            sources.Add(fileName, source);
+        }
+        CreateZipFromFiles(outputFullPath, sources);
+    }
+
+    /// <summary>
+    /// 将多个文件打包为 zip 文件。
+    /// 会自动创建文件夹。会覆盖已有文件。
+    /// </summary>
+    /// <param name="sources">键为 zip 文件下的路径,值为文件的本地路径。</param>
+    public static void CreateZipFromFiles(string outputFullPath, IDictionary<string, string> sources) {
         outputFullPath = PathUtils.WithLongPath(outputFullPath);
-        sourceFiles = sourceFiles.Select(PathUtils.WithLongPath).ToArray();
         DirectoryUtils.Create(outputFullPath, isFilePath: true);
         FileUtils.Delete(outputFullPath);
         using var archive = ZipFile.Open(outputFullPath, ZipArchiveMode.Create);
-        foreach (var source in sourceFiles) {
-            if (!FileUtils.Exists(source)) throw new FileNotFoundException($"未找到需要被压缩的文件({source})", source);
-            using var sourceStream = FileUtils.ReadAsStream(source);
-            using var entryStream = archive.CreateEntry(Path.GetFileName(source), CompressionLevel.Optimal).Open();
-            sourceStream.CopyTo(entryStream);
-        }
+        foreach (var pair in sources) archive.CreateEntryFromFile(PathUtils.WithLongPath(pair.Value), pair.Key.Replace('\\', '/'), CompressionLevel.Optimal);
     }
 
     #endregion