|
|
@@ -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
|