using ControlzEx.Standard; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NPOI.OpenXmlFormats.Wordprocessing; using NPOI.Util; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using TeamAAS_VP; using TeamAAS_VP.Models; using TeamAAS_VP.Resources.Languages; using TeamAAS_VP.Views.Home; namespace TeamAAS_VP.Core { /// /// 提供文件读写、备份与恢复相关的静态辅助方法。 /// 功能包括:安全写入(临时文件 + 原子替换)、JSON 读写(含格式校验)、备份版本管理、临时/备份清理与恢复等。 /// 该类内部包含可配置的选项,通过 进行设置。 /// public static class FileHelper { #region 配置选项 /// /// 文件操作辅助类的配置项集合。 /// 可通过 修改。 /// public class FileHelperOptions { /// /// 是否启用自动恢复(读取失败时尝试从备份恢复)。默认值:true。 /// public bool EnableAutoRecovery { get; set; } = true; /// /// 是否保留每次写入时生成的 .bak 备份文件。默认值:false。 /// public bool KeepBackupFiles { get; set; } = false; /// /// 保留的版本化备份最大数量(超过会清理)。默认值:3。 /// public int MaxBackupVersions { get; set; } = 3; /// /// 是否启用内部日志记录(通过 LogHelper)。默认值:false。 /// public bool EnableLogging { get; set; } = false; /// /// 版本化备份存放目录名称(相对于原文件目录)。默认值:"Backups"。 /// public string BackupDirectory { get; set; } = "Backups"; /// /// 文件读写所使用的编码。默认值:UTF8。 /// public Encoding FileEncoding { get; set; } = Encoding.UTF8; } private static FileHelperOptions _options = new FileHelperOptions(); /// /// 配置 FileHelper 的运行时选项。 /// /// 接收并修改 的委托;如果为 null 则保持默认配置。 public static void Configure(Action configure) { configure?.Invoke(_options); } #endregion #region Json文件操作 /// /// 将对象序列化为格式化 JSON 并安全写入指定路径。 /// 写入使用临时文件 + 原子替换以减少损坏风险,并根据配置创建版本化备份。 /// /// 要序列化并写入的对象。 /// 目标文件完整路径。 public static void WriteJsonFile(object obj, string path) { ValidatePath(path); string json = JsonConvert.SerializeObject(obj, Formatting.Indented); WriteFileInternal(json, path); } /// /// 读取 JSON 文件并反序列化为指定类型,支持自动从备份恢复。 /// 如果读取失败且启用了自动恢复,将尝试从 .bak 或版本化备份恢复。 /// /// 反序列化的目标类型。 /// JSON 文件路径。 /// 可选:覆盖默认的自动恢复行为(null 表示使用配置项)。 /// 反序列化后的对象。 /// 当文件不存在且无法恢复时抛出。 public static T ReadJsonFile(string path, bool? autoRecover = null) { ValidatePath(path); bool shouldRecover = autoRecover ?? _options.EnableAutoRecovery; // 检查并清理临时文件 CheckAndCleanTempFile(path); // 尝试读取主文件 Exception lastException = null; try { if (File.Exists(path)) { string json = ReadAndValidateJsonFile(path); return JsonConvert.DeserializeObject(json); } } catch (Exception ex) { lastException = ex; Log($"读取主文件失败: {ex.Message}"); if (shouldRecover) { try { // 尝试从备份恢复 T recovered = TryRecoverJsonFile(path); if (recovered != null) return recovered; } catch (Exception recoveryEx) { Log($"恢复尝试失败: {recoveryEx.Message}"); } } } // 如果文件不存在且允许恢复,尝试从默认备份位置恢复 if (shouldRecover && !File.Exists(path)) { T recovered = TryFindAndRecoverJsonFile(path); if (recovered != null) return recovered; } throw new FileNotFoundException($"文件 {path} 不存在且无法恢复", lastException); } #endregion #region 普通文件操作 /// /// 安全写入文本文件(支持目录创建、临时写入、原子替换与可选版本化备份)。 /// /// 要写入的文本内容。 /// 目标文件完整路径。 public static void WriteFile(string content, string path) { ValidatePath(path); WriteFileInternal(content, path); } /// /// 读取文本文件,支持自动恢复(从 .bak 或版本化备份恢复)。 /// /// 文件路径。 /// 可选:覆盖默认的自动恢复行为(null 表示使用配置项)。 /// 文件内容字符串。 /// 当文件不存在且无法恢复时抛出。 public static string ReadFile(string path, bool? autoRecover = null) { ValidatePath(path); bool shouldRecover = autoRecover ?? _options.EnableAutoRecovery; // 检查并清理临时文件 CheckAndCleanTempFile(path); // 尝试读取主文件 Exception lastException = null; try { if (File.Exists(path)) { return ReadFileWithValidation(path); } } catch (Exception ex) { lastException = ex; Log($"读取主文件失败: {ex.Message}"); if (shouldRecover) { try { // 尝试从备份恢复 string recovered = TryRecoverFile(path); if (recovered != null) return recovered; } catch (Exception recoveryEx) { Log($"恢复尝试失败: {recoveryEx.Message}"); } } } // 如果文件不存在且允许恢复,尝试从默认备份位置恢复 if (shouldRecover && !File.Exists(path)) { string recovered = TryFindAndRecoverFile(path); if (recovered != null) return recovered; } throw new FileNotFoundException($"文件 {path} 不存在且无法恢复", lastException); } /// /// 在不抛出异常的情况下安全读取文件内容,失败时返回提供的默认值。 /// /// 文件路径。 /// 读取失败时返回的默认值(可为 null)。 /// 文件内容或默认值。 public static string SafeReadFile(string path, string defaultValue = null) { try { return ReadFile(path, true); } catch { return defaultValue; } } #endregion #region 文件恢复相关 /// /// 检查并修复指定目录下的所有相关临时文件与备份文件。 /// - 恢复或清理 .tmp 文件 /// - 清理孤立或过旧的 .bak 文件 /// - 清理版本化备份超过保留数量的旧版本 /// /// 要检查的目录路径。 public static void CheckAndRepairDirectory(string directoryPath) { if (!Directory.Exists(directoryPath)) return; // 处理临时文件 var tempFiles = Directory.GetFiles(directoryPath, "*.tmp", SearchOption.AllDirectories); foreach (var tempFile in tempFiles) { TryRecoverFromTempFile(tempFile); } // 处理备份文件 var backupFiles = Directory.GetFiles(directoryPath, "*.bak", SearchOption.AllDirectories); foreach (var backupFile in backupFiles) { TryCleanOrphanedBackup(backupFile); } // 处理版本化备份 CleanupOldBackupVersions(directoryPath); } /// /// 强制从备份恢复主文件。优先使用直接的 .bak,其次尝试版本化备份目录中的最新文件。 /// /// 原文件路径。 /// 成功返回 true,失败返回 false。 public static bool ForceRecoverFile(string originalPath) { try { // 尝试从直接备份恢复 string backupPath = originalPath + ".bak"; if (File.Exists(backupPath)) { File.Copy(backupPath, originalPath, true); Log($"从备份强制恢复: {originalPath}"); return true; } // 尝试从版本化备份恢复 var recovered = TryFindLatestBackup(originalPath); if (recovered != null) { File.Copy(recovered, originalPath, true); Log($"从版本备份强制恢复: {originalPath}"); return true; } return false; } catch (Exception ex) { Log($"强制恢复失败: {ex.Message}"); return false; } } #endregion #region 辅助方法 /// /// 检测给定的文件名是否合规(不包含 Windows 文件名禁止字符)。 /// /// 仅文件名部分(不含路径)。 /// 文件名合法返回 true,否则返回 false。 public static bool CheckFileName(string filename) { if (string.IsNullOrWhiteSpace(filename)) return false; // 定义文件名合法性的正则表达式 string pattern = @"^[^\\/:*?""<>|\x00-\x1F]*$"; return Regex.IsMatch(filename, pattern); } /// /// 检查文件是否存在且有效(可读且长度大于 0)。 /// /// 文件完整路径。 /// 文件存在且有效返回 true,否则返回 false。 public static bool IsFileValid(string path) { try { if (!File.Exists(path)) return false; // 尝试读取一小部分内容来验证文件可访问性 using (var fs = File.OpenRead(path)) { return fs.CanRead && fs.Length > 0; } } catch { return false; } } /// /// 为给定原始路径生成一个安全的临时文件路径(放在系统临时目录),包含随机 GUID 前缀。 /// /// 原文件完整路径,用于生成可识别的临时文件名。 /// 生成的临时文件完整路径。 public static string GetTempFilePath(string originalPath) { // 尝试放在目标目录(确保同一卷) string dir = Path.GetDirectoryName(originalPath); try { if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) { string safeName = Path.GetFileName(originalPath).Replace(" ", "_").Replace(":", "_"); return Path.Combine(dir, $"{Guid.NewGuid():N}_{safeName}.tmp"); } } catch { // 忽略并回退到系统临时目录 } // 回退(极少用到) string tempDir = Path.GetTempPath(); string fallbackName = Path.GetFileName(originalPath).Replace(" ", "_").Replace(":", "_"); return Path.Combine(tempDir, $"{Guid.NewGuid():N}_{fallbackName}.tmp"); } #endregion #region 私有实现方法 /// /// 验证路径与文件名合法性(非空且文件名不包含非法字符)。 /// /// 要验证的文件路径。 private static void ValidatePath(string path) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("路径不能为空", nameof(path)); string fileName = Path.GetFileName(path); if (!CheckFileName(fileName)) throw new ArgumentException($"文件名 '{fileName}' 包含非法字符", nameof(path)); } /// /// 内部写文件实现:创建目录、可选创建版本化备份、写入临时文件并以原子方式替换目标文件。 /// 发生异常时会尝试清理临时文件。 /// /// 要写入的文本内容。 /// 目标文件完整路径。 private static void WriteFileInternal(string content, string path) { // 创建目录 string directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 创建版本化备份 if (_options.KeepBackupFiles && File.Exists(path)) { CreateVersionedBackup(path); } // 临时文件路径 string tempPath = path + ".tmp"; //GetTempFilePath(path); try { // 写入临时文件 using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: false)) using (var sw = new StreamWriter(fs, _options.FileEncoding)) { sw.Write(content); sw.Flush(); fs.Flush(true); // 强制刷新到磁盘 } // 原子性替换 if (File.Exists(path)) { string backupPath = path + ".bak"; File.Replace(tempPath, path, backupPath, true); // 如果不保留备份文件,删除它 if (!_options.KeepBackupFiles && File.Exists(backupPath)) { try { File.Delete(backupPath); } catch { } } } else { File.Move(tempPath, path); } } catch { // 清理临时文件 SafeDelete(tempPath); throw; } finally { // 确保临时文件被清理 SafeDelete(tempPath); } } /// /// 创建版本化备份:将原文件复制到同目录下的备份目录,并带时间戳后缀。 /// 发生异常时仅记录日志,不抛出。 /// /// 要备份的原文件路径。 private static void CreateVersionedBackup(string originalPath) { try { string backupDir = Path.Combine(Path.GetDirectoryName(originalPath), _options.BackupDirectory); if (!Directory.Exists(backupDir)) Directory.CreateDirectory(backupDir); string fileName = Path.GetFileNameWithoutExtension(originalPath); string extension = Path.GetExtension(originalPath); string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"); string backupName = $"{fileName}_{timestamp}{extension}"; string backupPath = Path.Combine(backupDir, backupName); File.Copy(originalPath, backupPath, true); Log($"创建版本备份: {backupPath}"); } catch (Exception ex) { Log($"创建版本备份失败: {ex.Message}"); } } /// /// 检查并清理与原文件同名的临时文件(使用 .tmp 后缀的临时文件),并在可能的情况下记录发现的信息。 /// /// 原文件完整路径。 private static void CheckAndCleanTempFile(string originalPath) { string tempPath = originalPath + ".tmp"; if (File.Exists(tempPath)) { try { // 检查临时文件是否较新(可能是中断的写入) if (File.Exists(originalPath)) { var originalTime = File.GetLastWriteTime(originalPath); var tempTime = File.GetLastWriteTime(tempPath); if (tempTime > originalTime) { Log($"发现较新的临时文件,可能上次写入未完成: {tempPath}"); } } File.Delete(tempPath); Log($"清理临时文件: {tempPath}"); } catch (Exception ex) { Log($"清理临时文件失败: {ex.Message}"); } } } /// /// 尝试从直接的 .bak 文件恢复 JSON 文件并返回反序列化的对象;恢复成功后尝试覆盖主文件。 /// /// 目标类型。 /// 原 JSON 文件路径。 /// 恢复并反序列化后的对象,失败返回 default(T)。 private static T TryRecoverJsonFile(string originalPath) { // 尝试从直接备份恢复 string backupPath = originalPath + ".bak"; if (File.Exists(backupPath)) { try { string json = ReadAndValidateJsonFile(backupPath); T result = JsonConvert.DeserializeObject(json); // 尝试恢复主文件 TryRestoreMainFile(backupPath, originalPath); Log($"从备份恢复Json文件: {originalPath}"); return result; } catch (Exception ex) { Log($"从备份恢复Json文件失败: {ex.Message}"); } } return default; } /// /// 尝试从直接的 .bak 文件恢复文本文件并返回内容;恢复成功后尝试覆盖主文件。 /// /// 原文件路径。 /// 恢复后的内容,失败返回 null。 private static string TryRecoverFile(string originalPath) { // 尝试从直接备份恢复 string backupPath = originalPath + ".bak"; if (File.Exists(backupPath)) { try { string content = ReadFileWithValidation(backupPath); // 尝试恢复主文件 TryRestoreMainFile(backupPath, originalPath); Log($"从备份恢复文件: {originalPath}"); return content; } catch (Exception ex) { Log($"从备份恢复文件失败: {ex.Message}"); } } return null; } /// /// 在版本化备份目录中查找最新的备份并尝试恢复 JSON 文件,成功返回反序列化对象并复制到主文件。 /// /// 目标类型。 /// 原 JSON 文件路径。 /// 恢复并反序列化后的对象,失败返回 default(T)。 private static T TryFindAndRecoverJsonFile(string originalPath) { string backup = TryFindLatestBackup(originalPath); if (backup != null) { try { string json = ReadAndValidateJsonFile(backup); T result = JsonConvert.DeserializeObject(json); File.Copy(backup, originalPath, true); Log($"从版本备份恢复Json文件: {originalPath}"); return result; } catch (Exception ex) { Log($"从版本备份恢复Json文件失败: {ex.Message}"); } } return default; } /// /// 在版本化备份目录中查找最新的备份并尝试恢复文本文件,成功后复制到主文件并返回内容。 /// /// 原文件路径。 /// 恢复后的内容,失败返回 null。 private static string TryFindAndRecoverFile(string originalPath) { string backup = TryFindLatestBackup(originalPath); if (backup != null) { try { string content = ReadFileWithValidation(backup); File.Copy(backup, originalPath, true); Log($"从版本备份恢复文件: {originalPath}"); return content; } catch (Exception ex) { Log($"从版本备份恢复文件失败: {ex.Message}"); } } return null; } /// /// 查找最新的版本化备份文件路径。优先检查指定的备份目录,其次检查原目录下同名带时间戳的文件。 /// /// 原始文件路径。 /// 最新备份文件的完整路径或 null(未找到)。 private static string TryFindLatestBackup(string originalPath) { string directory = Path.GetDirectoryName(originalPath); string fileName = Path.GetFileNameWithoutExtension(originalPath); string extension = Path.GetExtension(originalPath); if (!Directory.Exists(directory)) return null; // 检查备份目录 string backupDir = Path.Combine(directory, _options.BackupDirectory); if (Directory.Exists(backupDir)) { var backups = Directory.GetFiles(backupDir, $"{fileName}_*{extension}") .OrderByDescending(f => f) .ToList(); if (backups.Any()) return backups.First(); } // 检查当前目录的备份文件 var localBackups = Directory.GetFiles(directory, $"{fileName}_*{extension}") .OrderByDescending(f => f) .ToList(); if (localBackups.Any()) return localBackups.First(); return null; } /// /// 读取文件并在读取后进行 JSON 格式校验(适用于 JSON 文件)。如果格式无效则抛出异常。 /// /// 文件路径。 /// 文件文本内容。 /// 当 JSON 格式无效时抛出。 private static string ReadAndValidateJsonFile(string filePath) { string json = ReadFileWithValidation(filePath); // 验证JSON格式 if (!string.IsNullOrWhiteSpace(json)) { try { JToken.Parse(json); } catch (JsonException ex) { throw new InvalidDataException($"文件 {filePath} 包含无效的JSON格式: {ex.Message}", ex); } } return json; } /// /// 读取文件并进行基本验证(文件存在且读取结果非 null)。 /// /// 文件路径。 /// 文件内容字符串。 /// 文件不存在时抛出。 /// 读取结果为 null 时抛出。 private static string ReadFileWithValidation(string filePath) { if (!File.Exists(filePath)) throw new FileNotFoundException($"文件不存在: {filePath}"); string content = File.ReadAllText(filePath, _options.FileEncoding); if (content == null) throw new InvalidDataException($"文件 {filePath} 内容为null"); return content; } /// /// 将备份文件复制回主文件路径以进行恢复,失败时记录日志但不抛出。 /// /// 备份文件路径。 /// 目标主文件路径。 private static void TryRestoreMainFile(string backupPath, string originalPath) { try { File.Copy(backupPath, originalPath, true); Log($"恢复主文件: {originalPath}"); } catch (Exception ex) { Log($"恢复主文件失败: {ex.Message}"); } } /// /// 尝试根据 .tmp 临时文件恢复原文件:如果临时文件较新或原文件不存在则移动临时文件覆盖原文件,否则删除临时文件。 /// 出错时尝试安全删除临时文件。 /// /// 临时文件路径。 private static void TryRecoverFromTempFile(string tempPath) { try { // 获取原文件路径(假设临时文件是 .tmp 扩展名) string originalPath = tempPath.EndsWith(".tmp") ? tempPath.Substring(0, tempPath.Length - 4) : tempPath; // 如果原文件不存在或临时文件更新,则恢复 if (!File.Exists(originalPath) || File.GetLastWriteTime(tempPath) > File.GetLastWriteTime(originalPath)) { File.Move(tempPath, originalPath); Log($"从临时文件恢复: {originalPath}"); } else { File.Delete(tempPath); } } catch { SafeDelete(tempPath); } } /// /// 清理孤立或过旧的 .bak 备份:如果主文件存在且备份比主文件早超过一定时间,则删除备份。 /// /// 备份文件路径(通常以 .bak 结尾)。 private static void TryCleanOrphanedBackup(string backupPath) { try { string originalPath = backupPath.EndsWith(".bak") ? backupPath.Substring(0, backupPath.Length - 4) : backupPath; // 如果主文件存在且备份较旧,删除备份 if (File.Exists(originalPath)) { var originalTime = File.GetLastWriteTime(originalPath); var backupTime = File.GetLastWriteTime(backupPath); if (originalTime > backupTime.AddMinutes(5)) { File.Delete(backupPath); Log($"清理旧备份: {backupPath}"); } } } catch { // 忽略错误 } } /// /// 清理版本化备份目录中超过保留数量的旧版本(按名称分组并按字符串降序保留最新的若干个)。 /// /// 原文件所在目录,该目录下可能包含版本化备份子目录。 private static void CleanupOldBackupVersions(string directory) { try { string backupDir = Path.Combine(directory, _options.BackupDirectory); if (!Directory.Exists(backupDir)) return; var fileGroups = Directory.GetFiles(backupDir) .GroupBy(f => Path.GetFileNameWithoutExtension(f).Split('_')[0]) .ToList(); foreach (var group in fileGroups) { var backups = group.OrderByDescending(f => f).ToList(); for (int i = _options.MaxBackupVersions; i < backups.Count; i++) { SafeDelete(backups[i]); } } } catch { // 忽略错误 } } /// /// 尝试安全删除指定文件,删除失败则忽略异常。 /// /// 要删除的文件路径。 private static void SafeDelete(string filePath) { try { if (File.Exists(filePath)) File.Delete(filePath); } catch { // 忽略删除错误 } } /// /// 内部日志方法,只有在配置启用时才会调用 LogHelper 记录调试日志。 /// /// 日志消息正文。 private static void Log(string message) { if (_options.EnableLogging) { //Console.WriteLine($"[FileHelper] {DateTime.Now:HH:mm:ss} - {message}"); LogHelper.WriteLogDebug($"[FileHelper] {DateTime.Now:HH:mm:ss} - {message}"); } } #endregion } }