Add Multi Instance Support , Serilog , little Hotfixes
This commit is contained in:
69
UmlautAdaptarr/Utilities/ApiKeyMaskingEnricher.cs
Normal file
69
UmlautAdaptarr/Utilities/ApiKeyMaskingEnricher.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Collections;
|
||||
using System.Text.RegularExpressions;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace UmlautAdaptarr.Utilities;
|
||||
|
||||
public class ApiKeyMaskingEnricher : ILogEventEnricher
|
||||
{
|
||||
private readonly List<string> apiKeys = new();
|
||||
|
||||
public ApiKeyMaskingEnricher(string appsetting)
|
||||
{
|
||||
ExtractApiKeysFromAppSettings(appsetting);
|
||||
ExtractApiKeysFromEnvironmentVariables();
|
||||
apiKeys = new List<string>(apiKeys.Distinct());
|
||||
}
|
||||
|
||||
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||
{
|
||||
//if (logEvent.Properties.TryGetValue("apikey", out var value) && value is ScalarValue scalarValue)
|
||||
//{
|
||||
var maskedValue = new ScalarValue("**Hidden Api Key**");
|
||||
foreach (var apikey in apiKeys) logEvent.AddOrUpdateProperty(new LogEventProperty(apikey, maskedValue));
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Scan all Env Variabels for known Apikeys
|
||||
/// </summary>
|
||||
/// <returns>List of all Apikeys</returns>
|
||||
public List<string> ExtractApiKeysFromEnvironmentVariables()
|
||||
{
|
||||
var envVariables = Environment.GetEnvironmentVariables();
|
||||
|
||||
foreach (DictionaryEntry envVariable in envVariables)
|
||||
if (envVariable.Key.ToString()!.Contains("ApiKey"))
|
||||
apiKeys.Add(envVariable.Value.ToString());
|
||||
|
||||
return apiKeys;
|
||||
}
|
||||
|
||||
|
||||
public List<string> ExtractApiKeysFromAppSettings(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var fileContent = File.ReadAllText(filePath);
|
||||
|
||||
var pattern = "\"ApiKey\": \"(.*?)\"";
|
||||
var regex = new Regex(pattern);
|
||||
var matches = regex.Matches(fileContent);
|
||||
|
||||
foreach (Match match in matches) apiKeys.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
return apiKeys;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
UmlautAdaptarr/Utilities/Helper.cs
Normal file
10
UmlautAdaptarr/Utilities/Helper.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace UmlautAdaptarr.Utilities
|
||||
{
|
||||
public static class Helper
|
||||
{
|
||||
public static void ShowLogo()
|
||||
{
|
||||
Console.WriteLine("\r\n _ _ _ _ ___ _ _ \r\n| | | | | | | | / _ \\ | | | | \r\n| | | |_ __ ___ | | __ _ _ _| |_/ /_\\ \\ __| | __ _ _ __ | |_ __ _ _ __ _ __ \r\n| | | | '_ ` _ \\| |/ _` | | | | __| _ |/ _` |/ _` | '_ \\| __/ _` | '__| '__|\r\n| |_| | | | | | | | (_| | |_| | |_| | | | (_| | (_| | |_) | || (_| | | | | \r\n \\___/|_| |_| |_|_|\\__,_|\\__,_|\\__\\_| |_/\\__,_|\\__,_| .__/ \\__\\__,_|_| |_| \r\n | | \r\n |_| \r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
74
UmlautAdaptarr/Utilities/KeyedServiceExtensions.cs
Normal file
74
UmlautAdaptarr/Utilities/KeyedServiceExtensions.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace UmlautAdaptarr.Utilities
|
||||
{
|
||||
// License: This code is published under the MIT license.
|
||||
// Source: https://stackoverflow.com/questions/77559201/
|
||||
public static class KeyedServiceExtensions
|
||||
{
|
||||
public static void AllowResolvingKeyedServicesAsDictionary(
|
||||
this IServiceCollection sc)
|
||||
{
|
||||
// KeyedServiceCache caches all the keys of a given type for a
|
||||
// specific service type. By making it a singleton we only have
|
||||
// determine the keys once, which makes resolving the dict very fast.
|
||||
sc.AddSingleton(typeof(KeyedServiceCache<,>));
|
||||
|
||||
// KeyedServiceCache depends on the IServiceCollection to get
|
||||
// the list of keys. That's why we register that here as well, as it
|
||||
// is not registered by default in MS.DI.
|
||||
sc.AddSingleton(sc);
|
||||
|
||||
// Last we make the registration for the dictionary itself, which maps
|
||||
// to our custom type below. This registration must be transient, as
|
||||
// the containing services could have any lifetime and this registration
|
||||
// should by itself not cause Captive Dependencies.
|
||||
sc.AddTransient(typeof(IDictionary<,>), typeof(KeyedServiceDictionary<,>));
|
||||
|
||||
// For completeness, let's also allow IReadOnlyDictionary to be resolved.
|
||||
sc.AddTransient(
|
||||
typeof(IReadOnlyDictionary<,>), typeof(KeyedServiceDictionary<,>));
|
||||
}
|
||||
|
||||
// We inherit from ReadOnlyDictionary, to disallow consumers from changing
|
||||
// the wrapped dependencies while reusing all its functionality. This way
|
||||
// we don't have to implement IDictionary<T,V> ourselves; too much work.
|
||||
private sealed class KeyedServiceDictionary<TKey, TService>(
|
||||
KeyedServiceCache<TKey, TService> keys, IServiceProvider provider)
|
||||
: ReadOnlyDictionary<TKey, TService>(Create(keys, provider))
|
||||
where TKey : notnull
|
||||
where TService : notnull
|
||||
{
|
||||
private static Dictionary<TKey, TService> Create(
|
||||
KeyedServiceCache<TKey, TService> keys, IServiceProvider provider)
|
||||
{
|
||||
var dict = new Dictionary<TKey, TService>(capacity: keys.Keys.Length);
|
||||
|
||||
foreach (TKey key in keys.Keys)
|
||||
{
|
||||
dict[key] = provider.GetRequiredKeyedService<TService>(key);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class KeyedServiceCache<TKey, TService>(IServiceCollection sc)
|
||||
where TKey : notnull
|
||||
where TService : notnull
|
||||
{
|
||||
// Once this class is resolved, all registrations are guaranteed to be
|
||||
// made, so we can, at that point, safely iterate the collection to get
|
||||
// the keys for the service type.
|
||||
public TKey[] Keys { get; } = (
|
||||
from service in sc
|
||||
where service.ServiceKey != null
|
||||
where service.ServiceKey!.GetType() == typeof(TKey)
|
||||
where service.ServiceType == typeof(TService)
|
||||
select (TKey)service.ServiceKey!)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,92 +1,145 @@
|
||||
using UmlautAdaptarr.Options;
|
||||
using UmlautAdaptarr.Options.ArrOptions;
|
||||
using UmlautAdaptarr.Interfaces;
|
||||
using UmlautAdaptarr.Options;
|
||||
using UmlautAdaptarr.Options.ArrOptions.InstanceOptions;
|
||||
using UmlautAdaptarr.Providers;
|
||||
using UmlautAdaptarr.Services;
|
||||
|
||||
namespace UmlautAdaptarr.Utilities
|
||||
namespace UmlautAdaptarr.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring services related to ARR Applications
|
||||
/// </summary>
|
||||
public static class ServicesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for configuring services related to ARR Applications
|
||||
/// Adds a service with specified options and service to the service collection.
|
||||
/// </summary>
|
||||
public static class ServicesExtensions
|
||||
/// <typeparam name="TOptions">The options type for the service.</typeparam>
|
||||
/// <typeparam name="TService">The service type for the service.</typeparam>
|
||||
/// <typeparam name="TInterface">The Interface of the service type</typeparam>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
|
||||
/// <param name="sectionName">The name of the configuration section containing service options.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
|
||||
private static WebApplicationBuilder AddServicesWithOptions<TOptions, TService, TInterface>(
|
||||
this WebApplicationBuilder builder, string sectionName)
|
||||
where TOptions : class, new()
|
||||
where TService : class, TInterface
|
||||
where TInterface : class
|
||||
{
|
||||
if (builder.Services == null) throw new ArgumentNullException(nameof(builder), "Service collection is null.");
|
||||
|
||||
/// <summary>
|
||||
/// Adds a service with specified options and service to the service collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions">The options type for the service.</typeparam>
|
||||
/// <typeparam name="TService">The service type for the service.</typeparam>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
|
||||
/// <param name="sectionName">The name of the configuration section containing service options.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
|
||||
private static WebApplicationBuilder AddServiceWithOptions<TOptions, TService>(this WebApplicationBuilder builder, string sectionName)
|
||||
where TOptions : class
|
||||
where TService : class
|
||||
|
||||
var singleInstance = builder.Configuration.GetSection(sectionName).Get<TOptions>();
|
||||
|
||||
var singleHost = (string?)typeof(TOptions).GetProperty("Host")?.GetValue(singleInstance, null);
|
||||
|
||||
// If we have no Single Instance , we try to parse for a Array
|
||||
var optionsArray = singleHost == null
|
||||
? builder.Configuration.GetSection(sectionName).Get<TOptions[]>()
|
||||
:
|
||||
[
|
||||
singleInstance
|
||||
];
|
||||
|
||||
if (optionsArray == null || !optionsArray.Any())
|
||||
throw new InvalidOperationException(
|
||||
$"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable.");
|
||||
|
||||
foreach (var options in optionsArray)
|
||||
{
|
||||
if (builder.Services == null)
|
||||
var instanceState = (bool)(typeof(TOptions).GetProperty("Enabled")?.GetValue(options, null) ?? false);
|
||||
|
||||
// We only want to create instances that are enabled in the Configs
|
||||
if (instanceState)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder), "Service collection is null.");
|
||||
// User can give the Instance a readable Name otherwise we use the Host Property
|
||||
var instanceName = (string)(typeof(TOptions).GetProperty("Name")?.GetValue(options, null) ??
|
||||
(string)typeof(TOptions).GetProperty("Host")?.GetValue(options, null)!);
|
||||
instanceName = instanceName.Replace(".", "");
|
||||
builder.Services.Configure(instanceName,
|
||||
delegate(TOptions serviceOptions) { serviceOptions = options; });
|
||||
|
||||
builder.Services.AllowResolvingKeyedServicesAsDictionary();
|
||||
builder.Services.AddKeyedSingleton<TInterface, TService>(instanceName);
|
||||
}
|
||||
|
||||
var options = builder.Configuration.GetSection(sectionName).Get<TOptions>();
|
||||
if (options == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable.");
|
||||
}
|
||||
|
||||
builder.Services.Configure<TOptions>(builder.Configuration.GetSection(sectionName));
|
||||
builder.Services.AddSingleton<TService>();
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for Sonarr with default options and client.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
|
||||
public static WebApplicationBuilder AddSonarrSupport(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServiceWithOptions<SonarrInstanceOptions, SonarrClient>("Sonarr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for Lidarr with default options and client.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
|
||||
public static WebApplicationBuilder AddLidarrSupport(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServiceWithOptions<LidarrInstanceOptions, LidarrClient>("Lidarr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for Readarr with default options and client.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
|
||||
public static WebApplicationBuilder AddReadarrSupport(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServiceWithOptions<ReadarrInstanceOptions, ReadarrClient>("Readarr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a title lookup service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
|
||||
public static WebApplicationBuilder AddTitleLookupService(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServiceWithOptions<GlobalOptions, TitleApiService>("Settings");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a proxy request service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
|
||||
public static WebApplicationBuilder AddProxyRequestService(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServiceWithOptions<GlobalOptions, ProxyRequestService>("Settings");
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a service with specified options and service to the service collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions">The options type for the service.</typeparam>
|
||||
/// <typeparam name="TService">The service type for the service.</typeparam>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
|
||||
/// <param name="sectionName">The name of the configuration section containing service options.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
|
||||
private static WebApplicationBuilder AddServiceWithOptions<TOptions, TService>(this WebApplicationBuilder builder,
|
||||
string sectionName)
|
||||
where TOptions : class
|
||||
where TService : class
|
||||
{
|
||||
if (builder.Services == null) throw new ArgumentNullException(nameof(builder), "Service collection is null.");
|
||||
|
||||
var options = builder.Configuration.GetSection(sectionName).Get<TOptions>();
|
||||
if (options == null)
|
||||
throw new InvalidOperationException(
|
||||
$"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable.");
|
||||
|
||||
builder.Services.Configure<TOptions>(builder.Configuration.GetSection(sectionName));
|
||||
builder.Services.AddSingleton<TService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for Sonarr with default options and client.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
|
||||
public static WebApplicationBuilder AddSonarrSupport(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServicesWithOptions<SonarrInstanceOptions, SonarrClient, IArrApplication>("Sonarr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for Lidarr with default options and client.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
|
||||
public static WebApplicationBuilder AddLidarrSupport(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServicesWithOptions<LidarrInstanceOptions, LidarrClient, IArrApplication>("Lidarr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds support for Readarr with default options and client.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
|
||||
public static WebApplicationBuilder AddReadarrSupport(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServicesWithOptions<ReadarrInstanceOptions, ReadarrClient, IArrApplication>("Readarr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a title lookup service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
|
||||
public static WebApplicationBuilder AddTitleLookupService(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServiceWithOptions<GlobalOptions, TitleApiService>("Settings");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a proxy request service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
|
||||
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
|
||||
public static WebApplicationBuilder AddProxyRequestService(this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder.AddServiceWithOptions<GlobalOptions, ProxyRequestService>("Settings");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user