IniFileCfgSource.cs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. using Apq.Cfg.Sources;
  2. using Apq.Cfg.Sources.File;
  3. using Microsoft.Extensions.Configuration;
  4. using Microsoft.Extensions.Configuration.Ini;
  5. using Microsoft.Extensions.FileProviders;
  6. namespace Apq.Cfg.Ini;
  7. /// <summary>
  8. /// INI 文件配置源,支持读取和写入 INI 格式的配置文件
  9. /// </summary>
  10. internal sealed class IniFileCfgSource : FileCfgSourceBase, IWritableCfgSource
  11. {
  12. /// <summary>
  13. /// 初始化 IniFileCfgSource 实例
  14. /// </summary>
  15. /// <param name="path">INI 文件路径</param>
  16. /// <param name="level">配置层级,数值越大优先级越高</param>
  17. /// <param name="writeable">是否可写</param>
  18. /// <param name="optional">是否为可选文件</param>
  19. /// <param name="reloadOnChange">文件变更时是否自动重载</param>
  20. /// <param name="isPrimaryWriter">是否为主要写入源</param>
  21. public IniFileCfgSource(string path, int level, bool writeable, bool optional, bool reloadOnChange,
  22. bool isPrimaryWriter)
  23. : base(path, level, writeable, optional, reloadOnChange, isPrimaryWriter)
  24. {
  25. }
  26. /// <summary>
  27. /// 构建 Microsoft.Extensions.Configuration 的 INI 配置源
  28. /// </summary>
  29. /// <returns>Microsoft.Extensions.Configuration.Ini.IniConfigurationSource 实例</returns>
  30. public override IConfigurationSource BuildSource()
  31. {
  32. var (fp, file) = CreatePhysicalFileProvider(_path);
  33. var src = new IniConfigurationSource
  34. {
  35. FileProvider = fp,
  36. Path = file,
  37. Optional = _optional,
  38. ReloadOnChange = _reloadOnChange
  39. };
  40. src.ResolveFileProvider();
  41. return src;
  42. }
  43. /// <summary>
  44. /// 应用配置更改到 INI 文件
  45. /// </summary>
  46. /// <param name="changes">要应用的配置更改</param>
  47. /// <param name="cancellationToken">取消令牌</param>
  48. /// <returns>表示异步操作的任务</returns>
  49. /// <exception cref="InvalidOperationException">当配置源不可写时抛出</exception>
  50. public async Task ApplyChangesAsync(IReadOnlyDictionary<string, string?> changes, CancellationToken cancellationToken)
  51. {
  52. if (!IsWriteable)
  53. throw new InvalidOperationException($"配置源 (层级 {Level}) 不可写");
  54. EnsureDirectoryFor(_path);
  55. var sections = new Dictionary<string, Dictionary<string, string?>>(StringComparer.OrdinalIgnoreCase);
  56. string? currentSection = null;
  57. if (File.Exists(_path))
  58. {
  59. var readEncoding = DetectEncodingEnhanced(_path);
  60. var lines = await File.ReadAllLinesAsync(_path, readEncoding, cancellationToken).ConfigureAwait(false);
  61. foreach (var line in lines)
  62. {
  63. var trimmed = line.Trim();
  64. if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith(";")) continue;
  65. if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
  66. {
  67. currentSection = trimmed.Substring(1, trimmed.Length - 2);
  68. if (!sections.ContainsKey(currentSection))
  69. sections[currentSection] = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
  70. continue;
  71. }
  72. var idx = trimmed.IndexOf('=');
  73. if (idx > 0)
  74. {
  75. var key = trimmed.Substring(0, idx).Trim();
  76. var value = trimmed.Substring(idx + 1);
  77. var section = currentSection ?? "";
  78. if (!sections.ContainsKey(section))
  79. sections[section] = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
  80. sections[section][key] = value;
  81. }
  82. }
  83. }
  84. foreach (var (configKey, value) in changes)
  85. {
  86. var colonIdx = configKey.IndexOf(':');
  87. string section, key;
  88. if (colonIdx > 0)
  89. {
  90. section = configKey.Substring(0, colonIdx);
  91. key = configKey.Substring(colonIdx + 1);
  92. }
  93. else
  94. {
  95. section = "";
  96. key = configKey;
  97. }
  98. if (!sections.ContainsKey(section))
  99. sections[section] = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
  100. if (value == null)
  101. sections[section].Remove(key);
  102. else
  103. sections[section][key] = value;
  104. }
  105. var sb = new System.Text.StringBuilder();
  106. if (sections.TryGetValue("", out var rootKeys) && rootKeys.Count > 0)
  107. {
  108. foreach (var kv in rootKeys)
  109. sb.Append(kv.Key).Append('=').Append(kv.Value).AppendLine();
  110. }
  111. foreach (var (sectionName, keys) in sections)
  112. {
  113. if (string.IsNullOrEmpty(sectionName) || keys.Count == 0) continue;
  114. if (sb.Length > 0) sb.AppendLine();
  115. sb.Append('[').Append(sectionName).Append(']').AppendLine();
  116. foreach (var kv in keys)
  117. sb.Append(kv.Key).Append('=').Append(kv.Value).AppendLine();
  118. }
  119. await File.WriteAllTextAsync(_path, sb.ToString(), GetWriteEncoding(), cancellationToken).ConfigureAwait(false);
  120. }
  121. }