Config.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. using System.Security.Cryptography;
  2. namespace MeloongCore;
  3. #region ConfigProvider | 配置提供器
  4. public interface IConfigProvider {
  5. void Set<T>(string key, T? value, bool encrypted);
  6. void Remove(string key);
  7. T? Read<T>(string key, T? defaultValue, bool encrypted);
  8. bool HasValue(string key);
  9. void DiscardCache();
  10. }
  11. public class JsonConfigProvider : IConfigProvider {
  12. public static readonly ConcurrentBag<WeakReference<JsonConfigProvider>> allProviders = [];
  13. private readonly string filePath;
  14. #pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 'required' 修饰符或声明为可以为 null。
  15. public JsonConfigProvider(string filePath) {
  16. #pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 'required' 修饰符或声明为可以为 null。
  17. this.filePath = filePath;
  18. allProviders.Add(new(this));
  19. saveWorker = new(Save, 500, RateLimitMode.WaitUnillNoNewInvoke);
  20. InitJson();
  21. }
  22. // 原始文件缓存
  23. private Lazy<JObject> json;
  24. // 反序列化后的结果缓存
  25. private readonly ConcurrentDictionary<string, object?> cache = new();
  26. private void InitJson() {
  27. json = new(() => {
  28. try {
  29. return FileUtils.Exists(filePath) ? ((JObject?) FileUtils.ReadAsJson(filePath) ?? []) : [];
  30. } catch (Exception ex) {
  31. Logger.Error(ex, $"读取配置文件失败({filePath})", LogBehavior.Alert);
  32. return [];
  33. }
  34. });
  35. }
  36. // ================================= 设置、读取、缓存 =================================
  37. public void Set<T>(string key, T? value, bool encrypted) {
  38. cache[key] = value;
  39. lock (this) {
  40. if (value is null) {
  41. // 直接在 JSON 中表示为 null
  42. json.Value[key] = JValue.CreateNull();
  43. Logger.Trace($"配置已修改:{key} = null({filePath})");
  44. } else if (encrypted) {
  45. // 需要加密,在 JSON 中保存密文字符串
  46. throw new NotImplementedException("加密需要改为使用识别码,现在尚未实现。");
  47. // Logger.Trace($"配置已修改:{key} = 已加密({filePath})");
  48. // json.Value[key] = CryptographyUtils.AesEncrypt(value is string str ? str : JsonConvert.SerializeObject(value));
  49. } else {
  50. // 用 JToken 保留原始结构
  51. JToken token = JToken.FromObject(value);
  52. json.Value[key] = token;
  53. Logger.Trace($"配置已修改:{key} = {token.ToString(Formatting.None)}({filePath})");
  54. }
  55. }
  56. MakeDirty();
  57. }
  58. public T? Read<T>(string key, T? defaultValue, bool encrypted) {
  59. if (cache.TryGetValue(key, out var cachedValue)) return (T?) cachedValue; // 读取缓存
  60. JToken entry;
  61. lock (this) {
  62. if (!json.Value.TryGetValue(key, out entry!)) return defaultValue; // 未找到该键
  63. entry = entry.DeepClone();
  64. }
  65. try {
  66. T? result;
  67. if (entry.Type is JTokenType.Null or JTokenType.Undefined) {
  68. result = default;
  69. } else if (encrypted) {
  70. string plainText = CryptographyUtils.AesDecrypt(entry.Value<string>())!;
  71. result = typeof(T) == typeof(string) ? (T) (object) plainText : JsonConvert.DeserializeObject<T>(plainText);
  72. } else {
  73. result = entry.ToObject<T>();
  74. }
  75. cache[key] = result;
  76. return result;
  77. } catch (CryptographicException ex) {
  78. Logger.Error(ex, $"解密配置失败,该配置将被重置({key})", LogBehavior.Alert);
  79. Remove(key);
  80. return defaultValue;
  81. }
  82. }
  83. public bool HasValue(string key) {
  84. lock (this) return json.Value.ContainsKey(key);
  85. }
  86. public void Remove(string key) {
  87. lock (this) json.Value.Remove(key);
  88. cache.TryRemove(key, out _);
  89. MakeDirty();
  90. }
  91. public void DiscardCache() {
  92. lock (this) InitJson();
  93. cache.Clear();
  94. }
  95. // ===================================== 保存 =====================================
  96. private readonly RateLimitedWorker saveWorker;
  97. private bool isDirty = false;
  98. public void MakeDirty() {
  99. lock (this) isDirty = true;
  100. saveWorker.Start();
  101. }
  102. /// <summary>同步保存配置,并等待同一实例正在进行的保存完成;保存失败时保留待保存状态。</summary>
  103. public void Save() {
  104. lock (this) {
  105. if (!isDirty) return;
  106. FileUtils.WriteAtomic(filePath, json.Value.ToString(Formatting.Indented));
  107. isDirty = false;
  108. }
  109. }
  110. }
  111. public static class ConfigProviders {
  112. public static JsonConfigProvider Default = new(Path.Combine(Paths.AppDataThenName, "config.json"));
  113. }
  114. #endregion
  115. #region ConfigEntry | 单个配置项
  116. /// <summary>
  117. /// 当前获取到的配置值。
  118. /// </summary>
  119. public class PreviewGetEventArgs<T>(T? value) : EventArgs {
  120. public T? Value { get; set; } = value;
  121. }
  122. /// <summary>
  123. /// 使用固定的 <see cref="IConfigProvider"/> 的配置项。
  124. /// <para/>若不指定 <paramref name="provider"/>,则使用 <see cref="ConfigProviders.Default"/>。
  125. /// </summary>
  126. public class ConfigEntry<T>(string key, T? defaultValue, bool encrypted = false, IConfigProvider? provider = null) {
  127. public IConfigProvider Provider { get; } = provider ?? ConfigProviders.Default;
  128. /// <summary>
  129. /// 当调整配置项的值时触发,参数为新值。<para/>新值可能和旧值相同。
  130. /// </summary>
  131. public event Action<T?>? Changed;
  132. /// <summary>
  133. /// 当获取配置项的值时触发。
  134. /// </summary>
  135. public event Action<PreviewGetEventArgs<T>, IConfigProvider>? PreviewGet;
  136. public void Edit(Func<T?, T?> editFunc) => Set(editFunc(Get()));
  137. public void Edit(RefAction editAction) {
  138. var current = Get();
  139. editAction(ref current);
  140. Set(current);
  141. }
  142. public delegate void RefAction(ref T? value);
  143. public void Set(T? value) {
  144. Provider.Set(key, value, encrypted);
  145. Changed?.Invoke(value);
  146. }
  147. public void Reset() {
  148. if (!Provider.HasValue(key)) return; // 值未设置,无需重置
  149. Provider.Remove(key);
  150. Changed?.Invoke(defaultValue);
  151. }
  152. public T? Get() {
  153. PreviewGetEventArgs<T> args = new(Provider.Read(key, defaultValue, encrypted));
  154. PreviewGet?.Invoke(args, Provider);
  155. return args.Value;
  156. }
  157. public bool HasValue() => Provider.HasValue(key);
  158. }
  159. /// <summary>
  160. /// 在每次操作时动态指定 <see cref="IConfigProvider"/> 的配置项。
  161. /// </summary>
  162. public class DynamicConfigEntry<T>(string key, T? defaultValue, bool encrypted = false) {
  163. /// <summary>
  164. /// 当调整配置项的值时触发,参数为新值。<para/>新值可能和旧值相同。
  165. /// </summary>
  166. public event Action<T?>? Changed;
  167. /// <summary>
  168. /// 当获取配置项的值时触发。
  169. /// </summary>
  170. public event Action<PreviewGetEventArgs<T>, IConfigProvider>? PreviewGet;
  171. public void Edit(Func<T?, T?> editFunc, IConfigProvider provider) => Set(editFunc(Get(provider)), provider);
  172. public void Edit(RefAction editAction, IConfigProvider provider) {
  173. var current = Get(provider);
  174. editAction(ref current);
  175. Set(current, provider);
  176. }
  177. public delegate void RefAction(ref T? value);
  178. public void Set(T? value, IConfigProvider provider) {
  179. provider.Set(key, value, encrypted);
  180. Changed?.Invoke(value);
  181. }
  182. public void Reset(IConfigProvider provider) {
  183. if (!provider.HasValue(key)) return; // 值未设置,无需重置
  184. provider.Remove(key);
  185. Changed?.Invoke(defaultValue);
  186. }
  187. public T? Get(IConfigProvider provider) {
  188. PreviewGetEventArgs<T> args = new(provider.Read(key, defaultValue, encrypted));
  189. PreviewGet?.Invoke(args, provider);
  190. return args.Value;
  191. }
  192. public bool HasValue(IConfigProvider provider) => provider.HasValue(key);
  193. }
  194. #endregion
  195. public static class ConfigUtils {
  196. /// <summary>立即保存所有配置。</summary>
  197. public static void SaveAll() {
  198. foreach (var weakRef in JsonConfigProvider.allProviders) {
  199. if (weakRef.TryGetTarget(out var provider)) provider.Save();
  200. }
  201. }
  202. private static readonly Lazy<JObject> secret = new(() => {
  203. string filePath = Path.Combine(Paths.AppData, "Meloong", "Dev", "secret.json");
  204. try {
  205. return FileUtils.Exists(filePath) ? JObject.Parse(FileUtils.ReadAsString(filePath)) : [];
  206. } catch {
  207. // JSON 异常可能包含凭据内容,不保留原始异常,也不回退空配置。
  208. throw new InvalidDataException($"读取密钥文件失败({filePath}),请检查文件内容与加密格式。");
  209. }
  210. });
  211. /// <summary>从 secret.json 中读取加密的密钥;如果未找到对应密钥则抛出异常。</summary>
  212. public static string GetSecret(string key) {
  213. string value = secret.Value.Value<string>(key) ?? throw new KeyNotFoundException($"未找到密钥:{key}");
  214. try {
  215. return CryptographyUtils.AesDecrypt(value)!;
  216. } catch {
  217. throw new InvalidDataException($"读取密钥失败({key}),请检查文件内容与加密格式。");
  218. }
  219. }
  220. }