| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986 |
- 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
- {
- /// <summary>
- /// 提供文件读写、备份与恢复相关的静态辅助方法。
- /// 功能包括:安全写入(临时文件 + 原子替换)、JSON 读写(含格式校验)、备份版本管理、临时/备份清理与恢复等。
- /// 该类内部包含可配置的选项,通过 <see cref="Configure"/> 进行设置。
- /// </summary>
- public static class FileHelper
- {
- #region 配置选项
- /// <summary>
- /// 文件操作辅助类的配置项集合。
- /// 可通过 <see cref="Configure"/> 修改。
- /// </summary>
- 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)
- {
- 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
- {
- if (File.Exists(path))
- {
- string json = ReadAndValidateJsonFile(path);
- return JsonConvert.DeserializeObject<T>(json);
- }
- }
- catch (Exception ex)
- {
- 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>
- /// 读取文本文件,支持自动恢复(从 .bak 或版本化备份恢复)。
- /// </summary>
- /// <param name="path">文件路径。</param>
- /// <param name="autoRecover">可选:覆盖默认的自动恢复行为(null 表示使用配置项)。</param>
- /// <returns>文件内容字符串。</returns>
- /// <exception cref="FileNotFoundException">当文件不存在且无法恢复时抛出。</exception>
- 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);
- }
- /// <summary>
- /// 在不抛出异常的情况下安全读取文件内容,失败时返回提供的默认值。
- /// </summary>
- /// <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(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);
- }
- /// <summary>
- /// 强制从备份恢复主文件。优先使用直接的 .bak,其次尝试版本化备份目录中的最新文件。
- /// </summary>
- /// <param name="originalPath">原文件路径。</param>
- /// <returns>成功返回 true,失败返回 false。</returns>
- 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 辅助方法
- /// <summary>
- /// 检测给定的文件名是否合规(不包含 Windows 文件名禁止字符)。
- /// </summary>
- /// <param name="filename">仅文件名部分(不含路径)。</param>
- /// <returns>文件名合法返回 true,否则返回 false。</returns>
- public static bool CheckFileName(string filename)
- {
- if (string.IsNullOrWhiteSpace(filename))
- return false;
- // 定义文件名合法性的正则表达式
- string pattern = @"^[^\\/:*?""<>|\x00-\x1F]*$";
- return Regex.IsMatch(filename, pattern);
- }
- /// <summary>
- /// 检查文件是否存在且有效(可读且长度大于 0)。
- /// </summary>
- /// <param name="path">文件完整路径。</param>
- /// <returns>文件存在且有效返回 true,否则返回 false。</returns>
- 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;
- }
- }
- /// <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
- #region 写CTQ的csv
- public static void WriteDataToCSV(DataFormCtq cd)
- {
- string s1, s2, s3, s4,
- t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18,
- r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18;
- //
- string path = "E:\\Data\\产品数据\\";
- if (!Directory.Exists(path))
- {
- Directory.CreateDirectory(path);
- }
- //
- t1 = "PPID";
- t2 = "PROCESS_NAME";
- t3 = "PROCESS_START_TIME";
- t4 = "CTQ_NAME";
- t5 = "CTQ_VALUE";
- t6 = "CTQ_LSL";
- t7 = "CTQ_USL";
- t8 = "PROCESS_END_TIME";
- t9 = "SUPPLIER_NAME";
- t10 = "COMMODITY_TYPE";
- t11 = "REVISION";
- t12 = "WO#";
- t13 = "MFG_LOT#";
- t14 = "MFG_ASSY_LINE";
- t15 = "PROCESS_OUTCOME";
- t16 = "PROCESS_MACHINE_ID";
- t17 = "CTQ_UNIT_OF_MEASURE";
- t18 = "ERROR_MESSAGE";
- //
- r1 = cd.PPID;
- r2 = cd.PROCESS_NAME;
- r3 = cd.PROCESS_START_TIME;
- r4 = cd.CTQ_NAME;
- r5 = cd.CTQ_VALUE.ToString();
- r6 = cd.CTQ_LSL.ToString();
- r7 = cd.CTQ_USL.ToString();
- r8 = cd.PROCESS_END_TIME;
- r9 = cd.SUPPLIER_NAME;
- r10 = cd.COMMODITY_TYPE;
- r11 = cd.REVISION;
- r12 = cd.WO;
- r13 = cd.MFG_LOT;
- r14 = cd.MFG_ASSY_LINE;
- r15 = cd.PROCESS_OUTCOME;
- r16 = cd.PROCESS_MACHINE_ID;
- r17 = cd.CTQ_UNIT_OF_MEASURE;
- r18 = cd.ERROR_MESSAGE;
- //=========================
- s1 = path;
- s2 = DateTime.Now.ToString("yyyyMMdd"); //表格命名以天记录
- s3 = t1 + "," + t2 + "," + t3 + "," + t4 + "," + t5 + "," + t6 + "," + t7 + "," + t8 + "," + t9 + "," + t10
- + "," + t11 + "," + t12 + "," + t13 + "," + t14 + "," + t15 + "," + t16 + "," + t17 + "," + t18;
- s4 = r1 + "," + r2 + "," + r3 + "," + r4 + "," + r5 + "," + r6 + "," + r7 + "," + r8 + "," + r9 + "," + r10
- + "," + r11 + "," + r12 + "," + r13 + "," + r14 + "," + r15 + "," + r16 + "," + r17 + "," + r18;
- //
- Save(s1, s2, s3, s4);
- }
- //写csv
- public static bool Save(string fullPath, string fileName, string RowName, string Data)
- {
- bool result = true;
- try
- {
- if (!Directory.Exists(fullPath))
- {
- Directory.CreateDirectory(fullPath);
- }
- if (fileName == null)
- {
- fileName = DateTime.Now.ToString("yyyyMMdd");
- }
- string text = "";
- string path = fullPath + "\\" + text;
- string text2 = ".csv";
- string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
- if (!File.Exists(fullPath + "\\" + fileNameWithoutExtension + fileName + text2))
- {
- using (File.Create(fullPath + "\\" + fileNameWithoutExtension + fileName + text2))
- {
- }
- FileStream fileStream2 = new FileStream(fullPath + "\\" + fileNameWithoutExtension + fileName + text2, FileMode.Append);
- StreamWriter streamWriter = new StreamWriter(fileStream2, Encoding.UTF8);
- streamWriter.WriteLine(RowName);
- streamWriter.WriteLine(Data);
- streamWriter.Flush();
- streamWriter.Close();
- fileStream2.Close();
- }
- else
- {
- FileStream fileStream2 = new FileStream(fullPath + "\\" + fileNameWithoutExtension + fileName + text2, FileMode.Append);
- StreamWriter streamWriter = new StreamWriter(fileStream2, Encoding.UTF8);
- streamWriter.WriteLine(Data);
- streamWriter.Flush();
- streamWriter.Close();
- fileStream2.Close();
- }
- }
- catch
- {
- result = false;
- }
- return result;
- }
- #endregion
- }
- }
|