using Serilog; using System; using System.IO; using System.Linq; using System.Threading.Tasks; namespace Ayay.SerilogLogs { /// /// 日志清理工具 /// 弥补 Serilog 原生清理功能的不足,支持“按天数”和“按总大小”进行全局清理。 /// ✅ 已适配多级目录结构,会自动清理删除文件后留下的空文件夹。 /// public static class LogCleaner { /// /// 异步执行清理任务 /// /// 配置选项 public static void RunAsync(LogOptions opts) { string rootPath = opts.LogRootPath; // 日志根目录 int maxDays = opts.MaxRetentionDays; // 最大保留天数 long maxBytes = opts.MaxTotalLogSize; // 最大总字节数 Task.Run(() => { try { CleanUp(rootPath, maxDays, maxBytes); } catch (Exception ex) { Log.Error(ex, "[LogCleaner] 日志自动清理任务执行失败"); } }); } private static void CleanUp(string rootPath, int maxDays, long maxBytes) { var dirInfo = new DirectoryInfo(rootPath); if (!dirInfo.Exists) return; // ========================================================= // 第一步:清理文件 (递归查找所有子目录) // ========================================================= // 获取所有 .txt 文件,按最后修改时间升序排列 (最旧的在前面) // SearchOption.AllDirectories 确保了能扫描到 2026-01-15 这种子文件夹里的内容 var allFiles = dirInfo.GetFiles("*.txt", SearchOption.AllDirectories) .OrderBy(f => f.LastWriteTime) .ToList(); if (allFiles.Count == 0) return; long currentTotalSize = 0; var cutOffDate = DateTime.Now.Date.AddDays(-maxDays); // 只保留到今天之前的 N 天 int deletedCount = 0; // --- 策略 A: 按时间清理 --- for (int i = allFiles.Count - 1; i >= 0; i--) { var file = allFiles[i]; if (file.LastWriteTime.Date < cutOffDate) { try { file.Delete(); allFiles.RemoveAt(i); deletedCount++; } catch { /* 忽略占用 */ } } else { currentTotalSize += file.Length; } } if (deletedCount > 0) { Log.ForContext("SourceContext", LogModules.Core) .Information("[LogCleaner] 时间策略: 已删除 {Count} 个过期文件", deletedCount); } // --- 策略 B: 按总大小清理 --- if (currentTotalSize > maxBytes) { int sizeDeletedCount = 0; long freedBytes = 0; foreach (var file in allFiles) { if (currentTotalSize <= maxBytes) break; try { long len = file.Length; file.Delete(); currentTotalSize -= len; freedBytes += len; sizeDeletedCount++; } catch { /* 忽略占用 */ } } if (sizeDeletedCount > 0) { Log.ForContext("SourceContext", LogModules.Core) .Warning("[LogCleaner] 空间策略: 已删除 {Count} 个旧文件, 释放 {SizeMB:F2} MB", sizeDeletedCount, freedBytes / 1024.0 / 1024.0); } } // ========================================================= // 第二步:清理空文件夹 (新增逻辑) // ========================================================= // 目的:当 2026-01-01 文件夹里的文件都被删光后,这个文件夹本身也应该被删掉 DeleteEmptyDirectories(dirInfo); } /// /// 递归删除空文件夹 /// private static void DeleteEmptyDirectories(DirectoryInfo dir) { // 1. 先递归处理子文件夹 foreach (var subDir in dir.GetDirectories()) { DeleteEmptyDirectories(subDir); } // 2. 检查当前文件夹是否为空 (且不是根目录自己) try { // 重新刷新状态 dir.Refresh(); // 如果没有文件 且 没有子文件夹 if (dir.GetFiles().Length == 0 && dir.GetDirectories().Length == 0) { // 注意:不要删除根目录 LogRootPath // 这里虽然逻辑上递归会删到底,但通常外层调用时传入的是 Logs 根目录, // 只要 LogBootstrapper 里保证 Logs 存在,这里删掉子目录没问题。 // 为了安全,可以判断一下是否是根目录的直接子级,或者简单地 try catch 忽略根目录无法删除的异常。 dir.Delete(); } } catch { // 忽略异常 (比如文件夹正被打开,或者试图删除根目录但被占用) } } } }