275 lines
10 KiB
C#
275 lines
10 KiB
C#
|
||
|
||
namespace Yes.Infrastructure.Helpers
|
||
{
|
||
public class ZipHelper
|
||
{
|
||
public static void ExtractAndProcessZip(string zipFilePath, string targetDirectory, string folderName)
|
||
{
|
||
#region 参数验证
|
||
if (string.IsNullOrEmpty(zipFilePath))
|
||
throw new ArgumentException("ZIP 文件路径不能为空。", nameof(zipFilePath));
|
||
|
||
if (string.IsNullOrEmpty(targetDirectory))
|
||
throw new ArgumentException("目标目录路径不能为空。", nameof(targetDirectory));
|
||
|
||
if (!File.Exists(zipFilePath))
|
||
throw new FileNotFoundException("指定的 ZIP 文件不存在。", zipFilePath);
|
||
|
||
if (!Directory.Exists(targetDirectory))
|
||
Directory.CreateDirectory(targetDirectory);
|
||
#endregion
|
||
|
||
|
||
string tempExtractPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||
Directory.CreateDirectory(tempExtractPath);
|
||
try
|
||
{
|
||
// 解压到临时目录
|
||
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempExtractPath, overwriteFiles: true);
|
||
|
||
// 找到实际文件目录(跳过单层空文件夹)
|
||
string contentRoot = GetContentRootDirectory(tempExtractPath);
|
||
|
||
|
||
string targetFolderPath = Path.Combine(targetDirectory, folderName);
|
||
|
||
|
||
#region 创建目标文件夹并移动文件
|
||
Directory.CreateDirectory(targetFolderPath);
|
||
foreach (var file in Directory.GetFiles(contentRoot))
|
||
{
|
||
string destFile = Path.Combine(targetFolderPath, Path.GetFileName(file));
|
||
File.Move(file, destFile, true);
|
||
}
|
||
|
||
foreach (var dir in Directory.GetDirectories(contentRoot))
|
||
{
|
||
string destDir = Path.Combine(targetFolderPath, Path.GetFileName(dir));
|
||
|
||
// 若目标目录存在,先删除
|
||
if (Directory.Exists(destDir))
|
||
{
|
||
Directory.Delete(destDir, recursive: true);
|
||
}
|
||
|
||
Directory.Move(dir, destDir);
|
||
}
|
||
#endregion
|
||
}
|
||
finally
|
||
{
|
||
// 清理临时目录
|
||
if (Directory.Exists(tempExtractPath))
|
||
Directory.Delete(tempExtractPath, recursive: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归查找实际内容目录(跳过仅包含单个文件夹的层级)
|
||
/// </summary>
|
||
private static string GetContentRootDirectory(string startPath)
|
||
{
|
||
string currentPath = startPath;
|
||
|
||
while (true)
|
||
{
|
||
string[] subDirs = Directory.GetDirectories(currentPath);
|
||
string[] files = Directory.GetFiles(currentPath);
|
||
|
||
if (files.Length > 0 || subDirs.Length == 0)
|
||
return currentPath;
|
||
|
||
if (subDirs.Length == 1)
|
||
currentPath = subDirs[0];
|
||
else
|
||
return currentPath; // 多个子目录,停止递归
|
||
}
|
||
}
|
||
public static void ExtractToDirectory(string zipPath, string targetDirectory, string themeName)
|
||
{
|
||
if (!Directory.Exists(targetDirectory))
|
||
{
|
||
Directory.CreateDirectory(targetDirectory);
|
||
}
|
||
|
||
var requiredFileName = "config.json";
|
||
|
||
using (var zipFile = new ZipFile(zipPath))
|
||
{
|
||
bool fileFound = false;
|
||
|
||
foreach (ZipEntry entry in zipFile)
|
||
{
|
||
if (entry.IsDirectory)
|
||
continue;
|
||
|
||
string entryName = entry.Name.Trim('/');
|
||
if (string.IsNullOrEmpty(entryName))
|
||
continue;
|
||
|
||
string[] parts = entryName.Split('/');
|
||
|
||
// 文件名必须匹配
|
||
if (!parts[^1].Equals(requiredFileName, StringComparison.OrdinalIgnoreCase))
|
||
continue;
|
||
|
||
// 所有父级目录必须为 themeName
|
||
bool allParentsAreTopDir = parts.Take(parts.Length - 1)
|
||
.All(p => p.Equals(themeName, StringComparison.OrdinalIgnoreCase));
|
||
|
||
if (allParentsAreTopDir)
|
||
{
|
||
fileFound = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!fileFound)
|
||
{
|
||
throw new FileNotFoundException(
|
||
$"ZIP 文件中未找到配置文件: {requiredFileName}");
|
||
}
|
||
|
||
|
||
foreach (ZipEntry entry in zipFile)
|
||
{
|
||
string originalPath = entry.Name.Trim('/');
|
||
if (string.IsNullOrEmpty(originalPath)) continue;
|
||
|
||
string[] parts = originalPath.Split('/');
|
||
int repeatCount = 0;
|
||
|
||
for (int i = 0; i < parts.Length; i++)
|
||
{
|
||
if (parts[i] == themeName)
|
||
{
|
||
repeatCount++;
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
var newPathParts = new List<string>();
|
||
newPathParts.Add(themeName);
|
||
newPathParts.AddRange(parts.Skip(repeatCount));
|
||
|
||
string newRelativePath = Path.Combine(newPathParts.ToArray());
|
||
string targetPath = Path.Combine(targetDirectory, newRelativePath);
|
||
|
||
if (entry.IsDirectory)
|
||
{
|
||
Directory.CreateDirectory(targetPath);
|
||
}
|
||
else
|
||
{
|
||
if (!IsSafeFile(entry.Name))
|
||
{
|
||
continue;
|
||
}
|
||
using (var stream = zipFile.GetInputStream(entry))
|
||
{
|
||
using (var fileStream = File.Create(targetPath))
|
||
{
|
||
stream.CopyTo(fileStream);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
private static bool IsSafeFile(string fileName)
|
||
{
|
||
var ext = Path.GetExtension(fileName).ToLower();
|
||
return new[] { ".liquid", ".js", ".css", ".html", ".md", ".ttf", ".svg", ".eot", ".woff", ".woff2", ".toml", ".xml" }.Contains(ext);
|
||
}
|
||
|
||
private static string GenerateSafeFolderName(string input, int maxLength = 200, char replaceSpaceWith = '_')
|
||
{
|
||
if (string.IsNullOrEmpty(input))
|
||
return "UnnamedFolder";
|
||
|
||
// 定义所有非法字符(Windows + Linux)
|
||
string invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()) + "<>:\"|?*");
|
||
Regex invalidCharRegex = new Regex($"[{invalidChars}]");
|
||
|
||
// 替换非法字符为空格
|
||
string sanitized = invalidCharRegex.Replace(input, " ");
|
||
|
||
// 替换连续空格为单个空格
|
||
sanitized = Regex.Replace(sanitized, @"\s+", " ").Trim();
|
||
|
||
// 替换空格为指定字符
|
||
sanitized = sanitized.Replace(" ", replaceSpaceWith.ToString());
|
||
|
||
// 处理保留名称(Windows)
|
||
string[] reservedNames = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
|
||
if (Array.Exists(reservedNames, name => name.Equals(sanitized, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
sanitized += "_safe";
|
||
}
|
||
|
||
// 截断到最大长度
|
||
if (sanitized.Length > maxLength)
|
||
{
|
||
sanitized = sanitized.Substring(0, maxLength);
|
||
}
|
||
|
||
// 如果最终结果为空,返回默认值
|
||
if (string.IsNullOrEmpty(sanitized))
|
||
{
|
||
sanitized = "UnnamedFolder";
|
||
}
|
||
|
||
return sanitized;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 将指定路径的文件夹打包为 ZIP 文件
|
||
/// </summary>
|
||
/// <param name="sourceFolderPath">要压缩的文件夹路径</param>
|
||
/// <param name="destinationZipPath">生成的 ZIP 文件路径</param>
|
||
/// <param name="compressionLevel">压缩级别</param>
|
||
/// <param name="overwrite">是否覆盖已存在的 ZIP 文件</param>
|
||
public static void CompressFolderToZip(
|
||
string sourceFolderPath,
|
||
string destinationZipPath,
|
||
CompressionLevel compressionLevel = CompressionLevel.BEST_SPEED,
|
||
bool overwrite = true)
|
||
{
|
||
if (string.IsNullOrEmpty(sourceFolderPath))
|
||
throw new ArgumentException("源文件夹路径不能为空。", nameof(sourceFolderPath));
|
||
|
||
if (string.IsNullOrEmpty(destinationZipPath))
|
||
throw new ArgumentException("目标 ZIP 文件路径不能为空。", nameof(destinationZipPath));
|
||
|
||
if (!Directory.Exists(sourceFolderPath))
|
||
throw new DirectoryNotFoundException($"源文件夹 '{sourceFolderPath}' 不存在。");
|
||
|
||
var destinationDirectory = Path.GetDirectoryName(destinationZipPath);
|
||
if (!Directory.Exists(destinationDirectory))
|
||
Directory.CreateDirectory(destinationDirectory);
|
||
|
||
if (File.Exists(destinationZipPath) && overwrite)
|
||
File.Delete(destinationZipPath);
|
||
|
||
try
|
||
{
|
||
System.IO.Compression.ZipFile.CreateFromDirectory(sourceFolderPath,destinationZipPath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new IOException($"压缩文件夹失败:{ex.Message}", ex);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|