namespace MiGu.Server.Infra; /// /// 原子写文件工具:先写同目录临时文件,再用 File.Replace / File.Move 整体替换目标, /// 避免 File.WriteAllText 写到一半进程崩溃 / 断电导致目标文件被截断成「半个 JSON」。 /// /// 用于 rbac.json / config-*.json / ops-audit.json 等关键持久化文件 —— 这些文件一旦损坏, /// 加载时会被当成「解析失败」回退默认 seed,进而静默丢失自定义用户 / 角色 / 配置。 /// public static class AtomicFile { public static void WriteAllText(string path, string contents) { var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); // 临时名带 GUID:保证同一目标文件的并发写各用独立临时文件,互不覆盖(即便调用方未加锁)。 var tmp = $"{path}.{Guid.NewGuid():N}.tmp"; try { File.WriteAllText(tmp, contents); try { if (File.Exists(path)) File.Replace(tmp, path, null); else File.Move(tmp, path); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // 个别环境(杀软锁定 / 跨卷)File.Replace 会失败:退化为覆盖复制兜底(仍优于半截写入)。 File.Copy(tmp, path, true); } } finally { // 兜底清理:File.Replace/Move 成功时 tmp 已不存在;其余异常路径下避免遗留临时文件累积。 try { if (File.Exists(tmp)) File.Delete(tmp); } catch { /* ignore */ } } } /// 把疑似损坏的文件复制一份带时间戳的备份(不抛异常)。返回备份路径或 null。 public static string? BackupCorrupt(string path) { try { if (!File.Exists(path)) return null; var bak = $"{path}.corrupt-{DateTime.UtcNow:yyyyMMddHHmmss}"; File.Copy(path, bak, true); return bak; } catch { return null; } } }