| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- using System.Collections.Specialized;
- using RackPeek.Domain.Persistence;
- using RackPeek.Domain.Persistence.Yaml;
- using RackPeek.Domain.Resources;
- using RackPeek.Domain.Resources.Models;
- using RackPeek.Domain.Resources.Services;
- using RackPeek.Domain.Resources.SystemResources;
- using RackPeek.Yaml;
- using YamlDotNet.Core;
- using YamlDotNet.Serialization;
- using YamlDotNet.Serialization.NamingConventions;
- public class ResourceCollection
- {
- public List<Resource> Resources { get; } = new();
- public readonly SemaphoreSlim FileLock = new(1, 1);
- }
- public sealed class YamlResourceCollection(
- string filePath,
- ITextFileStore fileStore,
- ResourceCollection resourceCollection)
- : IResourceCollection
- {
- public Task<bool> Exists(string name)
- {
- return Task.FromResult(resourceCollection.Resources.Exists(r =>
- r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
- }
-
- public Task<Dictionary<string, int>> GetTagsAsync()
- {
- var result = resourceCollection.Resources
- .Where(r => r.Tags != null)
- .SelectMany(r => r.Tags!) // flatten all tag arrays
- .Where(t => !string.IsNullOrWhiteSpace(t))
- .GroupBy(t => t)
- .ToDictionary(g => g.Key, g => g.Count());
- return Task.FromResult(result);
- }
- public Task<IReadOnlyList<Resource>> GetByTagAsync(string name)
- {
- return Task.FromResult<IReadOnlyList<Resource>>(resourceCollection.Resources.Where(r => r.Tags.Contains(name)).ToList());
- }
-
- public async Task LoadAsync()
- {
- var loaded = await LoadFromFileAsync();
- try
- {
- resourceCollection.Resources.Clear();
- }
- catch
- {
- // ignore
- }
- resourceCollection.Resources.AddRange(loaded);
- }
- public IReadOnlyList<Hardware> HardwareResources =>
- resourceCollection.Resources.OfType<Hardware>().ToList();
- public IReadOnlyList<SystemResource> SystemResources =>
- resourceCollection.Resources.OfType<SystemResource>().ToList();
- public IReadOnlyList<Service> ServiceResources =>
- resourceCollection.Resources.OfType<Service>().ToList();
- public Resource? GetByName(string name) =>
- resourceCollection.Resources.FirstOrDefault(r =>
- r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
- public Task AddAsync(Resource resource) =>
- UpdateWithLockAsync(list =>
- {
- if (list.Any(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase)))
- throw new InvalidOperationException($"'{resource.Name}' already exists.");
- resource.Kind = GetKind(resource);
- list.Add(resource);
- });
- public Task UpdateAsync(Resource resource) =>
- UpdateWithLockAsync(list =>
- {
- var index = list.FindIndex(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
- if (index == -1) throw new InvalidOperationException("Not found.");
- resource.Kind = GetKind(resource);
- list[index] = resource;
- });
- public Task DeleteAsync(string name) =>
- UpdateWithLockAsync(list =>
- list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
- private async Task UpdateWithLockAsync(Action<List<Resource>> action)
- {
- await resourceCollection.FileLock.WaitAsync();
- try
- {
- action(resourceCollection.Resources);
- var serializer = new SerializerBuilder()
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
- .WithTypeConverter(new StorageSizeYamlConverter())
- .WithTypeConverter(new NotesStringYamlConverter())
- .ConfigureDefaultValuesHandling(
- DefaultValuesHandling.OmitNull |
- DefaultValuesHandling.OmitEmptyCollections
- )
- .Build();
- var payload = new OrderedDictionary
- {
- ["resources"] = resourceCollection.Resources.Select(SerializeResource).ToList()
- };
- await fileStore.WriteAllTextAsync(
- filePath,
- serializer.Serialize(payload));
- }
- finally
- {
- resourceCollection.FileLock.Release();
- }
- }
- private async Task<List<Resource>> LoadFromFileAsync()
- {
- var yaml = await fileStore.ReadAllTextAsync(filePath);
- if (string.IsNullOrWhiteSpace(yaml))
- return new();
- var deserializer = new DeserializerBuilder()
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
- .WithCaseInsensitivePropertyMatching()
- .WithTypeConverter(new StorageSizeYamlConverter())
- .WithTypeConverter(new NotesStringYamlConverter())
-
- .WithTypeDiscriminatingNodeDeserializer(options =>
- {
- options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
- {
- { Server.KindLabel, typeof(Server) },
- { Switch.KindLabel, typeof(Switch) },
- { Firewall.KindLabel, typeof(Firewall) },
- { Router.KindLabel, typeof(Router) },
- { Desktop.KindLabel, typeof(Desktop) },
- { Laptop.KindLabel, typeof(Laptop) },
- { AccessPoint.KindLabel, typeof(AccessPoint) },
- { Ups.KindLabel, typeof(Ups) },
- { SystemResource.KindLabel, typeof(SystemResource) },
- { Service.KindLabel, typeof(Service) }
- });
- })
- .Build();
- try
- {
- var root = deserializer.Deserialize<YamlRoot>(yaml);
- return root?.Resources ?? new();
- }
- catch (YamlException)
- {
- return new();
- }
- }
- private string GetKind(Resource resource) => resource switch
- {
- Server => "Server",
- Switch => "Switch",
- Firewall => "Firewall",
- Router => "Router",
- Desktop => "Desktop",
- Laptop => "Laptop",
- AccessPoint => "AccessPoint",
- Ups => "Ups",
- SystemResource => "System",
- Service => "Service",
- _ => throw new InvalidOperationException($"Unknown resource type: {resource.GetType().Name}")
- };
- private OrderedDictionary SerializeResource(Resource resource)
- {
- var map = new OrderedDictionary
- {
- ["kind"] = GetKind(resource)
- };
- var serializer = new SerializerBuilder()
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
- .WithTypeConverter(new NotesStringYamlConverter())
- .ConfigureDefaultValuesHandling(
- DefaultValuesHandling.OmitNull |
- DefaultValuesHandling.OmitEmptyCollections
- )
- .Build();
- var yaml = serializer.Serialize(resource);
- var props = new DeserializerBuilder()
- .Build()
- .Deserialize<Dictionary<string, object?>>(yaml);
- foreach (var (key, value) in props)
- if (key != "kind")
- map[key] = value;
- return map;
- }
- }
- public class YamlRoot
- {
- public List<Resource>? Resources { get; set; }
- }
|