feat: Move config templates to new repository

pull/201/head
Robert Dailey 1 year ago
parent 7edcd77f98
commit 3823b0ec43

@ -19,6 +19,8 @@ changes you may need to make.
lessens the need to redact information in the console.
- **BREAKING**: `replace_existing_custom_formats` now defaults to `true`.
- **BREAKING**: Restructured repository settings.
- Configuration templates repository moved to `recyclarr/config-templates` on GitHub. Corresponding
settings for this repo as well (see the Settings YAML Reference on the wiki for more details).
### Removed

@ -1,6 +1,7 @@
using System.IO.Abstractions;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TrashLib.Settings;
namespace Recyclarr.Cli.Tests;
@ -74,11 +75,11 @@ log_janitor:
var expectedDirs = new[]
{
Paths.CacheDirectory.FullName,
Paths.LogDirectory.FullName,
Paths.RepoDirectory.FullName,
Paths.CacheDirectory.FullName
Paths.ConfigsDirectory.FullName
};
expectedDirs.Should().IntersectWith(Fs.AllDirectories);
Fs.LeafDirectories().Should().BeEquivalentTo(expectedDirs);
}
}

@ -3,7 +3,10 @@ using System.Diagnostics.CodeAnalysis;
using Autofac;
using Autofac.Core;
using NUnit.Framework.Internal;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TestLibrary.Autofac;
using Recyclarr.TrashLib.Startup;
using Serilog.Core;
using Spectre.Console;
namespace Recyclarr.Cli.Tests;
@ -14,17 +17,26 @@ public class CompositionRootTest
// Warning CA1812 : CompositionRootTest.ConcreteTypeEnumerator is an internal class that is apparently never
// instantiated.
[SuppressMessage("Performance", "CA1812", Justification = "Created via reflection by TestCaseSource attribute")]
private sealed class ConcreteTypeEnumerator : CliIntegrationFixture, IEnumerable
private sealed class ConcreteTypeEnumerator : IEnumerable
{
public IEnumerator GetEnumerator()
{
return Container.ComponentRegistry.Registrations
var builder = new ContainerBuilder();
CompositionRoot.Setup(builder);
CompositionRoot.RegisterExternal(builder, new LoggingLevelSwitch(), new AppDataPathProvider());
// These are things that Spectre.Console normally registers for us, so they won't explicitly be
// in the CompositionRoot. Register mocks/stubs here.
builder.RegisterMockFor<IAnsiConsole>();
var container = builder.Build();
return container.ComponentRegistry.Registrations
.SelectMany(x => x.Services)
.OfType<TypedService>()
.Select(x => x.ServiceType)
.Distinct()
.Where(x => x.FullName == null || !x.FullName.StartsWith("Autofac."))
.Select(x => new TestCaseParameters(new object[] {Container, x}) {TestName = x.FullName})
.Select(x => new TestCaseParameters(new object[] {container, x}) {TestName = x.FullName})
.GetEnumerator();
}
}

@ -1,4 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using Recyclarr.Cli.Pipelines.CustomFormat.Guide;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Startup;
@ -50,7 +51,7 @@ public class CustomFormatCategoryParserTest
------
";
var file = paths.RepoDirectory
var file = fs.CurrentDirectory()
.SubDirectory("docs")
.SubDirectory("Radarr")
.File("Radarr-collection-of-custom-formats.md");

@ -4,6 +4,7 @@ using Recyclarr.Cli.TestLibrary;
using Recyclarr.Common.Extensions;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Repo;
namespace Recyclarr.Cli.Tests.Pipelines.QualitySize.Guide;
@ -15,8 +16,23 @@ public class QualityGuideServiceTest : CliIntegrationFixture
[TestCase(SupportedServices.Radarr, "radarr")]
public void Get_data_for_service(SupportedServices service, string serviceDir)
{
var repo = Resolve<ITrashGuidesRepo>();
const string metadataJson = @"
{
'json_paths': {
'radarr': {
'qualities': ['docs/json/radarr/quality-size']
},
'sonarr': {
'qualities': ['docs/json/sonarr/quality-size']
}
}
}";
Fs.AddFile(repo.Path.File("metadata.json"), new MockFileData(metadataJson));
Fs.AddFileFromEmbeddedResource(
Paths.RepoDirectory.SubDir("docs", "json", serviceDir, "quality-size").File("metadata.json"),
repo.Path.SubDir("docs", "json", serviceDir, "quality-size").File("some-quality-size.json"),
GetType(),
"Data.quality_size.json");

@ -10,20 +10,20 @@ public class ServiceCompatibilityIntegrationTest : CliIntegrationFixture
[Test]
public void Load_settings_yml_correctly_when_file_exists()
{
var sut = Resolve<ISettingsProvider>();
var sut = Resolve<SettingsProvider>();
// For this test, it doesn't really matter if the YAML data matches what SettingsValue expects.
// This test only ensures that the data deserialized is from the actual correct file.
const string yamlData = @"
repositories:
trash_guide:
trash_guides:
clone_url: http://the_url.com
";
Fs.AddFile(Paths.SettingsPath.FullName, new MockFileData(yamlData));
Fs.AddFile(Paths.SettingsPath, new MockFileData(yamlData));
Paths.SettingsPath.Refresh();
var settings = sut.Settings;
settings.Repositories.TrashGuide.CloneUrl.Should().Be("http://the_url.com");
settings.Repositories.TrashGuides.CloneUrl.Should().Be("http://the_url.com");
}
}

@ -19,6 +19,8 @@ using Recyclarr.Cli.Processors;
using Recyclarr.Common;
using Recyclarr.TrashLib;
using Recyclarr.TrashLib.Interfaces;
using Recyclarr.TrashLib.Startup;
using Serilog.Core;
using Spectre.Console.Cli;
namespace Recyclarr.Cli;
@ -87,4 +89,13 @@ public static class CompositionRoot
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AssignableTo<CommandSettings>();
}
public static void RegisterExternal(
ContainerBuilder builder,
LoggingLevelSwitch logLevelSwitch,
AppDataPathProvider appDataPathProvider)
{
builder.RegisterInstance(logLevelSwitch);
builder.RegisterInstance(appDataPathProvider);
}
}

@ -5,7 +5,6 @@ using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Settings;
using Spectre.Console.Cli;
#pragma warning disable CS8765
@ -17,8 +16,7 @@ namespace Recyclarr.Cli.Console.Commands;
internal class ListCustomFormatsCommand : AsyncCommand<ListCustomFormatsCommand.CliSettings>
{
private readonly CustomFormatDataLister _lister;
private readonly IRepoUpdater _repoUpdater;
private readonly ISettingsProvider _settings;
private readonly ITrashGuidesRepo _repo;
[UsedImplicitly]
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
@ -30,19 +28,15 @@ internal class ListCustomFormatsCommand : AsyncCommand<ListCustomFormatsCommand.
public SupportedServices Service { get; init; }
}
public ListCustomFormatsCommand(
CustomFormatDataLister lister,
IRepoUpdater repoUpdater,
ISettingsProvider settings)
public ListCustomFormatsCommand(CustomFormatDataLister lister, ITrashGuidesRepo repo)
{
_lister = lister;
_repoUpdater = repoUpdater;
_settings = settings;
_repo = repo;
}
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
{
await _repoUpdater.UpdateRepo(_settings.Settings.Repositories.TrashGuide);
await _repo.Update();
_lister.ListCustomFormats(settings.Service);
return 0;
}

@ -5,7 +5,6 @@ using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Pipelines.QualitySize.Guide;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Settings;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;
@ -16,8 +15,7 @@ namespace Recyclarr.Cli.Console.Commands;
internal class ListQualitiesCommand : AsyncCommand<ListQualitiesCommand.CliSettings>
{
private readonly QualitySizeDataLister _lister;
private readonly IRepoUpdater _repoUpdater;
private readonly ISettingsProvider _settings;
private readonly ITrashGuidesRepo _repoUpdater;
[UsedImplicitly]
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
@ -29,16 +27,15 @@ internal class ListQualitiesCommand : AsyncCommand<ListQualitiesCommand.CliSetti
public SupportedServices Service { get; init; }
}
public ListQualitiesCommand(QualitySizeDataLister lister, IRepoUpdater repoUpdater, ISettingsProvider settings)
public ListQualitiesCommand(QualitySizeDataLister lister, ITrashGuidesRepo repo)
{
_lister = lister;
_repoUpdater = repoUpdater;
_settings = settings;
_repoUpdater = repo;
}
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
{
await _repoUpdater.UpdateRepo(_settings.Settings.Repositories.TrashGuide);
await _repoUpdater.Update();
_lister.ListQualities(settings.Service);
return 0;
}

@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Recyclarr.Cli.Pipelines.ReleaseProfile.Guide;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Settings;
using Spectre.Console.Cli;
#pragma warning disable CS8765
@ -16,8 +15,7 @@ internal class ListReleaseProfilesCommand : AsyncCommand<ListReleaseProfilesComm
{
private readonly ILogger _log;
private readonly ReleaseProfileDataLister _lister;
private readonly IRepoUpdater _repoUpdater;
private readonly ISettingsProvider _settings;
private readonly ITrashGuidesRepo _repoUpdater;
[UsedImplicitly]
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
@ -31,23 +29,18 @@ internal class ListReleaseProfilesCommand : AsyncCommand<ListReleaseProfilesComm
public string? ListTerms { get; init; }
}
public ListReleaseProfilesCommand(
ILogger log,
ReleaseProfileDataLister lister,
IRepoUpdater repoUpdater,
ISettingsProvider settings)
public ListReleaseProfilesCommand(ILogger log, ReleaseProfileDataLister lister, ITrashGuidesRepo repo)
{
_log = log;
_lister = lister;
_repoUpdater = repoUpdater;
_settings = settings;
_repoUpdater = repo;
}
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
{
try
{
await _repoUpdater.UpdateRepo(_settings.Settings.Repositories.TrashGuide);
await _repoUpdater.Update();
if (settings.ListTerms is not null)
{

@ -8,7 +8,6 @@ using Recyclarr.Cli.Migration;
using Recyclarr.Cli.Processors;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Settings;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;
@ -18,9 +17,8 @@ namespace Recyclarr.Cli.Console.Commands;
public class SyncCommand : AsyncCommand<SyncCommand.CliSettings>
{
private readonly IMigrationExecutor _migration;
private readonly IRepoUpdater _repoUpdater;
private readonly ITrashGuidesRepo _repoUpdater;
private readonly ISyncProcessor _syncProcessor;
private readonly ISettingsProvider _settings;
[UsedImplicitly]
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
@ -52,16 +50,11 @@ public class SyncCommand : AsyncCommand<SyncCommand.CliSettings>
public IReadOnlyCollection<string> Instances => InstancesOption;
}
public SyncCommand(
IMigrationExecutor migration,
IRepoUpdater repoUpdater,
ISyncProcessor syncProcessor,
ISettingsProvider settings)
public SyncCommand(IMigrationExecutor migration, ITrashGuidesRepo repo, ISyncProcessor syncProcessor)
{
_migration = migration;
_repoUpdater = repoUpdater;
_repoUpdater = repo;
_syncProcessor = syncProcessor;
_settings = settings;
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
@ -69,7 +62,7 @@ public class SyncCommand : AsyncCommand<SyncCommand.CliSettings>
{
// Will throw if migration is required, otherwise just a warning is issued.
_migration.CheckNeededMigrations();
await _repoUpdater.UpdateRepo(_settings.Settings.Repositories.TrashGuide);
await _repoUpdater.Update();
return (int) await _syncProcessor.ProcessConfigs(settings);
}

@ -1,5 +1,6 @@
using Autofac.Features.Indexed;
using Recyclarr.TrashLib.Config.Listers;
using Recyclarr.TrashLib.Repo;
namespace Recyclarr.Cli.Processors;
@ -7,15 +8,25 @@ public class ConfigListProcessor
{
private readonly ILogger _log;
private readonly IIndex<ConfigListCategory, IConfigLister> _configListers;
private readonly IConfigTemplatesRepo _repo;
public ConfigListProcessor(ILogger log, IIndex<ConfigListCategory, IConfigLister> configListers)
public ConfigListProcessor(
ILogger log,
IIndex<ConfigListCategory, IConfigLister> configListers,
IConfigTemplatesRepo repo)
{
_log = log;
_configListers = configListers;
_repo = repo;
}
public async Task Process(ConfigListCategory listCategory)
{
if (listCategory == ConfigListCategory.Templates)
{
await _repo.Update();
}
_log.Debug("Listing configuration for category {Category}", listCategory);
if (!_configListers.TryGetValue(listCategory, out var lister))
{

@ -24,10 +24,8 @@ internal static partial class Program
CompositionRoot.Setup(builder);
var logLevelSwitch = new LoggingLevelSwitch();
builder.RegisterInstance(logLevelSwitch);
var appDataPathProvider = new AppDataPathProvider();
builder.RegisterInstance(appDataPathProvider);
CompositionRoot.RegisterExternal(builder, logLevelSwitch, appDataPathProvider);
var app = new CommandApp(new AutofacTypeRegistrar(builder, s => _scope = s));
app.Configure(config =>

@ -1,4 +1,5 @@
using Autofac;
using Autofac.Builder;
namespace Recyclarr.Common.Extensions;
@ -9,4 +10,16 @@ public static class AutofacExtensions
var type = genericType.MakeGenericType(genericArgs);
return scope.Resolve(type);
}
public static IRegistrationBuilder<TLimit, TReflectionActivatorData, TStyle>
WithTypeParameter<TLimit, TReflectionActivatorData, TStyle>(
this IRegistrationBuilder<TLimit, TReflectionActivatorData, TStyle> builder,
Type paramType,
Func<IComponentContext, object> resolver)
where TReflectionActivatorData : ReflectionActivatorData
{
return builder.WithParameter(
(info, _) => info.ParameterType == paramType,
(_, context) => resolver(context));
}
}

@ -1,18 +0,0 @@
{
"json_paths": {
"radarr": {
"custom_formats": ["docs/json/radarr/cf"],
"qualities": ["docs/json/radarr/quality-size"],
"naming": ["docs/json/radarr/naming"]
},
"sonarr": {
"release_profiles": ["docs/json/sonarr/rp"],
"custom_formats": ["docs/json/sonarr/cf"],
"qualities": ["docs/json/sonarr/quality-size"],
"naming": ["docs/json/sonarr/naming"]
}
},
"recyclarr": {
"templates": "docs/recyclarr-configs"
}
}

@ -5,7 +5,6 @@ using Autofac.Features.ResolveAnything;
using AutoMapper.Contrib.Autofac.DependencyInjection;
using AutoMapper.EquivalencyExpression;
using Recyclarr.Common;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TestLibrary.Autofac;
using Recyclarr.TrashLib.ApiServices.System;
using Recyclarr.TrashLib.Repo.VersionControl;
@ -28,8 +27,6 @@ public abstract class TrashLibIntegrationFixture : IDisposable
Paths = new AppPaths(Fs.CurrentDirectory().SubDirectory("test").SubDirectory("recyclarr"));
Logger = CreateLogger();
SetupMetadataJson();
_container = new Lazy<IContainer>(() =>
{
var builder = new ContainerBuilder();
@ -77,12 +74,6 @@ public abstract class TrashLibIntegrationFixture : IDisposable
.CreateLogger();
}
private void SetupMetadataJson()
{
var metadataFile = Paths.RepoDirectory.File("metadata.json");
Fs.AddFileFromEmbeddedResource(metadataFile, typeof(TrashLibIntegrationFixture), "Data.metadata.json");
}
// ReSharper disable MemberCanBePrivate.Global
private readonly Lazy<IContainer> _container;

@ -18,7 +18,7 @@ public class ConfigTemplateListerTest : TrashLibIntegrationFixture
[Frozen] IConfigTemplateGuideService guideService,
ConfigTemplateLister sut)
{
guideService.TemplateData.Returns(new[]
guideService.LoadTemplateData().Returns(new[]
{
new TemplatePath(SupportedServices.Radarr, "r1", stubFile, false),
new TemplatePath(SupportedServices.Radarr, "r2", stubFile, false),

@ -1,8 +1,8 @@
using System.IO.Abstractions;
using Recyclarr.Common.Extensions;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.Config.Services;
@ -15,15 +15,16 @@ public class ConfigTemplateGuideServiceTest : TrashLibIntegrationFixture
public void Throw_when_templates_dir_does_not_exist(
ConfigTemplateGuideService sut)
{
var act = () => _ = sut.TemplateData;
var act = () => _ = sut.LoadTemplateData();
act.Should().Throw<InvalidDataException>().WithMessage("Path*templates*");
act.Should().Throw<InvalidDataException>().WithMessage("Recyclarr*templates*");
}
[Test]
public void Normal_behavior()
{
var templateDir = Paths.RepoDirectory.SubDir("docs/recyclarr-configs");
var repo = Resolve<IConfigTemplatesRepo>();
var templateDir = repo.Path;
Fs.AddSameFileFromEmbeddedResource(templateDir.File("templates.json"), typeof(ConfigTemplateGuideServiceTest));
TemplatePath MakeTemplatePath(SupportedServices service, string id, string path)
@ -43,7 +44,7 @@ public class ConfigTemplateGuideServiceTest : TrashLibIntegrationFixture
var sut = Resolve<ConfigTemplateGuideService>();
var data = sut.TemplateData;
var data = sut.LoadTemplateData();
data.Should().BeEquivalentTo(expectedPaths, o => o.Excluding(x => x.TemplateFile));
data.Select(x => x.TemplateFile.FullName)
.Should().BeEquivalentTo(expectedPaths.Select(x => x.TemplateFile.FullName));

@ -20,7 +20,7 @@ public class AppPaths : IAppPaths
public IFileInfo SettingsPath => AppDataDirectory.File("settings.yml");
public IFileInfo SecretsPath => AppDataDirectory.File("secrets.yml");
public IDirectoryInfo LogDirectory => AppDataDirectory.SubDir("logs", "cli");
public IDirectoryInfo RepoDirectory => AppDataDirectory.SubDir("repo");
public IDirectoryInfo ReposDirectory => AppDataDirectory.SubDir("repositories");
public IDirectoryInfo CacheDirectory => AppDataDirectory.SubDir("cache");
public IDirectoryInfo ConfigsDirectory => AppDataDirectory.SubDir("configs");
}

@ -1,6 +1,4 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Settings;
using Spectre.Console;
namespace Recyclarr.TrashLib.Config.Listers;
@ -9,26 +7,18 @@ public class ConfigTemplateLister : IConfigLister
{
private readonly IAnsiConsole _console;
private readonly IConfigTemplateGuideService _guideService;
private readonly IRepoUpdater _repoUpdater;
private readonly ISettingsProvider _settings;
public ConfigTemplateLister(
IAnsiConsole console,
IConfigTemplateGuideService guideService,
IRepoUpdater repoUpdater,
ISettingsProvider settings)
IConfigTemplateGuideService guideService)
{
_console = console;
_guideService = guideService;
_repoUpdater = repoUpdater;
_settings = settings;
}
public async Task List()
public Task List()
{
await _repoUpdater.UpdateRepo(_settings.Settings.Repositories.ConfigTemplates);
var data = _guideService.TemplateData;
var data = _guideService.LoadTemplateData();
var table = new Table();
var empty = new Markup("");
@ -44,6 +34,7 @@ public class ConfigTemplateLister : IConfigLister
}
_console.Write(table);
return Task.CompletedTask;
}
private static IEnumerable<Markup> RenderTemplates(

@ -23,7 +23,7 @@ public static class ContextualMessages
{
return
"Usage of 'repository' setting is no " +
"longer supported. Use 'trash_guide' under 'repositories' instead." +
"longer supported. Use 'trash_guides' under 'repositories' instead." +
"See: https://recyclarr.dev/wiki/upgrade-guide/v5.0/#settings-repository-changes";
}

@ -1,9 +1,7 @@
using System.Collections.ObjectModel;
using System.IO.Abstractions;
using JetBrains.Annotations;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Config.Services;
@ -20,35 +18,27 @@ public record TemplatePath(SupportedServices Service, string Id, IFileInfo Templ
public class ConfigTemplateGuideService : IConfigTemplateGuideService
{
private readonly IRepoMetadataBuilder _metadataBuilder;
private readonly IAppPaths _paths;
private readonly Lazy<IReadOnlyCollection<TemplatePath>> _templateData;
private readonly IConfigTemplatesRepo _repo;
public ConfigTemplateGuideService(
IRepoMetadataBuilder metadataBuilder,
IAppPaths paths)
public ConfigTemplateGuideService(IConfigTemplatesRepo repo)
{
_metadataBuilder = metadataBuilder;
_paths = paths;
_templateData = new Lazy<IReadOnlyCollection<TemplatePath>>(LoadTemplateData);
_repo = repo;
}
private IReadOnlyCollection<TemplatePath> LoadTemplateData()
public IReadOnlyCollection<TemplatePath> LoadTemplateData()
{
var metadata = _metadataBuilder.GetMetadata();
var templatesPath = _paths.RepoDirectory.SubDir(metadata.Recyclarr.Templates);
var templatesPath = _repo.Path.File("templates.json");
if (!templatesPath.Exists)
{
throw new InvalidDataException(
$"Path to recyclarr templates does not exist: {metadata.Recyclarr.Templates}");
$"Recyclarr templates.json does not exist: {templatesPath}");
}
var templates = TrashRepoJsonParser.Deserialize<TemplatesData>(templatesPath.File("templates.json"));
var templates = TrashRepoJsonParser.Deserialize<TemplatesData>(templatesPath);
TemplatePath NewTemplatePath(TemplateEntry entry, SupportedServices service)
{
return new TemplatePath(service, entry.Id, templatesPath.File(entry.Template), entry.Hidden);
return new TemplatePath(service, entry.Id, _repo.Path.File(entry.Template), entry.Hidden);
}
return templates.Radarr
@ -56,6 +46,4 @@ public class ConfigTemplateGuideService : IConfigTemplateGuideService
.Concat(templates.Sonarr.Select(x => NewTemplatePath(x, SupportedServices.Sonarr)))
.ToList();
}
public IReadOnlyCollection<TemplatePath> TemplateData => _templateData.Value;
}

@ -2,5 +2,5 @@ namespace Recyclarr.TrashLib.Config.Services;
public interface IConfigTemplateGuideService
{
IReadOnlyCollection<TemplatePath> TemplateData { get; }
IReadOnlyCollection<TemplatePath> LoadTemplateData();
}

@ -0,0 +1,26 @@
using System.IO.Abstractions;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Settings;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Repo;
public class ConfigTemplatesRepo : IConfigTemplatesRepo
{
private readonly IRepoUpdater _repoUpdater;
private readonly ISettingsProvider _settings;
public ConfigTemplatesRepo(IRepoUpdater repoUpdater, IAppPaths paths, ISettingsProvider settings)
{
_repoUpdater = repoUpdater;
_settings = settings;
Path = paths.ReposDirectory.SubDir("config-templates");
}
public IDirectoryInfo Path { get; }
public Task Update()
{
return _repoUpdater.UpdateRepo(Path, _settings.Settings.Repositories.ConfigTemplates);
}
}

@ -0,0 +1,9 @@
using System.IO.Abstractions;
namespace Recyclarr.TrashLib.Repo;
public interface IConfigTemplatesRepo
{
IDirectoryInfo Path { get; }
Task Update();
}

@ -5,6 +5,5 @@ namespace Recyclarr.TrashLib.Repo;
public interface IRepoUpdater
{
IDirectoryInfo RepoPath { get; }
Task UpdateRepo(IRepositorySettings repoSettings);
Task UpdateRepo(IDirectoryInfo repoPath, IRepositorySettings repoSettings);
}

@ -0,0 +1,9 @@
using System.IO.Abstractions;
namespace Recyclarr.TrashLib.Repo;
public interface ITrashGuidesRepo
{
IDirectoryInfo Path { get; }
Task Update();
}

@ -7,6 +7,9 @@ public class RepoAutofacModule : Module
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterType<ConfigTemplatesRepo>().As<IConfigTemplatesRepo>();
builder.RegisterType<TrashGuidesRepo>().As<ITrashGuidesRepo>();
builder.RegisterType<RepoUpdater>().As<IRepoUpdater>();
builder.RegisterType<RepoMetadataBuilder>().As<IRepoMetadataBuilder>().InstancePerLifetimeScope();
}

@ -19,13 +19,7 @@ public record JsonPaths
public SonarrMetadata Sonarr { get; init; } = new();
}
public record RecyclarrMetadata
{
public string Templates { get; init; } = "";
}
public record RepoMetadata
{
public JsonPaths JsonPaths { get; init; } = new();
public RecyclarrMetadata Recyclarr { get; init; } = new();
}

@ -1,26 +1,25 @@
using System.IO.Abstractions;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Repo;
public class RepoMetadataBuilder : IRepoMetadataBuilder
{
private readonly IAppPaths _paths;
private readonly Lazy<RepoMetadata> _metadata;
private readonly IDirectoryInfo _repoPath;
public RepoMetadataBuilder(IAppPaths paths)
public RepoMetadataBuilder(ITrashGuidesRepo repo)
{
_paths = paths;
_metadata = new Lazy<RepoMetadata>(()
=> TrashRepoJsonParser.Deserialize<RepoMetadata>(_paths.RepoDirectory.File("metadata.json")));
_repoPath = repo.Path;
_metadata = new Lazy<RepoMetadata>(
() => TrashRepoJsonParser.Deserialize<RepoMetadata>(_repoPath.File("metadata.json")));
}
public IReadOnlyList<IDirectoryInfo> ToDirectoryInfoList(IEnumerable<string> listOfDirectories)
{
return listOfDirectories.Select(x => _paths.RepoDirectory.SubDirectory(x)).ToList();
return listOfDirectories.Select(x => _repoPath.SubDirectory(x)).ToList();
}
public IDirectoryInfo DocsDirectory => _paths.RepoDirectory.SubDirectory("docs");
public IDirectoryInfo DocsDirectory => _repoPath.SubDirectory("docs");
public RepoMetadata GetMetadata()
{

@ -2,49 +2,43 @@ using System.IO.Abstractions;
using Recyclarr.Common;
using Recyclarr.TrashLib.Repo.VersionControl;
using Recyclarr.TrashLib.Settings;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Repo;
public class RepoUpdater : IRepoUpdater
{
private readonly ILogger _log;
private readonly IAppPaths _paths;
private readonly IGitRepositoryFactory _repositoryFactory;
private readonly IFileUtilities _fileUtils;
public RepoUpdater(
ILogger log,
IAppPaths paths,
IGitRepositoryFactory repositoryFactory,
IFileUtilities fileUtils)
{
_log = log;
_paths = paths;
_repositoryFactory = repositoryFactory;
_fileUtils = fileUtils;
}
public IDirectoryInfo RepoPath => _paths.RepoDirectory;
public async Task UpdateRepo(IRepositorySettings repoSettings)
public async Task UpdateRepo(IDirectoryInfo repoPath, IRepositorySettings repoSettings)
{
// Retry only once if there's a failure. This gives us an opportunity to delete the git repository and start
// fresh.
try
{
await CheckoutAndUpdateRepo(repoSettings);
await CheckoutAndUpdateRepo(repoPath, repoSettings);
}
catch (GitCmdException e)
{
_log.Debug(e, "Non-zero exit code {ExitCode} while executing Git command: {Error}", e.ExitCode, e.Error);
_log.Warning("Deleting local git repo and retrying git operation due to error...");
_fileUtils.DeleteReadOnlyDirectory(RepoPath.FullName);
await CheckoutAndUpdateRepo(repoSettings);
_log.Warning("Deleting local git repo '{Repodir}' and retrying git operation due to error", repoPath.Name);
_fileUtils.DeleteReadOnlyDirectory(repoPath.FullName);
await CheckoutAndUpdateRepo(repoPath, repoSettings);
}
}
private async Task CheckoutAndUpdateRepo(IRepositorySettings repoSettings)
private async Task CheckoutAndUpdateRepo(IDirectoryInfo repoPath, IRepositorySettings repoSettings)
{
var cloneUrl = repoSettings.CloneUrl;
var branch = repoSettings.Branch;
@ -55,7 +49,7 @@ public class RepoUpdater : IRepoUpdater
_log.Warning("Using explicit SHA1 for local repository: {Sha1}", repoSettings.Sha1);
}
using var repo = await _repositoryFactory.CreateAndCloneIfNeeded(cloneUrl, RepoPath.FullName, branch);
using var repo = await _repositoryFactory.CreateAndCloneIfNeeded(cloneUrl, repoPath, branch);
await repo.ForceCheckout(branch);
await repo.Fetch();
await repo.ResetHard(repoSettings.Sha1 ?? $"origin/{branch}");

@ -0,0 +1,26 @@
using System.IO.Abstractions;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Settings;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Repo;
public class TrashGuidesRepo : ITrashGuidesRepo
{
private readonly IRepoUpdater _repoUpdater;
private readonly ISettingsProvider _settings;
public TrashGuidesRepo(IRepoUpdater repoUpdater, IAppPaths paths, ISettingsProvider settings)
{
_repoUpdater = repoUpdater;
_settings = settings;
Path = paths.ReposDirectory.SubDir("trash-guides");
}
public IDirectoryInfo Path { get; }
public Task Update()
{
return _repoUpdater.UpdateRepo(Path, _settings.Settings.Repositories.TrashGuides);
}
}

@ -1,21 +1,20 @@
using System.IO.Abstractions;
using System.Text;
using CliWrap;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Repo.VersionControl;
public sealed class GitRepository : IGitRepository
{
private readonly ILogger _log;
private readonly IAppPaths _paths;
private readonly IGitPath _gitPath;
private readonly IDirectoryInfo _workDir;
public GitRepository(ILogger log, IAppPaths paths, IGitPath gitPath)
public GitRepository(ILogger log, IGitPath gitPath, IDirectoryInfo workDir)
{
_log = log;
_paths = paths;
_gitPath = gitPath;
_workDir = workDir;
}
private Task RunGitCmd(params string[] args)
@ -30,18 +29,15 @@ public sealed class GitRepository : IGitRepository
var output = new StringBuilder();
var error = new StringBuilder();
_log.Debug("Using working directory: {Dir}", _workDir.FullName);
_workDir.Create();
var cli = Cli.Wrap(_gitPath.Path)
.WithArguments(args)
.WithValidation(CommandResultValidation.None)
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(output))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(error));
if (_paths.RepoDirectory.Exists)
{
var workDir = _paths.RepoDirectory.FullName;
_log.Debug("Using working directory: {Dir}", workDir);
cli = cli.WithWorkingDirectory(workDir);
}
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(error))
.WithWorkingDirectory(_workDir.FullName);
var result = await cli.ExecuteAsync();
@ -53,8 +49,6 @@ public sealed class GitRepository : IGitRepository
}
}
public IDirectoryInfo Path => _paths.RepoDirectory;
public void Dispose()
{
// Nothing to do here
@ -93,7 +87,7 @@ public sealed class GitRepository : IGitRepository
args.AddRange(new[] {"-b", branch});
}
args.AddRange(new[] {cloneUrl.ToString(), _paths.RepoDirectory.FullName});
args.AddRange(new[] {cloneUrl.ToString(), "."});
await RunGitCmd(args);
}
}

@ -1,25 +1,25 @@
using System.IO.Abstractions;
namespace Recyclarr.TrashLib.Repo.VersionControl;
public class GitRepositoryFactory : IGitRepositoryFactory
{
private readonly Func<string, IGitRepository> _repoFactory;
private readonly ILogger _log;
private readonly IGitPath _gitPath;
public GitRepositoryFactory(
Func<string, IGitRepository> repoFactory,
ILogger log)
public GitRepositoryFactory(ILogger log, IGitPath gitPath)
{
_repoFactory = repoFactory;
_log = log;
_gitPath = gitPath;
}
public async Task<IGitRepository> CreateAndCloneIfNeeded(Uri repoUrl, string repoPath, string branch)
public async Task<IGitRepository> CreateAndCloneIfNeeded(Uri repoUrl, IDirectoryInfo repoPath, string branch)
{
var repo = _repoFactory(repoPath);
var repo = new GitRepository(_log, _gitPath, repoPath);
if (!repo.Path.Exists)
if (!repoPath.Exists)
{
_log.Information("Cloning trash repository...");
_log.Information("Cloning '{RepoName}' repository...", repoPath.Name);
await repo.Clone(repoUrl, branch);
}
else

@ -1,5 +1,3 @@
using System.IO.Abstractions;
namespace Recyclarr.TrashLib.Repo.VersionControl;
public interface IGitRepository : IDisposable
@ -8,7 +6,6 @@ public interface IGitRepository : IDisposable
Task Fetch(string remote = "origin");
Task ResetHard(string toBranchOrSha1);
Task SetRemote(string name, Uri newUrl);
IDirectoryInfo Path { get; }
Task Clone(Uri cloneUrl, string? branch = null);
Task Status();
}

@ -1,6 +1,8 @@
using System.IO.Abstractions;
namespace Recyclarr.TrashLib.Repo.VersionControl;
public interface IGitRepositoryFactory
{
Task<IGitRepository> CreateAndCloneIfNeeded(Uri repoUrl, string repoPath, string branch);
Task<IGitRepository> CreateAndCloneIfNeeded(Uri repoUrl, IDirectoryInfo repoPath, string branch);
}

@ -6,7 +6,6 @@ public class VersionControlAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<GitRepository>().As<IGitRepository>();
builder.RegisterType<GitRepositoryFactory>().As<IGitRepositoryFactory>();
builder.RegisterType<GitPath>().As<IGitPath>();
base.Load(builder);

@ -1,6 +1,8 @@
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Parsing.ErrorHandling;
using Recyclarr.TrashLib.Config.Yaml;
using Recyclarr.TrashLib.Startup;
using YamlDotNet.Core;
namespace Recyclarr.TrashLib.Settings;
@ -8,11 +10,13 @@ public class SettingsProvider : ISettingsProvider
{
public SettingsValues Settings => _settings.Value;
private readonly ILogger _log;
private readonly IAppPaths _paths;
private readonly Lazy<SettingsValues> _settings;
public SettingsProvider(IAppPaths paths, IYamlSerializerFactory serializerFactory)
public SettingsProvider(ILogger log, IAppPaths paths, IYamlSerializerFactory serializerFactory)
{
_log = log;
_paths = paths;
_settings = new Lazy<SettingsValues>(() => LoadOrCreateSettingsFile(serializerFactory));
}
@ -24,9 +28,22 @@ public class SettingsProvider : ISettingsProvider
CreateDefaultSettingsFile();
}
using var stream = _paths.SettingsPath.OpenText();
var deserializer = serializerFactory.CreateDeserializer();
return deserializer.Deserialize<SettingsValues?>(stream.ReadToEnd()) ?? new SettingsValues();
try
{
using var stream = _paths.SettingsPath.OpenText();
var deserializer = serializerFactory.CreateDeserializer();
return deserializer.Deserialize<SettingsValues?>(stream.ReadToEnd()) ?? new SettingsValues();
}
catch (YamlException e)
{
_log.Debug(e, "Exception while parsing settings file");
var line = e.Start.Line;
var msg = ContextualMessages.GetContextualErrorFromException(e);
_log.Error("Exception while parsing settings.yml at line {Line}: {Msg}", line, msg);
throw;
}
}
private void CreateDefaultSettingsFile()

@ -23,7 +23,7 @@ public record LogJanitorSettings
public record Repositories
{
public TrashRepository TrashGuide { get; [UsedImplicitly] init; } = new();
public TrashRepository TrashGuides { get; [UsedImplicitly] init; } = new();
public ConfigTemplateRepository ConfigTemplates { get; [UsedImplicitly] init; } = new();
}

@ -9,7 +9,7 @@ public interface IAppPaths
IFileInfo SettingsPath { get; }
IFileInfo SecretsPath { get; }
IDirectoryInfo LogDirectory { get; }
IDirectoryInfo RepoDirectory { get; }
IDirectoryInfo ReposDirectory { get; }
IDirectoryInfo CacheDirectory { get; }
IDirectoryInfo ConfigsDirectory { get; }
}

Loading…
Cancel
Save