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