59 lines
2.2 KiB
C#
59 lines
2.2 KiB
C#
namespace MiGu.Server.Infra;
|
|
|
|
/// <summary>
|
|
/// 原子写文件工具:先写同目录临时文件,再用 File.Replace / File.Move 整体替换目标,
|
|
/// 避免 File.WriteAllText 写到一半进程崩溃 / 断电导致目标文件被截断成「半个 JSON」。
|
|
///
|
|
/// 用于 rbac.json / config-*.json / ops-audit.json 等关键持久化文件 —— 这些文件一旦损坏,
|
|
/// 加载时会被当成「解析失败」回退默认 seed,进而静默丢失自定义用户 / 角色 / 配置。
|
|
/// </summary>
|
|
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 */ }
|
|
}
|
|
}
|
|
|
|
/// <summary>把疑似损坏的文件复制一份带时间戳的备份(不抛异常)。返回备份路径或 null。</summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|