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); } } /// /// 递归查找实际内容目录(跳过仅包含单个文件夹的层级) /// 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(); 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; } /// /// 将指定路径的文件夹打包为 ZIP 文件 /// /// 要压缩的文件夹路径 /// 生成的 ZIP 文件路径 /// 压缩级别 /// 是否覆盖已存在的 ZIP 文件 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); } } } }