For deleting one or many custom formats from a specific Sonarr or Radarr service.json-serializing-nullable-fields-issue
parent
a84c8a0efc
commit
f6465316d2
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="delete custom-formats" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="delete custom-formats radarr_develop --all --preview" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -1,6 +1,69 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
using Recyclarr.Cli.Processors;
|
||||||
|
using Recyclarr.Cli.Processors.Delete;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Console.Commands;
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
public class DeleteCustomFormatsCommand
|
[Description("Delete things from services like Radarr & Sonarr")]
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class DeleteCustomFormatsCommand : AsyncCommand<DeleteCustomFormatsCommand.CliSettings>
|
||||||
{
|
{
|
||||||
|
private readonly IDeleteCustomFormatsProcessor _processor;
|
||||||
|
private readonly ConsoleExceptionHandler _exceptionHandler;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
[SuppressMessage("Performance", "CA1819:Properties should not return arrays",
|
||||||
|
Justification = "Spectre.Console requires it")]
|
||||||
|
public class CliSettings : ServiceCommandSettings, IDeleteCustomFormatSettings
|
||||||
|
{
|
||||||
|
[CommandArgument(0, "<instance_name>")]
|
||||||
|
[Description("The name of the instance to delete CFs from.")]
|
||||||
|
public string InstanceName { get; init; } = "";
|
||||||
|
|
||||||
|
[CommandArgument(0, "[cf_names]")]
|
||||||
|
[Description("One or more custom format names to delete. Optional only if `--all` is used.")]
|
||||||
|
public string[] CustomFormatNamesOption { get; init; } = Array.Empty<string>();
|
||||||
|
public IReadOnlyCollection<string> CustomFormatNames => CustomFormatNamesOption;
|
||||||
|
|
||||||
|
[CommandOption("-a|--all")]
|
||||||
|
[Description("Delete ALL custom formats.")]
|
||||||
|
public bool All { get; init; } = false;
|
||||||
|
|
||||||
|
[CommandOption("-f|--force")]
|
||||||
|
[Description("Perform the delete operation with NO confirmation prompt.")]
|
||||||
|
public bool Force { get; init; } = false;
|
||||||
|
|
||||||
|
[CommandOption("-p|--preview")]
|
||||||
|
[Description("Preview what custom formats will be deleted without actually deleting them.")]
|
||||||
|
public bool Preview { get; init; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeleteCustomFormatsCommand(
|
||||||
|
IDeleteCustomFormatsProcessor processor,
|
||||||
|
ConsoleExceptionHandler exceptionHandler)
|
||||||
|
{
|
||||||
|
_processor = processor;
|
||||||
|
_exceptionHandler = exceptionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||||
|
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _processor.Process(settings);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await _exceptionHandler.HandleException(e);
|
||||||
|
return (int) ExitStatus.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) ExitStatus.Succeeded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace Recyclarr.Cli.Console.Settings;
|
||||||
|
|
||||||
|
public interface IDeleteCustomFormatSettings
|
||||||
|
{
|
||||||
|
public string InstanceName { get; }
|
||||||
|
public IReadOnlyCollection<string> CustomFormatNames { get; }
|
||||||
|
public bool All { get; }
|
||||||
|
public bool Force { get; }
|
||||||
|
bool Preview { get; }
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
using Flurl.Http;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.ErrorHandling;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Http;
|
||||||
|
using Recyclarr.TrashLib.Repo.VersionControl;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors;
|
||||||
|
|
||||||
|
public class ConsoleExceptionHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
|
||||||
|
public ConsoleExceptionHandler(ILogger log)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleException(Exception sourceException)
|
||||||
|
{
|
||||||
|
switch (sourceException)
|
||||||
|
{
|
||||||
|
case GitCmdException e:
|
||||||
|
_log.Error(e, "Non-zero exit code {ExitCode} while executing Git command: {Error}",
|
||||||
|
e.ExitCode, e.Error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FlurlHttpException e:
|
||||||
|
_log.Error("HTTP error: {Message}", e.SanitizedExceptionMessage());
|
||||||
|
foreach (var error in await GetValidationErrorsAsync(e))
|
||||||
|
{
|
||||||
|
_log.Error("Reason: {Error}", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NoConfigurationFilesException:
|
||||||
|
_log.Error("No configuration files found");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InvalidInstancesException e:
|
||||||
|
_log.Error("The following instances do not exist: {Names}", e.InstanceNames);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SplitInstancesException e:
|
||||||
|
_log.Error("The following configs share the same `base_url`, which isn't allowed: {Instances}",
|
||||||
|
e.InstanceNames);
|
||||||
|
_log.Error(
|
||||||
|
"Consolidate the config files manually to fix. " +
|
||||||
|
"See: https://recyclarr.dev/wiki/yaml/config-examples/#merge-single-instance");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InvalidConfigurationFilesException e:
|
||||||
|
_log.Error("Manually-specified configuration files do not exist: {Files}", e.InvalidFiles);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CommandException e:
|
||||||
|
_log.Error(e.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// This handles non-deterministic/unexpected exceptions.
|
||||||
|
default:
|
||||||
|
throw sourceException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IReadOnlyCollection<string>> GetValidationErrorsAsync(FlurlHttpException e)
|
||||||
|
{
|
||||||
|
var response = await e.GetResponseJsonAsync<List<dynamic>>();
|
||||||
|
if (response is null)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
.Select(x => (string) x.errorMessage)
|
||||||
|
.NotNull(x => !string.IsNullOrEmpty(x))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
using Recyclarr.Cli.Pipelines.CustomFormat.Api;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Models;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors.Delete;
|
||||||
|
|
||||||
|
public class DeleteCustomFormatsProcessor : IDeleteCustomFormatsProcessor
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly IAnsiConsole _console;
|
||||||
|
private readonly ICustomFormatService _api;
|
||||||
|
private readonly IConfigurationRegistry _configRegistry;
|
||||||
|
|
||||||
|
public DeleteCustomFormatsProcessor(
|
||||||
|
ILogger log,
|
||||||
|
IAnsiConsole console,
|
||||||
|
ICustomFormatService api,
|
||||||
|
IConfigurationRegistry configRegistry)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
_console = console;
|
||||||
|
_api = api;
|
||||||
|
_configRegistry = configRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Process(IDeleteCustomFormatSettings settings)
|
||||||
|
{
|
||||||
|
var config = GetTargetConfig(settings);
|
||||||
|
var cfs = await ObtainCustomFormats(config);
|
||||||
|
|
||||||
|
if (!settings.All)
|
||||||
|
{
|
||||||
|
if (!settings.CustomFormatNames.Any())
|
||||||
|
{
|
||||||
|
throw new CommandException("Custom format names must be specified if the `--all` option is not used.");
|
||||||
|
}
|
||||||
|
|
||||||
|
cfs = ProcessManuallySpecifiedFormats(settings, cfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfs.Any())
|
||||||
|
{
|
||||||
|
_console.MarkupLine("[yellow]Done[/]: No custom formats found or specified to delete.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintPreview(cfs);
|
||||||
|
|
||||||
|
if (settings.Preview)
|
||||||
|
{
|
||||||
|
_console.MarkupLine("This is a preview! [u]No actual deletions will be performed.[/]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.Force &&
|
||||||
|
!_console.Confirm("\nAre you sure you want to [bold red]permanently delete[/] the above custom formats?"))
|
||||||
|
{
|
||||||
|
_console.WriteLine("Aborted!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DeleteCustomFormats(cfs, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||||
|
private async Task DeleteCustomFormats(ICollection<CustomFormatData> cfs, IServiceConfiguration config)
|
||||||
|
{
|
||||||
|
await _console.Progress().StartAsync(async ctx =>
|
||||||
|
{
|
||||||
|
var task = ctx.AddTask("Deleting Custom Formats").MaxValue(cfs.Count);
|
||||||
|
|
||||||
|
var options = new ParallelOptions {MaxDegreeOfParallelism = 8};
|
||||||
|
await Parallel.ForEachAsync(cfs, options, async (cf, token) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _api.DeleteCustomFormat(config, cf.Id, token);
|
||||||
|
_log.Debug("Deleted {Name}", cf.Name);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_log.Debug(e, "Failed to delete CF");
|
||||||
|
_console.WriteLine($"Failed to delete CF: {cf.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Increment(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IList<CustomFormatData>> ObtainCustomFormats(IServiceConfiguration config)
|
||||||
|
{
|
||||||
|
IList<CustomFormatData> cfs = new List<CustomFormatData>();
|
||||||
|
|
||||||
|
await _console.Status().StartAsync("Obtaining custom formats...", async _ =>
|
||||||
|
{
|
||||||
|
cfs = await _api.GetCustomFormats(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<CustomFormatData> ProcessManuallySpecifiedFormats(
|
||||||
|
IDeleteCustomFormatSettings settings,
|
||||||
|
IList<CustomFormatData> cfs)
|
||||||
|
{
|
||||||
|
ILookup<bool, (string Name, IEnumerable<CustomFormatData> Cfs)> result = settings.CustomFormatNames
|
||||||
|
.GroupJoin(cfs,
|
||||||
|
x => x,
|
||||||
|
x => x.Name,
|
||||||
|
(x, y) => (Name: x, Cf: y),
|
||||||
|
StringComparer.InvariantCultureIgnoreCase)
|
||||||
|
.ToLookup(x => x.Cf.Any());
|
||||||
|
|
||||||
|
// 'false' means there were no CFs matched to this CF name
|
||||||
|
if (result[false].Any())
|
||||||
|
{
|
||||||
|
var cfNames = result[false].Select(x => x.Name).ToList();
|
||||||
|
_log.Debug("Unmatched CFs: {Names}", cfNames);
|
||||||
|
foreach (var name in cfNames)
|
||||||
|
{
|
||||||
|
_console.MarkupLineInterpolated($"[yellow]Warning[/]: Unmatched CF Name: [teal]{name}[/]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'true' represents CFs that match names provided in user-input (if provided)
|
||||||
|
cfs = result[true].SelectMany(x => x.Cfs).ToList();
|
||||||
|
return cfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "CoVariantArrayConversion")]
|
||||||
|
private void PrintPreview(ICollection<CustomFormatData> cfs)
|
||||||
|
{
|
||||||
|
_console.MarkupLine("The following custom formats will be [bold red]DELETED[/]:");
|
||||||
|
_console.WriteLine();
|
||||||
|
|
||||||
|
var cfNames = cfs
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.Order(StringComparer.InvariantCultureIgnoreCase)
|
||||||
|
.Chunk(Math.Max(15, cfs.Count / 3)) // Minimum row size is 15 for the table
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var grid = new Grid().AddColumns(cfNames.Count);
|
||||||
|
|
||||||
|
foreach (var rowItems in cfNames.Transpose())
|
||||||
|
{
|
||||||
|
grid.AddRow(rowItems
|
||||||
|
.Select(x => Markup.FromInterpolated($"[bold white]{x}[/]"))
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
_console.Write(grid);
|
||||||
|
_console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IServiceConfiguration GetTargetConfig(IDeleteCustomFormatSettings settings)
|
||||||
|
{
|
||||||
|
var configs = _configRegistry.FindAndLoadConfigs(new ConfigFilterCriteria
|
||||||
|
{
|
||||||
|
Instances = new[] {settings.InstanceName}
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (configs.Count)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
throw new ArgumentException($"No configuration found with name: {settings.InstanceName}");
|
||||||
|
|
||||||
|
case > 1:
|
||||||
|
throw new ArgumentException($"More than one instance found with this name: {settings.InstanceName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs.Single();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors.Delete;
|
||||||
|
|
||||||
|
public interface IDeleteCustomFormatsProcessor
|
||||||
|
{
|
||||||
|
Task Process(IDeleteCustomFormatSettings settings);
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
using Recyclarr.Cli.Console.Settings;
|
|
||||||
using Recyclarr.Common.Extensions;
|
|
||||||
using Recyclarr.TrashLib.Config;
|
|
||||||
using Recyclarr.TrashLib.Config.Services;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Processors;
|
|
||||||
|
|
||||||
public static class ProcessorExtensions
|
|
||||||
{
|
|
||||||
public static IEnumerable<IServiceConfiguration> GetConfigsBasedOnSettings(
|
|
||||||
this IEnumerable<IServiceConfiguration> configs,
|
|
||||||
ISyncSettings settings)
|
|
||||||
{
|
|
||||||
// later, if we filter by "operation type" (e.g. release profiles, CFs, quality sizes) it's just another
|
|
||||||
// ".Where()" in the LINQ expression below.
|
|
||||||
return configs.GetConfigsOfType(settings.Service)
|
|
||||||
.Where(x => settings.Instances.IsEmpty() ||
|
|
||||||
settings.Instances!.Any(y => y.EqualsIgnoreCase(x.InstanceName)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<string> GetSplitInstances(this IEnumerable<IServiceConfiguration> configs)
|
|
||||||
{
|
|
||||||
return configs
|
|
||||||
.GroupBy(x => x.BaseUrl)
|
|
||||||
.Where(x => x.Count() > 1)
|
|
||||||
.SelectMany(x => x.Select(y => y.InstanceName));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<string> GetInvalidInstanceNames(
|
|
||||||
this ISyncSettings settings,
|
|
||||||
IEnumerable<IServiceConfiguration> configs)
|
|
||||||
{
|
|
||||||
if (settings.Instances is null)
|
|
||||||
{
|
|
||||||
return Array.Empty<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var configInstances = configs.Select(x => x.InstanceName).ToList();
|
|
||||||
return settings.Instances
|
|
||||||
.Where(x => !configInstances.Contains(x, StringComparer.InvariantCultureIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace Recyclarr.TrashLib.Config;
|
||||||
|
|
||||||
|
public record ConfigFilterCriteria
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<string>? ManualConfigFiles { get; init; }
|
||||||
|
public SupportedServices? Service { get; init; }
|
||||||
|
public IReadOnlyCollection<string>? Instances { get; init; }
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.ErrorHandling;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config;
|
||||||
|
|
||||||
|
public class ConfigurationRegistry : IConfigurationRegistry
|
||||||
|
{
|
||||||
|
private readonly IConfigurationLoader _loader;
|
||||||
|
private readonly IConfigurationFinder _finder;
|
||||||
|
private readonly IFileSystem _fs;
|
||||||
|
|
||||||
|
public ConfigurationRegistry(IConfigurationLoader loader, IConfigurationFinder finder, IFileSystem fs)
|
||||||
|
{
|
||||||
|
_loader = loader;
|
||||||
|
_finder = finder;
|
||||||
|
_fs = fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<IServiceConfiguration> FindAndLoadConfigs(ConfigFilterCriteria? filterCriteria = null)
|
||||||
|
{
|
||||||
|
filterCriteria ??= new ConfigFilterCriteria();
|
||||||
|
|
||||||
|
var manualConfigs = filterCriteria.ManualConfigFiles;
|
||||||
|
var configs = manualConfigs is not null && manualConfigs.Any()
|
||||||
|
? PrepareManualConfigs(manualConfigs)
|
||||||
|
: _finder.GetConfigFiles();
|
||||||
|
|
||||||
|
return LoadAndFilterConfigs(configs, filterCriteria).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyCollection<IFileInfo> PrepareManualConfigs(IEnumerable<string> manualConfigs)
|
||||||
|
{
|
||||||
|
var configFiles = manualConfigs
|
||||||
|
.Select(x => _fs.FileInfo.New(x))
|
||||||
|
.ToLookup(x => x.Exists);
|
||||||
|
|
||||||
|
if (configFiles[false].Any())
|
||||||
|
{
|
||||||
|
throw new InvalidConfigurationFilesException(configFiles[false].ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return configFiles[true].ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<IServiceConfiguration> LoadAndFilterConfigs(
|
||||||
|
IEnumerable<IFileInfo> configs,
|
||||||
|
ConfigFilterCriteria filterCriteria)
|
||||||
|
{
|
||||||
|
var loadedConfigs = configs.SelectMany(x => _loader.Load(x)).ToList();
|
||||||
|
|
||||||
|
var invalidInstances = loadedConfigs.GetInvalidInstanceNames(filterCriteria).ToList();
|
||||||
|
if (invalidInstances.Any())
|
||||||
|
{
|
||||||
|
throw new InvalidInstancesException(invalidInstances);
|
||||||
|
}
|
||||||
|
|
||||||
|
var splitInstances = loadedConfigs.GetSplitInstances().ToList();
|
||||||
|
if (splitInstances.Any())
|
||||||
|
{
|
||||||
|
throw new SplitInstancesException(splitInstances);
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedConfigs.GetConfigsBasedOnSettings(filterCriteria);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config;
|
||||||
|
|
||||||
|
public interface IConfigurationRegistry
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<IServiceConfiguration> FindAndLoadConfigs(ConfigFilterCriteria? filterCriteria = null);
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing.ErrorHandling;
|
||||||
|
|
||||||
|
public class InvalidConfigurationFilesException : Exception
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<IFileInfo> InvalidFiles { get; }
|
||||||
|
|
||||||
|
public InvalidConfigurationFilesException(IReadOnlyCollection<IFileInfo> invalidFiles)
|
||||||
|
{
|
||||||
|
InvalidFiles = invalidFiles;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
|
||||||
|
public class CommandException : Exception
|
||||||
|
{
|
||||||
|
public CommandException(string? message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing.ErrorHandling;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.TestLibrary;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class ConfigurationRegistryTest : TrashLibIntegrationFixture
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Use_explicit_paths_instead_of_default()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigurationRegistry>();
|
||||||
|
|
||||||
|
Fs.AddFile("manual.yml", new MockFileData(
|
||||||
|
"""
|
||||||
|
radarr:
|
||||||
|
instance1:
|
||||||
|
base_url: http://localhost:7878
|
||||||
|
api_key: asdf
|
||||||
|
"""));
|
||||||
|
|
||||||
|
var result = sut.FindAndLoadConfigs(new ConfigFilterCriteria
|
||||||
|
{
|
||||||
|
ManualConfigFiles = new[] {"manual.yml"}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(new[]
|
||||||
|
{
|
||||||
|
new RadarrConfiguration
|
||||||
|
{
|
||||||
|
BaseUrl = new Uri("http://localhost:7878"),
|
||||||
|
ApiKey = "asdf",
|
||||||
|
InstanceName = "instance1"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_on_invalid_config_files()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigurationRegistry>();
|
||||||
|
|
||||||
|
var act = () => sut.FindAndLoadConfigs(new ConfigFilterCriteria
|
||||||
|
{
|
||||||
|
ManualConfigFiles = new[] {"manual.yml"}
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().ThrowExactly<InvalidConfigurationFilesException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_on_invalid_instances()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigurationRegistry>();
|
||||||
|
|
||||||
|
Fs.AddFile("manual.yml", new MockFileData(
|
||||||
|
"""
|
||||||
|
radarr:
|
||||||
|
instance1:
|
||||||
|
base_url: http://localhost:7878
|
||||||
|
api_key: asdf
|
||||||
|
"""));
|
||||||
|
|
||||||
|
var act = () => sut.FindAndLoadConfigs(new ConfigFilterCriteria
|
||||||
|
{
|
||||||
|
ManualConfigFiles = new[] {"manual.yml"},
|
||||||
|
Instances = new[] {"instance1", "instance2"}
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().ThrowExactly<InvalidInstancesException>()
|
||||||
|
.Which.InstanceNames.Should().BeEquivalentTo("instance2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_on_split_instances()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigurationRegistry>();
|
||||||
|
|
||||||
|
Fs.AddFile("manual.yml", new MockFileData(
|
||||||
|
"""
|
||||||
|
radarr:
|
||||||
|
instance1:
|
||||||
|
base_url: http://localhost:7878
|
||||||
|
api_key: asdf
|
||||||
|
instance2:
|
||||||
|
base_url: http://localhost:7878
|
||||||
|
api_key: asdf
|
||||||
|
"""));
|
||||||
|
|
||||||
|
var act = () => sut.FindAndLoadConfigs(new ConfigFilterCriteria
|
||||||
|
{
|
||||||
|
ManualConfigFiles = new[] {"manual.yml"}
|
||||||
|
});
|
||||||
|
|
||||||
|
act.Should().ThrowExactly<SplitInstancesException>()
|
||||||
|
.Which.InstanceNames.Should().BeEquivalentTo("instance1", "instance2");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue