You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
recyclarr/src/Recyclarr.Cli/Config/ConfigurationLoader.cs

162 lines
4.9 KiB

using System.IO.Abstractions;
using Recyclarr.Cli.Logging;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Config.Yaml;
using Serilog;
using Serilog.Context;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
namespace Recyclarr.Cli.Config;
public class ConfigurationLoader<T> : IConfigurationLoader<T>
where T : ServiceConfiguration
{
private readonly ILogger _log;
private readonly IDeserializer _deserializer;
private readonly IFileSystem _fs;
public ConfigurationLoader(
ILogger log,
IFileSystem fs,
IYamlSerializerFactory yamlFactory)
{
_log = log;
_fs = fs;
_deserializer = yamlFactory.CreateDeserializer();
}
public ICollection<T> LoadMany(IEnumerable<string> configFiles, string configSection)
{
return configFiles.SelectMany(file => Load(file, configSection)).ToList();
}
public ICollection<T> Load(string file, string configSection)
{
_log.Debug("Loading config file: {File}", file);
using var logScope = LogContext.PushProperty(LogProperty.Scope, _fs.Path.GetFileName(file));
try
{
using var stream = _fs.File.OpenText(file);
return LoadFromStream(stream, configSection);
}
catch (EmptyYamlException)
{
_log.Warning("Configuration file yielded no usable configuration (is it empty?)");
return Array.Empty<T>();
}
catch (YamlException e)
{
var line = e.Start.Line;
switch (e.InnerException)
{
case InvalidCastException:
_log.Error("Incompatible value assigned/used at line {Line}: {Msg}", line,
e.InnerException.Message);
break;
default:
_log.Error("Exception at line {Line}: {Msg}", line, e.InnerException?.Message ?? e.Message);
break;
}
}
_log.Error("Due to previous exception, this file will be skipped: {File}", file);
return Array.Empty<T>();
}
public ICollection<T> LoadFromStream(TextReader stream, string requestedSection)
{
_log.Debug("Loading config section: {Section}", requestedSection);
var parser = new Parser(stream);
parser.Consume<StreamStart>();
if (parser.Current is StreamEnd)
{
_log.Debug("Skipping this config due to StreamEnd");
throw new EmptyYamlException();
}
parser.Consume<DocumentStart>();
if (parser.Current is DocumentEnd)
{
_log.Debug("Skipping this config due to DocumentEnd");
throw new EmptyYamlException();
}
return ParseAllSections(parser, requestedSection);
}
private ICollection<T> ParseAllSections(Parser parser, string requestedSection)
{
var configs = new List<T>();
parser.Consume<MappingStart>();
while (parser.TryConsume<Scalar>(out var section))
{
if (section.Value == requestedSection)
{
configs.AddRange(ParseSingleSection(parser));
}
else
{
_log.Debug("Skipping non-matching config section {Section} at line {Line}",
section.Value, section.Start.Line);
parser.SkipThisAndNestedEvents();
}
}
// If any config names are null, that means user specified array-style (deprecated) instances.
if (configs.Any(x => x.Name is null))
{
_log.Warning(
"Found array-style list of instances instead of named-style. " +
"Array-style lists of Sonarr/Radarr instances are deprecated");
}
return configs;
}
private ICollection<T> ParseSingleSection(Parser parser)
{
var configs = new List<T>();
switch (parser.Current)
{
case MappingStart:
ParseAndAdd<MappingStart, MappingEnd>(parser, configs);
break;
case SequenceStart:
ParseAndAdd<SequenceStart, SequenceEnd>(parser, configs);
break;
}
return configs;
}
private void ParseAndAdd<TStart, TEnd>(Parser parser, ICollection<T> configs)
where TStart : ParsingEvent
where TEnd : ParsingEvent
{
parser.Consume<TStart>();
while (!parser.TryConsume<TEnd>(out _))
{
var lineNumber = parser.Current?.Start.Line;
string? instanceName = null;
if (parser.TryConsume<Scalar>(out var key))
{
instanceName = key.Value;
}
var newConfig = _deserializer.Deserialize<T>(parser);
newConfig.Name = instanceName;
newConfig.LineNumber = lineNumber ?? 0;
configs.Add(newConfig);
}
}
}