YamlResourceCollection.cs 8.6 KB

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