4
0

YamlResourceCollection.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. using System.Collections.Specialized;
  2. using RackPeek.Domain.Persistence;
  3. using RackPeek.Domain.Persistence.Yaml;
  4. using RackPeek.Domain.Resources;
  5. using RackPeek.Domain.Resources.Models;
  6. using RackPeek.Domain.Resources.Services;
  7. using RackPeek.Domain.Resources.SystemResources;
  8. using RackPeek.Yaml;
  9. using YamlDotNet.Core;
  10. using YamlDotNet.Serialization;
  11. using YamlDotNet.Serialization.NamingConventions;
  12. public sealed class YamlResourceCollection : IResourceCollection
  13. {
  14. private readonly string _filePath;
  15. private readonly ITextFileStore _fileStore;
  16. private static readonly SemaphoreSlim _fileLock = new(1, 1);
  17. private static readonly List<Resource> _resources = new();
  18. public YamlResourceCollection(string filePath, ITextFileStore fileStore)
  19. {
  20. _filePath = filePath;
  21. _fileStore = fileStore;
  22. }
  23. public async Task LoadAsync()
  24. {
  25. var loaded = await LoadFromFileAsync();
  26. _resources.Clear();
  27. _resources.AddRange(loaded);
  28. }
  29. public IReadOnlyList<Hardware> HardwareResources =>
  30. _resources.OfType<Hardware>().ToList();
  31. public IReadOnlyList<SystemResource> SystemResources =>
  32. _resources.OfType<SystemResource>().ToList();
  33. public IReadOnlyList<Service> ServiceResources =>
  34. _resources.OfType<Service>().ToList();
  35. public Resource? GetByName(string name) =>
  36. _resources.FirstOrDefault(r =>
  37. r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
  38. public Task AddAsync(Resource resource) =>
  39. UpdateWithLockAsync(list =>
  40. {
  41. if (list.Any(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase)))
  42. throw new InvalidOperationException($"'{resource.Name}' already exists.");
  43. resource.Kind = GetKind(resource);
  44. list.Add(resource);
  45. });
  46. public Task UpdateAsync(Resource resource) =>
  47. UpdateWithLockAsync(list =>
  48. {
  49. var index = list.FindIndex(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
  50. if (index == -1) throw new InvalidOperationException("Not found.");
  51. resource.Kind = GetKind(resource);
  52. list[index] = resource;
  53. });
  54. public Task DeleteAsync(string name) =>
  55. UpdateWithLockAsync(list =>
  56. list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
  57. private async Task UpdateWithLockAsync(Action<List<Resource>> action)
  58. {
  59. await _fileLock.WaitAsync();
  60. try
  61. {
  62. action(_resources);
  63. var serializer = new SerializerBuilder()
  64. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  65. .Build();
  66. var payload = new OrderedDictionary
  67. {
  68. ["resources"] = _resources.Select(SerializeResource).ToList()
  69. };
  70. await _fileStore.WriteAllTextAsync(
  71. _filePath,
  72. serializer.Serialize(payload));
  73. }
  74. finally
  75. {
  76. _fileLock.Release();
  77. }
  78. }
  79. private async Task<List<Resource>> LoadFromFileAsync()
  80. {
  81. var yaml = await _fileStore.ReadAllTextAsync(_filePath);
  82. if (string.IsNullOrWhiteSpace(yaml))
  83. return new();
  84. var deserializer = new DeserializerBuilder()
  85. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  86. .WithCaseInsensitivePropertyMatching()
  87. .WithTypeConverter(new StorageSizeYamlConverter())
  88. .WithTypeDiscriminatingNodeDeserializer(options =>
  89. {
  90. options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
  91. {
  92. { Server.KindLabel, typeof(Server) },
  93. { Switch.KindLabel, typeof(Switch) },
  94. { Firewall.KindLabel, typeof(Firewall) },
  95. { Router.KindLabel, typeof(Router) },
  96. { Desktop.KindLabel, typeof(Desktop) },
  97. { Laptop.KindLabel, typeof(Laptop) },
  98. { AccessPoint.KindLabel, typeof(AccessPoint) },
  99. { Ups.KindLabel, typeof(Ups) },
  100. { SystemResource.KindLabel, typeof(SystemResource) },
  101. { Service.KindLabel, typeof(Service) }
  102. });
  103. })
  104. .Build();
  105. try
  106. {
  107. var root = deserializer.Deserialize<YamlRoot>(yaml);
  108. return root?.Resources ?? new();
  109. }
  110. catch (YamlException)
  111. {
  112. return new();
  113. }
  114. }
  115. private string GetKind(Resource resource) => resource switch
  116. {
  117. Server => "Server",
  118. Switch => "Switch",
  119. Firewall => "Firewall",
  120. Router => "Router",
  121. Desktop => "Desktop",
  122. Laptop => "Laptop",
  123. AccessPoint => "AccessPoint",
  124. Ups => "Ups",
  125. SystemResource => "System",
  126. Service => "Service",
  127. _ => throw new InvalidOperationException($"Unknown resource type: {resource.GetType().Name}")
  128. };
  129. private OrderedDictionary SerializeResource(Resource resource)
  130. {
  131. var map = new OrderedDictionary
  132. {
  133. ["kind"] = GetKind(resource)
  134. };
  135. var serializer = new SerializerBuilder()
  136. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  137. .Build();
  138. var yaml = serializer.Serialize(resource);
  139. var props = new DeserializerBuilder()
  140. .Build()
  141. .Deserialize<Dictionary<string, object?>>(yaml);
  142. foreach (var (key, value) in props)
  143. if (key != "kind")
  144. map[key] = value;
  145. return map;
  146. }
  147. private class YamlRoot
  148. {
  149. public List<Resource>? Resources { get; set; }
  150. }
  151. }