| | | 1 | | using Microsoft.Extensions.Configuration; |
| | | 2 | | using System.Globalization; |
| | | 3 | | using System.IO.Abstractions; |
| | | 4 | | using System.Text; |
| | | 5 | | |
| | | 6 | | namespace Anichron.Infrastructure.Configuration; |
| | | 7 | | |
| | | 8 | | public sealed record IniEntry(string Section, string Key, Func<string> DefaultValue); |
| | | 9 | | |
| | 17 | 10 | | public sealed class AppIniInitializer(IFileSystem fileSystem) |
| | | 11 | | { |
| | | 12 | | public void EnsureUpToDate(string iniPath, IReadOnlyList<IniEntry> expectedEntries) |
| | 17 | 13 | | { |
| | 17 | 14 | | fileSystem.Directory.CreateDirectory(fileSystem.Path.GetDirectoryName(iniPath)!); |
| | | 15 | | |
| | 17 | 16 | | if (!fileSystem.File.Exists(iniPath)) |
| | 10 | 17 | | { |
| | 10 | 18 | | WriteNew(iniPath, expectedEntries); |
| | 9 | 19 | | return; |
| | | 20 | | } |
| | | 21 | | |
| | 7 | 22 | | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(fileSystem.File.ReadAllText(iniPath))); |
| | 7 | 23 | | var config = new ConfigurationBuilder().AddIniStream(stream).Build(); |
| | 7 | 24 | | var missing = expectedEntries |
| | 7 | 25 | | .Where(e => config[$"{e.Section}:{e.Key}"] is null) |
| | 7 | 26 | | .ToList(); |
| | | 27 | | |
| | 7 | 28 | | if (missing.Count == 0) |
| | 4 | 29 | | return; |
| | | 30 | | |
| | 3 | 31 | | AppendMissing(iniPath, missing); |
| | 19 | 32 | | } |
| | | 33 | | |
| | | 34 | | private void WriteNew(string iniPath, IReadOnlyList<IniEntry> entries) |
| | 10 | 35 | | { |
| | 10 | 36 | | var sb = new StringBuilder(); |
| | 58 | 37 | | foreach (var section in entries.GroupBy(e => e.Section)) |
| | 14 | 38 | | { |
| | 14 | 39 | | sb.AppendLine(CultureInfo.InvariantCulture, $"[{section.Key}]"); |
| | 80 | 40 | | foreach (var entry in section) |
| | 19 | 41 | | sb.AppendLine(CultureInfo.InvariantCulture, $"{entry.Key} = {entry.DefaultValue()}"); |
| | 14 | 42 | | sb.AppendLine(); |
| | 14 | 43 | | } |
| | | 44 | | |
| | | 45 | | try |
| | 10 | 46 | | { |
| | | 47 | | // FileMode.CreateNew is O_CREAT|O_EXCL — atomic exclusive creation; only one wins if two instances race. |
| | 10 | 48 | | using var stream = fileSystem.FileStream.New(iniPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); |
| | 8 | 49 | | using var writer = new StreamWriter(stream); |
| | 8 | 50 | | writer.Write(sb.ToString()); |
| | 8 | 51 | | } |
| | 1 | 52 | | catch (IOException) |
| | 1 | 53 | | { |
| | 1 | 54 | | return; // Another instance created the file concurrently — proceed with that file. |
| | | 55 | | } |
| | | 56 | | |
| | 8 | 57 | | Console.Error.WriteLine( |
| | 8 | 58 | | $"[WARN] Configuration file created at {iniPath}. Review the values and keep this file secure."); |
| | 9 | 59 | | } |
| | | 60 | | |
| | | 61 | | private void AppendMissing(string iniPath, IReadOnlyList<IniEntry> missing) |
| | 3 | 62 | | { |
| | 3 | 63 | | var sb = new StringBuilder(); |
| | 19 | 64 | | foreach (var section in missing.GroupBy(e => e.Section)) |
| | 5 | 65 | | { |
| | 5 | 66 | | sb.AppendLine(); |
| | 5 | 67 | | sb.AppendLine(CultureInfo.InvariantCulture, $"[{section.Key}]"); |
| | 25 | 68 | | foreach (var entry in section) |
| | 5 | 69 | | sb.AppendLine(CultureInfo.InvariantCulture, $"{entry.Key} = {entry.DefaultValue()}"); |
| | 5 | 70 | | } |
| | | 71 | | |
| | | 72 | | // Not atomic: two processes racing here can both append the same entries. |
| | | 73 | | // The INI provider tolerates duplicate keys (last value wins), so behavior is correct. |
| | 3 | 74 | | fileSystem.File.AppendAllText(iniPath, sb.ToString()); |
| | 3 | 75 | | Console.Error.WriteLine($"[INFO] Added missing configuration entries to {iniPath}."); |
| | 3 | 76 | | } |
| | | 77 | | } |