YamlResourceCollection.cs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. using System.Collections.Specialized;
  2. using RackPeek.Domain.Resources;
  3. using RackPeek.Domain.Resources.Models;
  4. using RackPeek.Domain.Resources.Services;
  5. using RackPeek.Domain.Resources.SystemResources;
  6. using YamlDotNet.Core;
  7. using YamlDotNet.Serialization;
  8. using YamlDotNet.Serialization.NamingConventions;
  9. namespace RackPeek.Domain.Persistence.Yaml;
  10. public class ResourceCollection
  11. {
  12. public List<Resource> Resources { get; } = new();
  13. public readonly SemaphoreSlim FileLock = new(1, 1);
  14. }
  15. public sealed class YamlResourceCollection(
  16. string filePath,
  17. ITextFileStore fileStore,
  18. ResourceCollection resourceCollection)
  19. : IResourceCollection
  20. {
  21. public Task<bool> Exists(string name)
  22. {
  23. return Task.FromResult(resourceCollection.Resources.Exists(r =>
  24. r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
  25. }
  26. public Task<Dictionary<string, int>> GetTagsAsync()
  27. {
  28. var result = resourceCollection.Resources
  29. .Where(r => r.Tags != null)
  30. .SelectMany(r => r.Tags!) // flatten all tag arrays
  31. .Where(t => !string.IsNullOrWhiteSpace(t))
  32. .GroupBy(t => t)
  33. .ToDictionary(g => g.Key, g => g.Count());
  34. return Task.FromResult(result);
  35. }
  36. public Task<IReadOnlyList<T>> GetAllOfTypeAsync<T>()
  37. {
  38. return Task.FromResult<IReadOnlyList<T>>(resourceCollection.Resources.OfType<T>().ToList());
  39. }
  40. public Task<IReadOnlyList<Resource>> GetDependantsAsync(string name)
  41. {
  42. return Task.FromResult<IReadOnlyList<Resource>>(resourceCollection.Resources.Where(r => r.RunsOn?.Equals(name, StringComparison.OrdinalIgnoreCase) ?? false).ToList());
  43. }
  44. public Task<IReadOnlyList<Resource>> GetByTagAsync(string name)
  45. {
  46. return Task.FromResult<IReadOnlyList<Resource>>(resourceCollection.Resources.Where(r => r.Tags.Contains(name)).ToList());
  47. }
  48. public async Task LoadAsync()
  49. {
  50. var loaded = await LoadFromFileAsync();
  51. try
  52. {
  53. resourceCollection.Resources.Clear();
  54. }
  55. catch
  56. {
  57. // ignore
  58. }
  59. resourceCollection.Resources.AddRange(loaded);
  60. }
  61. public IReadOnlyList<Hardware> HardwareResources =>
  62. resourceCollection.Resources.OfType<Hardware>().ToList();
  63. public IReadOnlyList<SystemResource> SystemResources =>
  64. resourceCollection.Resources.OfType<SystemResource>().ToList();
  65. public IReadOnlyList<Service> ServiceResources =>
  66. resourceCollection.Resources.OfType<Service>().ToList();
  67. public Task<Resource?> GetByNameAsync(string name)
  68. {
  69. return Task.FromResult(resourceCollection.Resources.FirstOrDefault(r =>
  70. r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
  71. }
  72. public Task<T?> GetByNameAsync<T>(string name) where T : Resource
  73. {
  74. var resource = resourceCollection.Resources.FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
  75. return Task.FromResult<T?>(resource as T);
  76. }
  77. public Resource? GetByName(string name) =>
  78. resourceCollection.Resources.FirstOrDefault(r =>
  79. r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
  80. public Task AddAsync(Resource resource) =>
  81. UpdateWithLockAsync(list =>
  82. {
  83. if (list.Any(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase)))
  84. throw new InvalidOperationException($"'{resource.Name}' already exists.");
  85. resource.Kind = GetKind(resource);
  86. list.Add(resource);
  87. });
  88. public Task UpdateAsync(Resource resource) =>
  89. UpdateWithLockAsync(list =>
  90. {
  91. var index = list.FindIndex(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
  92. if (index == -1) throw new InvalidOperationException("Not found.");
  93. resource.Kind = GetKind(resource);
  94. list[index] = resource;
  95. });
  96. public Task DeleteAsync(string name) =>
  97. UpdateWithLockAsync(list =>
  98. list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
  99. private async Task UpdateWithLockAsync(Action<List<Resource>> action)
  100. {
  101. await resourceCollection.FileLock.WaitAsync();
  102. try
  103. {
  104. action(resourceCollection.Resources);
  105. var serializer = new SerializerBuilder()
  106. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  107. .WithTypeConverter(new StorageSizeYamlConverter())
  108. .WithTypeConverter(new NotesStringYamlConverter())
  109. .ConfigureDefaultValuesHandling(
  110. DefaultValuesHandling.OmitNull |
  111. DefaultValuesHandling.OmitEmptyCollections
  112. )
  113. .Build();
  114. var payload = new OrderedDictionary
  115. {
  116. ["resources"] = resourceCollection.Resources.Select(SerializeResource).ToList()
  117. };
  118. await fileStore.WriteAllTextAsync(
  119. filePath,
  120. serializer.Serialize(payload));
  121. }
  122. finally
  123. {
  124. resourceCollection.FileLock.Release();
  125. }
  126. }
  127. private async Task<List<Resource>> LoadFromFileAsync()
  128. {
  129. var yaml = await fileStore.ReadAllTextAsync(filePath);
  130. if (string.IsNullOrWhiteSpace(yaml))
  131. return new();
  132. var deserializer = new DeserializerBuilder()
  133. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  134. .WithCaseInsensitivePropertyMatching()
  135. .WithTypeConverter(new StorageSizeYamlConverter())
  136. .WithTypeConverter(new NotesStringYamlConverter())
  137. .WithTypeDiscriminatingNodeDeserializer(options =>
  138. {
  139. options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
  140. {
  141. { Server.KindLabel, typeof(Server) },
  142. { Switch.KindLabel, typeof(Switch) },
  143. { Firewall.KindLabel, typeof(Firewall) },
  144. { Router.KindLabel, typeof(Router) },
  145. { Desktop.KindLabel, typeof(Desktop) },
  146. { Laptop.KindLabel, typeof(Laptop) },
  147. { AccessPoint.KindLabel, typeof(AccessPoint) },
  148. { Ups.KindLabel, typeof(Ups) },
  149. { SystemResource.KindLabel, typeof(SystemResource) },
  150. { Service.KindLabel, typeof(Service) }
  151. });
  152. })
  153. .Build();
  154. try
  155. {
  156. var root = deserializer.Deserialize<YamlRoot>(yaml);
  157. return root?.Resources ?? new();
  158. }
  159. catch (YamlException)
  160. {
  161. return new();
  162. }
  163. }
  164. private string GetKind(Resource resource) => resource switch
  165. {
  166. Server => "Server",
  167. Switch => "Switch",
  168. Firewall => "Firewall",
  169. Router => "Router",
  170. Desktop => "Desktop",
  171. Laptop => "Laptop",
  172. AccessPoint => "AccessPoint",
  173. Ups => "Ups",
  174. SystemResource => "System",
  175. Service => "Service",
  176. _ => throw new InvalidOperationException($"Unknown resource type: {resource.GetType().Name}")
  177. };
  178. private OrderedDictionary SerializeResource(Resource resource)
  179. {
  180. var map = new OrderedDictionary
  181. {
  182. ["kind"] = GetKind(resource)
  183. };
  184. var serializer = new SerializerBuilder()
  185. .WithNamingConvention(CamelCaseNamingConvention.Instance)
  186. .WithTypeConverter(new NotesStringYamlConverter())
  187. .ConfigureDefaultValuesHandling(
  188. DefaultValuesHandling.OmitNull |
  189. DefaultValuesHandling.OmitEmptyCollections
  190. )
  191. .Build();
  192. var yaml = serializer.Serialize(resource);
  193. var props = new DeserializerBuilder()
  194. .Build()
  195. .Deserialize<Dictionary<string, object?>>(yaml);
  196. foreach (var (key, value) in props)
  197. if (key != "kind")
  198. map[key] = value;
  199. return map;
  200. }
  201. }
  202. public class YamlRoot
  203. {
  204. public List<Resource>? Resources { get; set; }
  205. }