|
|
@@ -1,5 +1,7 @@
|
|
|
using ControlzEx.Standard;
|
|
|
using Newtonsoft.Json;
|
|
|
+using Newtonsoft.Json.Linq;
|
|
|
+using NPOI.Util;
|
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
using System.Collections.ObjectModel;
|
|
|
@@ -8,118 +10,858 @@ using System.Linq;
|
|
|
using System.Text;
|
|
|
using System.Text.RegularExpressions;
|
|
|
using System.Threading.Tasks;
|
|
|
-using TeamAAS_VP.Models;
|
|
|
using TeamAAS_VP;
|
|
|
-using static Org.BouncyCastle.Math.EC.ECCurve;
|
|
|
+using TeamAAS_VP.Models;
|
|
|
+using TeamAAS_VP.Resources.Languages;
|
|
|
|
|
|
namespace TeamAAS_VP.Core
|
|
|
{
|
|
|
+ /// <summary>
|
|
|
+ /// 提供文件读写、备份与恢复相关的静态辅助方法。
|
|
|
+ /// 功能包括:安全写入(临时文件 + 原子替换)、JSON 读写(含格式校验)、备份版本管理、临时/备份清理与恢复等。
|
|
|
+ /// 该类内部包含可配置的选项,通过 <see cref="Configure"/> 进行设置。
|
|
|
+ /// </summary>
|
|
|
public static class FileHelper
|
|
|
{
|
|
|
+ #region 配置选项
|
|
|
/// <summary>
|
|
|
- /// 写入Json文件
|
|
|
+ /// 文件操作辅助类的配置项集合。
|
|
|
+ /// 可通过 <see cref="Configure"/> 修改。
|
|
|
/// </summary>
|
|
|
- /// <param name="obj">对象</param>
|
|
|
- /// <param name="path">文件路径</param>
|
|
|
+ public class FileHelperOptions
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// 是否启用自动恢复(读取失败时尝试从备份恢复)。默认值:true。
|
|
|
+ /// </summary>
|
|
|
+ public bool EnableAutoRecovery { get; set; } = true;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 是否保留每次写入时生成的 .bak 备份文件。默认值:false。
|
|
|
+ /// </summary>
|
|
|
+ public bool KeepBackupFiles { get; set; } = false;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 保留的版本化备份最大数量(超过会清理)。默认值:3。
|
|
|
+ /// </summary>
|
|
|
+ public int MaxBackupVersions { get; set; } = 3;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 是否启用内部日志记录(通过 LogHelper)。默认值:false。
|
|
|
+ /// </summary>
|
|
|
+ public bool EnableLogging { get; set; } = false;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 版本化备份存放目录名称(相对于原文件目录)。默认值:"Backups"。
|
|
|
+ /// </summary>
|
|
|
+ public string BackupDirectory { get; set; } = "Backups";
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 文件读写所使用的编码。默认值:UTF8。
|
|
|
+ /// </summary>
|
|
|
+ public Encoding FileEncoding { get; set; } = Encoding.UTF8;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static FileHelperOptions _options = new FileHelperOptions();
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 配置 FileHelper 的运行时选项。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="configure">接收并修改 <see cref="FileHelperOptions"/> 的委托;如果为 null 则保持默认配置。</param>
|
|
|
+ public static void Configure(Action<FileHelperOptions> configure)
|
|
|
+ {
|
|
|
+ configure?.Invoke(_options);
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Json文件操作
|
|
|
+ /// <summary>
|
|
|
+ /// 将对象序列化为格式化 JSON 并安全写入指定路径。
|
|
|
+ /// 写入使用临时文件 + 原子替换以减少损坏风险,并根据配置创建版本化备份。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="obj">要序列化并写入的对象。</param>
|
|
|
+ /// <param name="path">目标文件完整路径。</param>
|
|
|
public static void WriteJsonFile(object obj, string path)
|
|
|
{
|
|
|
- if (!Directory.Exists(Path.GetDirectoryName(path)))
|
|
|
+ ValidatePath(path);
|
|
|
+ string json = JsonConvert.SerializeObject(obj, Formatting.Indented);
|
|
|
+ WriteFileInternal(json, path);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 读取 JSON 文件并反序列化为指定类型,支持自动从备份恢复。
|
|
|
+ /// 如果读取失败且启用了自动恢复,将尝试从 .bak 或版本化备份恢复。
|
|
|
+ /// </summary>
|
|
|
+ /// <typeparam name="T">反序列化的目标类型。</typeparam>
|
|
|
+ /// <param name="path">JSON 文件路径。</param>
|
|
|
+ /// <param name="autoRecover">可选:覆盖默认的自动恢复行为(null 表示使用配置项)。</param>
|
|
|
+ /// <returns>反序列化后的对象。</returns>
|
|
|
+ /// <exception cref="FileNotFoundException">当文件不存在且无法恢复时抛出。</exception>
|
|
|
+ public static T ReadJsonFile<T>(string path, bool? autoRecover = null)
|
|
|
+ {
|
|
|
+ ValidatePath(path);
|
|
|
+
|
|
|
+ bool shouldRecover = autoRecover ?? _options.EnableAutoRecovery;
|
|
|
+
|
|
|
+ // 检查并清理临时文件
|
|
|
+ CheckAndCleanTempFile(path);
|
|
|
+
|
|
|
+ // 尝试读取主文件
|
|
|
+ Exception lastException = null;
|
|
|
+
|
|
|
+ try
|
|
|
{
|
|
|
- Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
|
+ if (File.Exists(path))
|
|
|
+ {
|
|
|
+ string json = ReadAndValidateJsonFile(path);
|
|
|
+ return JsonConvert.DeserializeObject<T>(json);
|
|
|
+ }
|
|
|
}
|
|
|
- using (StreamWriter sw = new StreamWriter(path))
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- string json = JsonConvert.SerializeObject(obj, Formatting.Indented);
|
|
|
- sw.Write(JsonConvert.SerializeObject(obj, Newtonsoft.Json.Formatting.Indented));
|
|
|
+ lastException = ex;
|
|
|
+ Log($"读取主文件失败: {ex.Message}");
|
|
|
+
|
|
|
+ if (shouldRecover)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 尝试从备份恢复
|
|
|
+ T recovered = TryRecoverJsonFile<T>(path);
|
|
|
+ if (recovered != null)
|
|
|
+ return recovered;
|
|
|
+ }
|
|
|
+ catch (Exception recoveryEx)
|
|
|
+ {
|
|
|
+ Log($"恢复尝试失败: {recoveryEx.Message}");
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ // 如果文件不存在且允许恢复,尝试从默认备份位置恢复
|
|
|
+ if (shouldRecover && !File.Exists(path))
|
|
|
+ {
|
|
|
+ T recovered = TryFindAndRecoverJsonFile<T>(path);
|
|
|
+ if (recovered != null)
|
|
|
+ return recovered;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new FileNotFoundException($"文件 {path} 不存在且无法恢复", lastException);
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 普通文件操作
|
|
|
+ /// <summary>
|
|
|
+ /// 安全写入文本文件(支持目录创建、临时写入、原子替换与可选版本化备份)。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="content">要写入的文本内容。</param>
|
|
|
+ /// <param name="path">目标文件完整路径。</param>
|
|
|
+ public static void WriteFile(string content, string path)
|
|
|
+ {
|
|
|
+ ValidatePath(path);
|
|
|
+ WriteFileInternal(content, path);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 读取Json文件
|
|
|
+ /// 读取文本文件,支持自动恢复(从 .bak 或版本化备份恢复)。
|
|
|
/// </summary>
|
|
|
- /// <typeparam name="T">对象</typeparam>
|
|
|
- /// <param name="path">文件路径</param>
|
|
|
- /// <returns></returns>
|
|
|
- /// <exception cref="Exception"></exception>
|
|
|
- public static T ReadJsonFile<T>(string path)
|
|
|
+ /// <param name="path">文件路径。</param>
|
|
|
+ /// <param name="autoRecover">可选:覆盖默认的自动恢复行为(null 表示使用配置项)。</param>
|
|
|
+ /// <returns>文件内容字符串。</returns>
|
|
|
+ /// <exception cref="FileNotFoundException">当文件不存在且无法恢复时抛出。</exception>
|
|
|
+ public static string ReadFile(string path, bool? autoRecover = null)
|
|
|
{
|
|
|
- if (File.Exists(path))
|
|
|
+ ValidatePath(path);
|
|
|
+
|
|
|
+ bool shouldRecover = autoRecover ?? _options.EnableAutoRecovery;
|
|
|
+
|
|
|
+ // 检查并清理临时文件
|
|
|
+ CheckAndCleanTempFile(path);
|
|
|
+
|
|
|
+ // 尝试读取主文件
|
|
|
+ Exception lastException = null;
|
|
|
+
|
|
|
+ try
|
|
|
{
|
|
|
- string buffer;
|
|
|
- using (StreamReader sr = new StreamReader(path))
|
|
|
+ if (File.Exists(path))
|
|
|
{
|
|
|
- buffer = sr.ReadToEnd();
|
|
|
+ return ReadFileWithValidation(path);
|
|
|
}
|
|
|
- return JsonConvert.DeserializeObject<T>(buffer);
|
|
|
}
|
|
|
- else
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- throw new Exception($"文件不存在:[{path}]");
|
|
|
+ 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);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 写入文件
|
|
|
+ /// 在不抛出异常的情况下安全读取文件内容,失败时返回提供的默认值。
|
|
|
/// </summary>
|
|
|
- /// <param name="content">对象</param>
|
|
|
- /// <param name="path">文件路径</param>
|
|
|
- public static void WriteFile(string content, string path)
|
|
|
+ /// <param name="path">文件路径。</param>
|
|
|
+ /// <param name="defaultValue">读取失败时返回的默认值(可为 null)。</param>
|
|
|
+ /// <returns>文件内容或默认值。</returns>
|
|
|
+ public static string SafeReadFile(string path, string defaultValue = null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return ReadFile(path, true);
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return defaultValue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 文件恢复相关
|
|
|
+ /// <summary>
|
|
|
+ /// 检查并修复指定目录下的所有相关临时文件与备份文件。
|
|
|
+ /// - 恢复或清理 .tmp 文件
|
|
|
+ /// - 清理孤立或过旧的 .bak 文件
|
|
|
+ /// - 清理版本化备份超过保留数量的旧版本
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="directoryPath">要检查的目录路径。</param>
|
|
|
+ public static void CheckAndRepairDirectory(string directoryPath)
|
|
|
{
|
|
|
- if (!Directory.Exists(Path.GetDirectoryName(path)))
|
|
|
+ if (!Directory.Exists(directoryPath))
|
|
|
+ return;
|
|
|
+
|
|
|
+ // 处理临时文件
|
|
|
+ var tempFiles = Directory.GetFiles(directoryPath, "*.tmp", SearchOption.AllDirectories);
|
|
|
+ foreach (var tempFile in tempFiles)
|
|
|
{
|
|
|
- Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
|
+ TryRecoverFromTempFile(tempFile);
|
|
|
}
|
|
|
- using (StreamWriter sw = new StreamWriter(path))
|
|
|
+
|
|
|
+ // 处理备份文件
|
|
|
+ var backupFiles = Directory.GetFiles(directoryPath, "*.bak", SearchOption.AllDirectories);
|
|
|
+ foreach (var backupFile in backupFiles)
|
|
|
{
|
|
|
- sw.Write(content);
|
|
|
+ TryCleanOrphanedBackup(backupFile);
|
|
|
}
|
|
|
+
|
|
|
+ // 处理版本化备份
|
|
|
+ CleanupOldBackupVersions(directoryPath);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 读取文件
|
|
|
+ /// 强制从备份恢复主文件。优先使用直接的 .bak,其次尝试版本化备份目录中的最新文件。
|
|
|
/// </summary>
|
|
|
- /// <param name="path"></param>
|
|
|
- /// <returns></returns>
|
|
|
- /// <exception cref="Exception"></exception>
|
|
|
- public static string ReadFile(string path)
|
|
|
+ /// <param name="originalPath">原文件路径。</param>
|
|
|
+ /// <returns>成功返回 true,失败返回 false。</returns>
|
|
|
+ public static bool ForceRecoverFile(string originalPath)
|
|
|
{
|
|
|
- if (File.Exists(path))
|
|
|
+ try
|
|
|
{
|
|
|
- string buffer;
|
|
|
- using (StreamReader sr = new StreamReader(path))
|
|
|
+ // 尝试从直接备份恢复
|
|
|
+ string backupPath = originalPath + ".bak";
|
|
|
+ if (File.Exists(backupPath))
|
|
|
{
|
|
|
- buffer = sr.ReadToEnd();
|
|
|
+ File.Copy(backupPath, originalPath, true);
|
|
|
+ Log($"从备份强制恢复: {originalPath}");
|
|
|
+ return true;
|
|
|
}
|
|
|
- return buffer;
|
|
|
+
|
|
|
+ // 尝试从版本化备份恢复
|
|
|
+ var recovered = TryFindLatestBackup(originalPath);
|
|
|
+ if (recovered != null)
|
|
|
+ {
|
|
|
+ File.Copy(recovered, originalPath, true);
|
|
|
+ Log($"从版本备份强制恢复: {originalPath}");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
}
|
|
|
- else
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- throw new Exception($"文件不存在:[{path}]");
|
|
|
+ Log($"强制恢复失败: {ex.Message}");
|
|
|
+ return false;
|
|
|
}
|
|
|
}
|
|
|
+ #endregion
|
|
|
|
|
|
+ #region 辅助方法
|
|
|
/// <summary>
|
|
|
- /// 检测文件名是否合规
|
|
|
+ /// 检测给定的文件名是否合规(不包含 Windows 文件名禁止字符)。
|
|
|
/// </summary>
|
|
|
- /// <param name="filename"></param>
|
|
|
- /// <returns></returns>
|
|
|
+ /// <param name="filename">仅文件名部分(不含路径)。</param>
|
|
|
+ /// <returns>文件名合法返回 true,否则返回 false。</returns>
|
|
|
public static bool CheckFileName(string filename)
|
|
|
{
|
|
|
- //
|
|
|
+ if (string.IsNullOrWhiteSpace(filename))
|
|
|
+ return false;
|
|
|
+
|
|
|
// 定义文件名合法性的正则表达式
|
|
|
- string pattern = @"^[^-\\/:*?""<>|\x00-\x1F]*$"; // 匹配不包含非法字符的文件名
|
|
|
+ string pattern = @"^[^\\/:*?""<>|\x00-\x1F]*$";
|
|
|
+ return Regex.IsMatch(filename, pattern);
|
|
|
+ }
|
|
|
|
|
|
- // 使用正则表达式进行匹配
|
|
|
- if (Regex.IsMatch(filename, pattern))
|
|
|
+ /// <summary>
|
|
|
+ /// 检查文件是否存在且有效(可读且长度大于 0)。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="path">文件完整路径。</param>
|
|
|
+ /// <returns>文件存在且有效返回 true,否则返回 false。</returns>
|
|
|
+ public static bool IsFileValid(string path)
|
|
|
+ {
|
|
|
+ try
|
|
|
{
|
|
|
- //Console.WriteLine("输入的名称符合文件名命名规则,并且不包含非法字符。");
|
|
|
- return true;
|
|
|
+ if (!File.Exists(path))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // 尝试读取一小部分内容来验证文件可访问性
|
|
|
+ using (var fs = File.OpenRead(path))
|
|
|
+ {
|
|
|
+ return fs.CanRead && fs.Length > 0;
|
|
|
+ }
|
|
|
}
|
|
|
- else
|
|
|
+ catch
|
|
|
{
|
|
|
- //Console.WriteLine("输入的名称不符合文件名命名规则,或者包含非法字符。");
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 为给定原始路径生成一个安全的临时文件路径(放在系统临时目录),包含随机 GUID 前缀。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="originalPath">原文件完整路径,用于生成可识别的临时文件名。</param>
|
|
|
+ /// <returns>生成的临时文件完整路径。</returns>
|
|
|
+ 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 私有实现方法
|
|
|
+ /// <summary>
|
|
|
+ /// 验证路径与文件名合法性(非空且文件名不包含非法字符)。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="path">要验证的文件路径。</param>
|
|
|
+ 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));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 内部写文件实现:创建目录、可选创建版本化备份、写入临时文件并以原子方式替换目标文件。
|
|
|
+ /// 发生异常时会尝试清理临时文件。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="content">要写入的文本内容。</param>
|
|
|
+ /// <param name="path">目标文件完整路径。</param>
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 创建版本化备份:将原文件复制到同目录下的备份目录,并带时间戳后缀。
|
|
|
+ /// 发生异常时仅记录日志,不抛出。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="originalPath">要备份的原文件路径。</param>
|
|
|
+ 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}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 检查并清理与原文件同名的临时文件(使用 .tmp 后缀的临时文件),并在可能的情况下记录发现的信息。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="originalPath">原文件完整路径。</param>
|
|
|
+ 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}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 尝试从直接的 .bak 文件恢复 JSON 文件并返回反序列化的对象;恢复成功后尝试覆盖主文件。
|
|
|
+ /// </summary>
|
|
|
+ /// <typeparam name="T">目标类型。</typeparam>
|
|
|
+ /// <param name="originalPath">原 JSON 文件路径。</param>
|
|
|
+ /// <returns>恢复并反序列化后的对象,失败返回 default(T)。</returns>
|
|
|
+ private static T TryRecoverJsonFile<T>(string originalPath)
|
|
|
+ {
|
|
|
+ // 尝试从直接备份恢复
|
|
|
+ string backupPath = originalPath + ".bak";
|
|
|
+ if (File.Exists(backupPath))
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ string json = ReadAndValidateJsonFile(backupPath);
|
|
|
+ T result = JsonConvert.DeserializeObject<T>(json);
|
|
|
+
|
|
|
+ // 尝试恢复主文件
|
|
|
+ TryRestoreMainFile(backupPath, originalPath);
|
|
|
+
|
|
|
+ Log($"从备份恢复Json文件: {originalPath}");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Log($"从备份恢复Json文件失败: {ex.Message}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return default;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 尝试从直接的 .bak 文件恢复文本文件并返回内容;恢复成功后尝试覆盖主文件。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="originalPath">原文件路径。</param>
|
|
|
+ /// <returns>恢复后的内容,失败返回 null。</returns>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 在版本化备份目录中查找最新的备份并尝试恢复 JSON 文件,成功返回反序列化对象并复制到主文件。
|
|
|
+ /// </summary>
|
|
|
+ /// <typeparam name="T">目标类型。</typeparam>
|
|
|
+ /// <param name="originalPath">原 JSON 文件路径。</param>
|
|
|
+ /// <returns>恢复并反序列化后的对象,失败返回 default(T)。</returns>
|
|
|
+ private static T TryFindAndRecoverJsonFile<T>(string originalPath)
|
|
|
+ {
|
|
|
+ string backup = TryFindLatestBackup(originalPath);
|
|
|
+ if (backup != null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ string json = ReadAndValidateJsonFile(backup);
|
|
|
+ T result = JsonConvert.DeserializeObject<T>(json);
|
|
|
+
|
|
|
+ File.Copy(backup, originalPath, true);
|
|
|
+ Log($"从版本备份恢复Json文件: {originalPath}");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Log($"从版本备份恢复Json文件失败: {ex.Message}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return default;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 在版本化备份目录中查找最新的备份并尝试恢复文本文件,成功后复制到主文件并返回内容。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="originalPath">原文件路径。</param>
|
|
|
+ /// <returns>恢复后的内容,失败返回 null。</returns>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 查找最新的版本化备份文件路径。优先检查指定的备份目录,其次检查原目录下同名带时间戳的文件。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="originalPath">原始文件路径。</param>
|
|
|
+ /// <returns>最新备份文件的完整路径或 null(未找到)。</returns>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 读取文件并在读取后进行 JSON 格式校验(适用于 JSON 文件)。如果格式无效则抛出异常。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="filePath">文件路径。</param>
|
|
|
+ /// <returns>文件文本内容。</returns>
|
|
|
+ /// <exception cref="InvalidDataException">当 JSON 格式无效时抛出。</exception>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 读取文件并进行基本验证(文件存在且读取结果非 null)。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="filePath">文件路径。</param>
|
|
|
+ /// <returns>文件内容字符串。</returns>
|
|
|
+ /// <exception cref="FileNotFoundException">文件不存在时抛出。</exception>
|
|
|
+ /// <exception cref="InvalidDataException">读取结果为 null 时抛出。</exception>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 将备份文件复制回主文件路径以进行恢复,失败时记录日志但不抛出。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="backupPath">备份文件路径。</param>
|
|
|
+ /// <param name="originalPath">目标主文件路径。</param>
|
|
|
+ private static void TryRestoreMainFile(string backupPath, string originalPath)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ File.Copy(backupPath, originalPath, true);
|
|
|
+ Log($"恢复主文件: {originalPath}");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Log($"恢复主文件失败: {ex.Message}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 尝试根据 .tmp 临时文件恢复原文件:如果临时文件较新或原文件不存在则移动临时文件覆盖原文件,否则删除临时文件。
|
|
|
+ /// 出错时尝试安全删除临时文件。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="tempPath">临时文件路径。</param>
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 清理孤立或过旧的 .bak 备份:如果主文件存在且备份比主文件早超过一定时间,则删除备份。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="backupPath">备份文件路径(通常以 .bak 结尾)。</param>
|
|
|
+ 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
|
|
|
+ {
|
|
|
+ // 忽略错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 清理版本化备份目录中超过保留数量的旧版本(按名称分组并按字符串降序保留最新的若干个)。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="directory">原文件所在目录,该目录下可能包含版本化备份子目录。</param>
|
|
|
+ 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
|
|
|
+ {
|
|
|
+ // 忽略错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 尝试安全删除指定文件,删除失败则忽略异常。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="filePath">要删除的文件路径。</param>
|
|
|
+ private static void SafeDelete(string filePath)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (File.Exists(filePath))
|
|
|
+ File.Delete(filePath);
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ // 忽略删除错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 内部日志方法,只有在配置启用时才会调用 LogHelper 记录调试日志。
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="message">日志消息正文。</param>
|
|
|
+ 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
|
|
|
}
|
|
|
}
|