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