2
0
Эх сурвалжийг харах

将 config 写入改为原子写入,防止潜在的异常中断导致配置文件失效问题

HerobrineXia 1 сар өмнө
parent
commit
6b292a45f7

+ 1 - 1
Shared/General/Config.cs

@@ -112,7 +112,7 @@ public class JsonConfigProvider : IConfigProvider {
     public void Save() {
         if (!isDirty) return;
         isDirty = false;
-        lock (this) FileUtils.Write(filePath, json.Value.ToString(Formatting.Indented));
+        lock (this) FileUtils.WriteAtomic(filePath, json.Value.ToString(Formatting.Indented));
     }
 }
 

+ 24 - 0
Shared/Utils/FileUtils.cs

@@ -104,6 +104,30 @@ public static class FileUtils {
         });
     }
 
+    /// <summary>
+    /// 原子写入文件。成功时目标文件完整切换为新内容,失败时已有目标文件保持不变。
+    /// 不提供并发写入合并保证。
+    /// </summary>
+    public static void WriteAtomic(string filePath, string text, Encoding? encoding = null) =>
+        FileUtils.WriteAtomic(filePath, (encoding ?? new UTF8Encoding()).GetBytes(text));
+    /// <summary>
+    /// 原子写入文件。成功时目标文件完整切换为新内容,失败时已有目标文件保持不变。
+    /// 不提供并发写入合并保证。
+    /// </summary>
+    public static void WriteAtomic(string filePath, byte[] content) {
+        string tempPath = $"{filePath}.{Guid.NewGuid():N}.tmp";
+        try {
+            FileUtils.Write(tempPath, content);
+            if (FileUtils.Exists(filePath)) {
+                File.Replace(PathUtils.ForApi(tempPath), PathUtils.ForApi(filePath), null, true);
+            } else {
+                File.Move(PathUtils.ForApi(tempPath), PathUtils.ForApi(filePath));
+            }
+        } finally {
+            FileUtils.Delete(tempPath);
+        }
+    }
+
     /// <summary>
     /// 创建文件,并将 <paramref name="stream" /> 写入文件。
     /// 会将流的位置主动重置到开头。

+ 16 - 0
Tests/General/ConfigTest.cs

@@ -89,6 +89,22 @@ public class ConfigTest : TestWithFolder {
         await Assert.That(FileUtils.ReadAsString(configFile)).Contains("\"text\": \"pending 中文\"");
     }
 
+    [Test]
+    public async Task JsonConfigProvider_Save_原子替换且不残留临时文件() {
+        string configFile = Path.Combine(tempFolder, "config.json");
+        FileUtils.Write(configFile, "{\"existing\":true}");
+        var provider = new JsonConfigProvider(configFile);
+        var entry = new ConfigEntry<string>("text", "", provider: provider);
+
+        entry.Set("updated");
+        provider.Save();
+
+        Newtonsoft.Json.Linq.JObject config = FileUtils.ReadAsJson<Newtonsoft.Json.Linq.JObject>(configFile)!;
+        await Assert.That(config.Value<bool>("existing")).IsTrue();
+        await Assert.That(config.Value<string>("text")).IsEqualTo("updated");
+        await Assert.That(DirectoryUtils.EnumerateFiles(tempFolder, searchPattern: "config.json.*.tmp")).IsEmpty();
+    }
+
     public class TestConfigItem {
         public string Name { get; set; } = "";
         public int Count { get; set; }