diff --git a/src/Common.Tests/Extensions/FileSystemExtensionsTest.cs b/src/Common.Tests/Extensions/FileSystemExtensionsTest.cs index 8baadce7..54a1d4aa 100644 --- a/src/Common.Tests/Extensions/FileSystemExtensionsTest.cs +++ b/src/Common.Tests/Extensions/FileSystemExtensionsTest.cs @@ -57,7 +57,9 @@ public class FileSystemExtensionsTest var fs = NewMockFileSystem(files, dirs, @"C:\root\path"); - fs.MergeDirectory("path1", "path2"); + fs.MergeDirectory( + fs.DirectoryInfo.FromDirectoryName("path1"), + fs.DirectoryInfo.FromDirectoryName("path2")); fs.AllDirectories.Select(MockUnixSupport.Path).Should() .NotContain(x => x.Contains("path1") || x.Contains("empty")); @@ -77,7 +79,9 @@ public class FileSystemExtensionsTest var fs = NewMockFileSystem(files, @"C:\root\path"); - var act = () => fs.MergeDirectory("path1", "path2"); + var act = () => fs.MergeDirectory( + fs.DirectoryInfo.FromDirectoryName("path1"), + fs.DirectoryInfo.FromDirectoryName("path2")); act.Should().Throw(); } @@ -97,7 +101,9 @@ public class FileSystemExtensionsTest var fs = NewMockFileSystem(files, dirs, @"C:\root\path"); - var act = () => fs.MergeDirectory("path1", "path2"); + var act = () => fs.MergeDirectory( + fs.DirectoryInfo.FromDirectoryName("path1"), + fs.DirectoryInfo.FromDirectoryName("path2")); act.Should().Throw(); } diff --git a/src/Common/Extensions/FileSystemExtensions.cs b/src/Common/Extensions/FileSystemExtensions.cs index 6b2678d8..936b19f7 100644 --- a/src/Common/Extensions/FileSystemExtensions.cs +++ b/src/Common/Extensions/FileSystemExtensions.cs @@ -6,14 +6,12 @@ namespace Common.Extensions; public static class FileSystemExtensions { - public static void MergeDirectory(this IFileSystem fs, string targetDir, string destDir, IConsole? console = null) + public static void MergeDirectory(this IFileSystem fs, IDirectoryInfo targetDir, IDirectoryInfo destDir, + IConsole? console = null) { - targetDir = fs.Path.GetFullPath(targetDir); - destDir = fs.Path.GetFullPath(destDir); - - var directories = fs.DirectoryInfo.FromDirectoryName(targetDir) + var directories = targetDir .EnumerateDirectories("*", SearchOption.AllDirectories) - .Append(fs.DirectoryInfo.FromDirectoryName(targetDir)) + .Append(targetDir) .OrderByDescending(x => x.FullName.Count(y => y is '/' or '\\')); foreach (var dir in directories) @@ -23,7 +21,7 @@ public static class FileSystemExtensions // Is it a symbolic link? if ((dir.Attributes & FileAttributes.ReparsePoint) != 0) { - var newPath = RelocatePath(dir.FullName, targetDir, destDir); + var newPath = RelocatePath(dir.FullName, targetDir.FullName, destDir.FullName); fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(newPath)); console?.Output.WriteLine($" - Symlink: {dir.FullName} :: TO :: {newPath}"); dir.MoveTo(newPath); @@ -33,7 +31,7 @@ public static class FileSystemExtensions // For real directories, move all the files inside foreach (var file in dir.EnumerateFiles()) { - var newPath = RelocatePath(file.FullName, targetDir, destDir); + var newPath = RelocatePath(file.FullName, targetDir.FullName, destDir.FullName); fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(newPath)); console?.Output.WriteLine($" - Moving: {file.FullName} :: TO :: {newPath}"); file.MoveTo(newPath); diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 0259bbed..7ca61b65 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -25,7 +25,6 @@ - @@ -41,6 +40,15 @@ + + + + diff --git a/src/Recyclarr.TestLibrary/Recyclarr.TestLibrary.csproj b/src/Recyclarr.TestLibrary/Recyclarr.TestLibrary.csproj new file mode 100644 index 00000000..76b73eff --- /dev/null +++ b/src/Recyclarr.TestLibrary/Recyclarr.TestLibrary.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Recyclarr.TestLibrary/TestAppPaths.cs b/src/Recyclarr.TestLibrary/TestAppPaths.cs new file mode 100644 index 00000000..22dff72a --- /dev/null +++ b/src/Recyclarr.TestLibrary/TestAppPaths.cs @@ -0,0 +1,12 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.Extensions; + +namespace Recyclarr.TestLibrary; + +public sealed class TestAppPaths : AppPaths +{ + public TestAppPaths(IFileSystem fs) + : base(fs.CurrentDirectory().SubDirectory("recyclarr")) + { + } +} diff --git a/src/Recyclarr.Tests/Command/BaseCommandTest.cs b/src/Recyclarr.Tests/Command/BaseCommandTest.cs new file mode 100644 index 00000000..bc13c407 --- /dev/null +++ b/src/Recyclarr.Tests/Command/BaseCommandTest.cs @@ -0,0 +1,51 @@ +using System.IO.Abstractions.TestingHelpers; +using AutoFixture.NUnit3; +using CliFx.Attributes; +using CliFx.Infrastructure; +using FluentAssertions; +using NUnit.Framework; +using Recyclarr.Command; +using Recyclarr.TestLibrary; +using TestLibrary.AutoFixture; + +namespace Recyclarr.Tests.Command; + +[Command] +public class StubBaseCommand : BaseCommand +{ + public override string? AppDataDirectory { get; set; } + + public StubBaseCommand(ICompositionRoot compositionRoot) + { + CompositionRoot = compositionRoot; + } + + public override Task Process(IServiceLocatorProxy container) + { + return Task.CompletedTask; + } +} + +[TestFixture] +// Cannot be parallelized due to static CompositionRoot property +public class BaseCommandTest +{ + [Test, AutoMockData] + public async Task All_directories_are_created( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, + IConsole console, + StubBaseCommand sut) + { + await sut.ExecuteAsync(console); + + var expectedDirs = new[] + { + paths.LogDirectory.FullName, + paths.RepoDirectory.FullName, + paths.CacheDirectory.FullName + }; + + expectedDirs.Should().IntersectWith(fs.AllDirectories); + } +} diff --git a/src/Recyclarr.Tests/Command/CreateConfigCommandTest.cs b/src/Recyclarr.Tests/Command/CreateConfigCommandTest.cs index 50458593..6ce3c7fa 100644 --- a/src/Recyclarr.Tests/Command/CreateConfigCommandTest.cs +++ b/src/Recyclarr.Tests/Command/CreateConfigCommandTest.cs @@ -7,45 +7,50 @@ using FluentAssertions; using NSubstitute; using NUnit.Framework; using Recyclarr.Command; +using Recyclarr.TestLibrary; using TestLibrary.AutoFixture; -using TrashLib; // ReSharper disable MethodHasAsyncOverload namespace Recyclarr.Tests.Command; [TestFixture] -[Parallelizable(ParallelScope.All)] +// Cannot be parallelized due to static CompositionRoot property public class CreateConfigCommandTest { [Test, AutoMockData] public async Task Config_file_created_when_using_default_path( - [Frozen] IAppPaths paths, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - CreateConfigCommand cmd) + IServiceLocatorProxy container, + ICompositionRoot compositionRoot, + CreateConfigCommand sut) { - const string ymlPath = "path/recyclarr.yml"; - paths.ConfigPath.Returns(ymlPath); - await cmd.ExecuteAsync(Substitute.For()); + BaseCommand.CompositionRoot = compositionRoot; - var file = fs.GetFile(ymlPath); + await sut.Process(container); + + var file = fs.GetFile(paths.ConfigPath.FullName); file.Should().NotBeNull(); file.Contents.Should().NotBeEmpty(); } [Test, AutoMockData] public async Task Config_file_created_when_using_user_specified_path( - [Frozen] IAppPaths paths, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - CreateConfigCommand cmd) + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, + ICompositionRoot compositionRoot, + CreateConfigCommand sut) { + BaseCommand.CompositionRoot = compositionRoot; + var ymlPath = fs.CurrentDirectory() .SubDirectory("user") .SubDirectory("specified") .File("file.yml").FullName; - cmd.Path = ymlPath; - await cmd.ExecuteAsync(Substitute.For()); + sut.AppDataDirectory = ymlPath; + await sut.ExecuteAsync(Substitute.For()); var file = fs.GetFile(ymlPath); file.Should().NotBeNull(); diff --git a/src/Recyclarr.Tests/Command/Helpers/CliTypeActivatorTest.cs b/src/Recyclarr.Tests/Command/Helpers/CliTypeActivatorTest.cs deleted file mode 100644 index baf2c6c8..00000000 --- a/src/Recyclarr.Tests/Command/Helpers/CliTypeActivatorTest.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Autofac; -using FluentAssertions; -using NUnit.Framework; -using Recyclarr.Command; -using Recyclarr.Command.Helpers; - -namespace Recyclarr.Tests.Command.Helpers; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class CliTypeActivatorTest -{ - // Warning CA1812 : an internal class that is apparently never instantiated. - [SuppressMessage("Performance", "CA1812", Justification = "Registered to and created by Autofac")] - private class NonServiceCommandType - { - } - - // Warning CA1812 : an internal class that is apparently never instantiated. - [SuppressMessage("Performance", "CA1812", Justification = "Registered to and created by Autofac")] - private class StubCommand : IServiceCommand - { - public bool Preview => false; - public bool Debug => false; - public ICollection Config => new List(); - public string CacheStoragePath => ""; - public string Name => ""; - } - - [Test] - public void Resolve_NonServiceCommandType_NoActiveCommandSet() - { - var builder = new ContainerBuilder(); - builder.RegisterType(); - var container = CompositionRoot.Setup(builder); - - var createdType = CliTypeActivator.ResolveType(container, typeof(NonServiceCommandType)); - - Action act = () => _ = container.Resolve().ActiveCommand; - - createdType.Should().BeOfType(); - act.Should() - .Throw() - .WithMessage("The active command has not yet been determined"); - } - - [Test] - public void Resolve_ServiceCommandType_ActiveCommandSet() - { - var builder = new ContainerBuilder(); - builder.RegisterType(); - var container = CompositionRoot.Setup(builder); - - var createdType = CliTypeActivator.ResolveType(container, typeof(StubCommand)); - var activeCommand = container.Resolve().ActiveCommand; - - activeCommand.Should().BeSameAs(createdType); - activeCommand.Should().BeOfType(); - } -} diff --git a/src/Recyclarr.Tests/Command/Initialization/DefaultAppDataSetupTest.cs b/src/Recyclarr.Tests/Command/Initialization/DefaultAppDataSetupTest.cs index 5fd898ec..e071c1f3 100644 --- a/src/Recyclarr.Tests/Command/Initialization/DefaultAppDataSetupTest.cs +++ b/src/Recyclarr.Tests/Command/Initialization/DefaultAppDataSetupTest.cs @@ -7,9 +7,7 @@ using FluentAssertions; using NSubstitute; using NUnit.Framework; using Recyclarr.Command.Initialization; -using TestLibrary; using TestLibrary.AutoFixture; -using TrashLib; namespace Recyclarr.Tests.Command.Initialization; @@ -21,55 +19,71 @@ public class DefaultAppDataSetupTest public void Initialize_using_default_path( [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen] IEnvironment env, - [Frozen] IAppPaths paths, DefaultAppDataSetup sut) { env.GetEnvironmentVariable(default!).ReturnsForAnyArgs((string?) null); - paths.DefaultAppDataDirectoryName.Returns("app_data"); - env.GetFolderPath(Arg.Any(), Arg.Any()) - .Returns(FileUtils.NormalizePath("base/path")); + var basePath = fs.CurrentDirectory() + .SubDirectory("base") + .SubDirectory("path"); - sut.SetupDefaultPath(null, false); + env.GetFolderPath(default, default).ReturnsForAnyArgs(basePath.FullName); - paths.Received().SetAppDataPath(FileUtils.NormalizePath("base/path/app_data")); + var paths = sut.CreateAppPaths(); + + paths.AppDataDirectory.FullName.Should().Be(basePath.SubDirectory("recyclarr").FullName); } [Test, AutoMockData] public void Initialize_using_path_override( [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - [Frozen] IAppPaths paths, DefaultAppDataSetup sut) { - var overridePath = FileUtils.NormalizePath("/override/path"); - sut.SetupDefaultPath(overridePath, false); + var overridePath = fs.CurrentDirectory() + .SubDirectory("override") + .SubDirectory("path"); + + var paths = sut.CreateAppPaths(overridePath.FullName); - paths.Received().SetAppDataPath(overridePath); - fs.AllDirectories.Should().Contain(overridePath); + paths.AppDataDirectory.FullName.Should().Be(overridePath.FullName); } [Test, AutoMockData] public void Force_creation_uses_correct_behavior_when_disabled( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen] IEnvironment env, DefaultAppDataSetup sut) { + var overridePath = fs.CurrentDirectory() + .SubDirectory("override") + .SubDirectory("path"); + env.GetEnvironmentVariable(default!).ReturnsForAnyArgs((string?) null); + env.GetFolderPath(default).ReturnsForAnyArgs(overridePath.FullName); - sut.SetupDefaultPath(null, false); + sut.CreateAppPaths(null, false); env.Received().GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.None); + fs.AllDirectories.Should().NotContain(overridePath.FullName); } [Test, AutoMockData] public void Force_creation_uses_correct_behavior_when_enabled( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen] IEnvironment env, DefaultAppDataSetup sut) { + var overridePath = fs.CurrentDirectory() + .SubDirectory("override") + .SubDirectory("path"); + env.GetEnvironmentVariable(default!).ReturnsForAnyArgs((string?) null); + env.GetFolderPath(default).ReturnsForAnyArgs(overridePath.FullName); - sut.SetupDefaultPath(null, true); + sut.CreateAppPaths(); env.Received().GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create); + fs.AllDirectories.Should().NotContain(overridePath.FullName); } [Test, AutoMockData] @@ -85,7 +99,7 @@ public class DefaultAppDataSetupTest env.GetEnvironmentVariable(default!).ReturnsForAnyArgs(expectedPath); - sut.SetupDefaultPath(null, true); + sut.CreateAppPaths(); env.Received().GetEnvironmentVariable("RECYCLARR_APP_DATA"); fs.AllDirectories.Should().Contain(expectedPath); @@ -102,7 +116,7 @@ public class DefaultAppDataSetupTest .SubDirectory("var") .SubDirectory("path").FullName; - sut.SetupDefaultPath(expectedPath, true); + sut.CreateAppPaths(expectedPath); env.DidNotReceiveWithAnyArgs().GetEnvironmentVariable(default!); fs.AllDirectories.Should().Contain(expectedPath); diff --git a/src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs b/src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs deleted file mode 100644 index a38bd2e0..00000000 --- a/src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.IO.Abstractions.TestingHelpers; -using AutoFixture.NUnit3; -using Common; -using FluentAssertions; -using NUnit.Framework; -using Recyclarr.Command; -using Recyclarr.Command.Initialization.Init; -using TestLibrary.AutoFixture; -using TrashLib; - -namespace Recyclarr.Tests.Command.Initialization.Init; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class InitializeAppDataPathTest -{ - [Test, AutoMockData] - public void All_directories_are_created( - [Frozen] IEnvironment env, - [Frozen] IAppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - SonarrCommand cmd, - InitializeAppDataPath sut) - { - sut.Initialize(cmd); - - var expectedDirs = new[] - { - paths.LogDirectory, - paths.RepoDirectory, - paths.CacheDirectory - }; - - fs.AllDirectories.Select(x => fs.Path.GetFileName(x)) - .Should().IntersectWith(expectedDirs); - } -} diff --git a/src/Recyclarr.Tests/Command/Initialization/ServiceInitializationAndCleanupTest.cs b/src/Recyclarr.Tests/Command/Initialization/ServiceInitializationAndCleanupTest.cs deleted file mode 100644 index 827b61d1..00000000 --- a/src/Recyclarr.Tests/Command/Initialization/ServiceInitializationAndCleanupTest.cs +++ /dev/null @@ -1,49 +0,0 @@ -using FluentAssertions; -using NSubstitute; -using NUnit.Framework; -using Recyclarr.Command; -using Recyclarr.Command.Initialization; -using Recyclarr.Command.Initialization.Cleanup; -using Recyclarr.Command.Initialization.Init; -using TestLibrary.AutoFixture; - -namespace Recyclarr.Tests.Command.Initialization; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class ServiceInitializationAndCleanupTest -{ - [Test, AutoMockData] - public async Task Cleanup_happens_when_exception_occurs_in_action( - ServiceCommand cmd, - IServiceCleaner cleaner) - { - var sut = new ServiceInitializationAndCleanup( - Enumerable.Empty().OrderBy(_ => 1), - new[] {cleaner}.OrderBy(_ => 1)); - - var act = () => sut.Execute(cmd, () => throw new NullReferenceException()); - - await act.Should().ThrowAsync(); - cleaner.Received().Cleanup(); - } - - [Test, AutoMockData] - public async Task Cleanup_happens_when_exception_occurs_in_init( - ServiceCommand cmd, - IServiceInitializer init, - IServiceCleaner cleaner) - { - var sut = new ServiceInitializationAndCleanup( - new[] {init}.OrderBy(_ => 1), - new[] {cleaner}.OrderBy(_ => 1)); - - init.WhenForAnyArgs(x => x.Initialize(default!)) - .Do(_ => throw new NullReferenceException()); - - var act = () => sut.Execute(cmd, () => Task.CompletedTask); - - await act.Should().ThrowAsync(); - cleaner.Received().Cleanup(); - } -} diff --git a/src/Recyclarr.Tests/Command/Services/SonarrServiceTest.cs b/src/Recyclarr.Tests/Command/SonarrCommandTest.cs similarity index 51% rename from src/Recyclarr.Tests/Command/Services/SonarrServiceTest.cs rename to src/Recyclarr.Tests/Command/SonarrCommandTest.cs index 6dbcc0c8..a937b745 100644 --- a/src/Recyclarr.Tests/Command/Services/SonarrServiceTest.cs +++ b/src/Recyclarr.Tests/Command/SonarrCommandTest.cs @@ -1,45 +1,45 @@ using AutoFixture.NUnit3; using CliFx.Exceptions; +using CliFx.Infrastructure; using FluentAssertions; using NSubstitute; using NUnit.Framework; using Recyclarr.Command; -using Recyclarr.Command.Services; using TestLibrary.AutoFixture; using TrashLib.Sonarr; -namespace Recyclarr.Tests.Command.Services; +namespace Recyclarr.Tests.Command; [TestFixture] -[Parallelizable(ParallelScope.All)] -public class SonarrServiceTest +// Cannot be parallelized due to static CompositionRoot property +public class SonarrCommandTest { [Test, AutoMockData] public async Task List_terms_without_value_fails( - [Frozen] ISonarrCommand cmd, - SonarrService sut) + IConsole console, + SonarrCommand sut) { - cmd.ListReleaseProfiles.Returns(false); + sut.ListReleaseProfiles = false; // When `--list-terms` is specified on the command line without a value, it gets a `null` value assigned. - cmd.ListTerms.Returns((string?) null); + sut.ListTerms = null; - var act = () => sut.Execute(cmd); + var act = async () => await sut.ExecuteAsync(console); await act.Should().ThrowAsync(); } [Test, AutoMockData] public async Task List_terms_with_empty_value_fails( - [Frozen] ISonarrCommand cmd, - SonarrService sut) + IConsole console, + SonarrCommand sut) { - cmd.ListReleaseProfiles.Returns(false); + sut.ListReleaseProfiles = false; // If the user specifies a blank string as the value, it should still fail. - cmd.ListTerms.Returns(""); + sut.ListTerms = ""; - var act = () => sut.Execute(cmd); + var act = async () => await sut.ExecuteAsync(console); await act.Should().ThrowAsync(); } @@ -47,14 +47,16 @@ public class SonarrServiceTest [Test, AutoMockData] public async Task List_terms_uses_specified_trash_id( [Frozen] IReleaseProfileLister lister, - [Frozen] ISonarrCommand cmd, - SonarrService sut) + IConsole console, + ICompositionRoot compositionRoot, + SonarrCommand sut) { - cmd.ListReleaseProfiles.Returns(false); + BaseCommand.CompositionRoot = compositionRoot; + sut.ListReleaseProfiles = false; - cmd.ListTerms.Returns("some_id"); + sut.ListTerms = "some_id"; - await sut.Execute(cmd); + await sut.ExecuteAsync(console); lister.Received().ListTerms("some_id"); } @@ -62,13 +64,16 @@ public class SonarrServiceTest [Test, AutoMockData] public async Task List_release_profiles_is_invoked( [Frozen] IReleaseProfileLister lister, - [Frozen] ISonarrCommand cmd, - SonarrService sut) + IConsole console, + ICompositionRoot compositionRoot, + SonarrCommand sut) { - cmd.ListReleaseProfiles.Returns(true); - cmd.ListTerms.Returns((string?) null); + BaseCommand.CompositionRoot = compositionRoot; - await sut.Execute(cmd); + sut.ListReleaseProfiles = true; + sut.ListTerms = null; + + await sut.ExecuteAsync(console); lister.Received().ListReleaseProfiles(); } diff --git a/src/Recyclarr.Tests/CompositionRootTest.cs b/src/Recyclarr.Tests/CompositionRootTest.cs index 4fbb8c39..3ed293d9 100644 --- a/src/Recyclarr.Tests/CompositionRootTest.cs +++ b/src/Recyclarr.Tests/CompositionRootTest.cs @@ -2,7 +2,9 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Autofac; using Autofac.Core; +using CliFx.Infrastructure; using FluentAssertions; +using NSubstitute; using NUnit.Framework; using VersionControl; @@ -33,7 +35,7 @@ public class CompositionRootTest { var act = () => { - using var container = CompositionRoot.Setup(); + using var container = new CompositionRoot().Setup("", Substitute.For(), default).Container; service.Instantiate(container); }; @@ -47,11 +49,11 @@ public class CompositionRootTest )] private sealed class ConcreteTypeEnumerator : IEnumerable { - private readonly IContainer _container; + private readonly ILifetimeScope _container; public ConcreteTypeEnumerator() { - _container = CompositionRoot.Setup(); + _container = new CompositionRoot().Setup("", Substitute.For(), default).Container; } public IEnumerator GetEnumerator() @@ -70,7 +72,7 @@ public class CompositionRootTest [TestCaseSource(typeof(ConcreteTypeEnumerator))] public void Service_should_be_instantiable(Type service) { - using var container = CompositionRoot.Setup(); + using var container = new CompositionRoot().Setup("", Substitute.For(), default).Container; container.Invoking(c => c.Resolve(service)) .Should().NotThrow() .And.NotBeNull(); diff --git a/src/Recyclarr.Tests/ConfigurationFinderTest.cs b/src/Recyclarr.Tests/ConfigurationFinderTest.cs index 58b419a8..b6a6694e 100644 --- a/src/Recyclarr.Tests/ConfigurationFinderTest.cs +++ b/src/Recyclarr.Tests/ConfigurationFinderTest.cs @@ -1,11 +1,13 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.Extensions; using System.IO.Abstractions.TestingHelpers; using AutoFixture.NUnit3; using Common; using FluentAssertions; using NSubstitute; using NUnit.Framework; +using Recyclarr.TestLibrary; using TestLibrary.AutoFixture; -using TrashLib; namespace Recyclarr.Tests; @@ -17,56 +19,48 @@ public class ConfigurationFinderTest public void Return_path_next_to_executable_if_present( [Frozen] IAppContext appContext, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - [Frozen] IAppPaths paths, ConfigurationFinder sut) { - var basePath = fs.Path.Combine("base", "path"); - var baseYaml = fs.Path.Combine(basePath, "recyclarr.yml"); + var basePath = fs.CurrentDirectory().SubDirectory("base").SubDirectory("path"); + var baseYaml = basePath.File("recyclarr.yml"); - paths.DefaultConfigFilename.Returns("recyclarr.yml"); - appContext.BaseDirectory.Returns(basePath); - fs.AddFile(baseYaml, new MockFileData("")); + appContext.BaseDirectory.Returns(basePath.FullName); + fs.AddFile(baseYaml.FullName, new MockFileData("")); var path = sut.FindConfigPath(); - path.Should().EndWith(baseYaml); + path.FullName.Should().Be(baseYaml.FullName); } [Test, AutoMockData] public void Return_app_data_dir_location_if_base_directory_location_not_present( [Frozen] IAppContext appContext, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - [Frozen] IAppPaths paths, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, ConfigurationFinder sut) { - var appYaml = fs.Path.Combine("app", "data", "recyclarr.yml"); - - paths.ConfigPath.Returns(appYaml); - var path = sut.FindConfigPath(); - path.Should().EndWith(appYaml); + path.FullName.Should().Be(paths.ConfigPath.FullName); } [Test, AutoMockData] public void Return_base_directory_location_if_both_files_are_present( [Frozen] IAppContext appContext, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - [Frozen] IAppPaths paths, ConfigurationFinder sut) { - var appPath = fs.Path.Combine("app", "data"); - var basePath = fs.Path.Combine("base", "path"); - var baseYaml = fs.Path.Combine(basePath, "recyclarr.yml"); - var appYaml = fs.Path.Combine(appPath, "recyclarr.yml"); + var appPath = fs.CurrentDirectory().SubDirectory("app").SubDirectory("data"); + var basePath = fs.CurrentDirectory().SubDirectory("base").SubDirectory("path"); + var baseYaml = basePath.File("recyclarr.yml"); + var appYaml = appPath.File("recyclarr.yml"); - paths.DefaultConfigFilename.Returns("recyclarr.yml"); - appContext.BaseDirectory.Returns(basePath); - fs.AddFile(baseYaml, new MockFileData("")); - fs.AddFile(appYaml, new MockFileData("")); + appContext.BaseDirectory.Returns(basePath.FullName); + fs.AddFile(baseYaml.FullName, new MockFileData("")); + fs.AddFile(appYaml.FullName, new MockFileData("")); var path = sut.FindConfigPath(); - path.Should().EndWith(baseYaml); + path.FullName.Should().Be(baseYaml.FullName); } } diff --git a/src/Recyclarr.Tests/LogJanitorTest.cs b/src/Recyclarr.Tests/LogJanitorTest.cs index a5b847c3..c320506a 100644 --- a/src/Recyclarr.Tests/LogJanitorTest.cs +++ b/src/Recyclarr.Tests/LogJanitorTest.cs @@ -1,12 +1,12 @@ +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using AutoFixture.NUnit3; using FluentAssertions; using MoreLinq.Extensions; -using NSubstitute; using NUnit.Framework; using Recyclarr.Logging; +using Recyclarr.TestLibrary; using TestLibrary.AutoFixture; -using TrashLib; namespace Recyclarr.Tests; @@ -16,26 +16,24 @@ public class LogJanitorTest { [Test, AutoMockData] public void Keep_correct_number_of_newest_log_files( - [Frozen] IAppPaths paths, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, LogJanitor janitor) { - const string logDir = "C:\\logs"; - paths.LogDirectory.Returns(logDir); - var testFiles = new[] { - $"{logDir}\\trash_2021-05-15_19-00-00", - $"{logDir}\\trash_2021-05-15_20-00-00", - $"{logDir}\\trash_2021-05-15_21-00-00", - $"{logDir}\\trash_2021-05-15_22-00-00" + paths.LogDirectory.File("trash_2021-05-15_19-00-00.log"), + paths.LogDirectory.File("trash_2021-05-15_20-00-00.log"), + paths.LogDirectory.File("trash_2021-05-15_21-00-00.log"), + paths.LogDirectory.File("trash_2021-05-15_22-00-00.log") }; - testFiles.ForEach(x => fs.AddFile(x, new MockFileData(""))); + testFiles.ForEach(x => fs.AddFile(x.FullName, new MockFileData(""))); janitor.DeleteOldestLogFiles(2); - fs.FileExists(testFiles[2]).Should().BeTrue(); - fs.FileExists(testFiles[3]).Should().BeTrue(); + fs.AllFiles.Should().BeEquivalentTo( + testFiles[2].FullName, + testFiles[3].FullName); } } diff --git a/src/Recyclarr.Tests/Logging/DelayedFileSinkTest.cs b/src/Recyclarr.Tests/Logging/DelayedFileSinkTest.cs deleted file mode 100644 index d62edbf1..00000000 --- a/src/Recyclarr.Tests/Logging/DelayedFileSinkTest.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO.Abstractions; -using AutoFixture.NUnit3; -using NSubstitute; -using NUnit.Framework; -using Recyclarr.Logging; -using Serilog.Events; -using TestLibrary.AutoFixture; -using TrashLib; - -namespace Recyclarr.Tests.Logging; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class DelayedFileSinkTest -{ - [Test, AutoMockData] - public void Should_not_open_file_if_app_data_invalid( - [Frozen] IAppPaths paths, - [Frozen] IFileSystem fs, - LogEvent logEvent, - DelayedFileSink sut) - { - paths.IsAppDataPathValid.Returns(false); - - sut.Emit(logEvent); - - fs.File.DidNotReceiveWithAnyArgs().OpenWrite(default!); - } -} diff --git a/src/Recyclarr.Tests/Migration/MigrationExecutorTest.cs b/src/Recyclarr.Tests/Migration/MigrationExecutorTest.cs index c6b0f6a9..19347c13 100644 --- a/src/Recyclarr.Tests/Migration/MigrationExecutorTest.cs +++ b/src/Recyclarr.Tests/Migration/MigrationExecutorTest.cs @@ -1,4 +1,3 @@ -using Autofac; using CliFx.Infrastructure; using FluentAssertions; using NSubstitute; @@ -15,7 +14,7 @@ public class MigrationExecutorTest [Test] public void Migration_steps_are_in_expected_order() { - var container = CompositionRoot.Setup(); + var container = new CompositionRoot().Setup("", Substitute.For(), default); var steps = container.Resolve>(); var orderedSteps = steps.OrderBy(x => x.Order).Select(x => x.GetType()).ToList(); orderedSteps.Should().BeEquivalentTo( diff --git a/src/Recyclarr.Tests/Migration/Steps/MigrateTrashUpdaterAppDataDirTest.cs b/src/Recyclarr.Tests/Migration/Steps/MigrateTrashUpdaterAppDataDirTest.cs index b9a247fe..ae873e76 100644 --- a/src/Recyclarr.Tests/Migration/Steps/MigrateTrashUpdaterAppDataDirTest.cs +++ b/src/Recyclarr.Tests/Migration/Steps/MigrateTrashUpdaterAppDataDirTest.cs @@ -1,8 +1,10 @@ +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using AutoFixture.NUnit3; using FluentAssertions; using NUnit.Framework; using Recyclarr.Migration.Steps; +using Recyclarr.TestLibrary; using TestLibrary; using TestLibrary.AutoFixture; @@ -14,17 +16,15 @@ public class MigrateTrashUpdaterAppDataDirTest { [Test, AutoMockData] public void Migration_check_returns_true_if_trash_updater_dir_exists( - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, MigrateTrashUpdaterAppDataDir sut) { - fs.AddDirectory(fs.Path.Combine(paths.BasePath, "trash-updater")); + paths.AppDataDirectory.Parent.SubDirectory("trash-updater").Create(); sut.CheckIfNeeded().Should().BeTrue(); } [Test, AutoMockData] public void Migration_check_returns_false_if_trash_updater_dir_doesnt_exists( - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, MigrateTrashUpdaterAppDataDir sut) { sut.CheckIfNeeded().Should().BeFalse(); @@ -36,8 +36,8 @@ public class MigrateTrashUpdaterAppDataDirTest [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, MigrateTrashUpdaterAppDataDir sut) { - fs.AddFileNoData($"{paths.BasePath}/trash-updater/recyclarr.yml"); - fs.AddFileNoData($"{paths.BasePath}/recyclarr/recyclarr.yml"); + fs.AddFileNoData(sut.OldPath.File("recyclarr.yml")); + fs.AddFileNoData(sut.NewPath.File("recyclarr.yml")); var act = () => sut.Execute(null); @@ -51,20 +51,23 @@ public class MigrateTrashUpdaterAppDataDirTest MigrateTrashUpdaterAppDataDir sut) { // Add file instead of directory since the migration step only operates on files - fs.AddFileNoData($"{paths.BasePath}/trash-updater/settings.yml"); - fs.AddFileNoData($"{paths.BasePath}/trash-updater/recyclarr.yml"); - fs.AddFileNoData($"{paths.BasePath}/trash-updater/this-gets-ignored.yml"); - fs.AddDirectory2($"{paths.BasePath}/trash-updater/repo"); - fs.AddDirectory2($"{paths.BasePath}/trash-updater/cache"); - fs.AddFileNoData($"{paths.BasePath}/trash-updater/cache/sonarr/test.txt"); + var baseDir = sut.OldPath; + fs.AddFileNoData(baseDir.File("settings.yml")); + fs.AddFileNoData(baseDir.File("recyclarr.yml")); + fs.AddFileNoData(baseDir.File("this-gets-ignored.yml")); + fs.AddDirectory(baseDir.SubDirectory("repo")); + fs.AddDirectory(baseDir.SubDirectory("cache")); + fs.AddFileNoData(baseDir.File("cache/sonarr/test.txt")); sut.Execute(null); + var expectedBase = sut.NewPath; + fs.AllDirectories.Should().NotContain(x => x.Contains("trash-updater")); fs.AllFiles.Should().BeEquivalentTo( - FileUtils.NormalizePath($"/{paths.BasePath}/recyclarr/settings.yml"), - FileUtils.NormalizePath($"/{paths.BasePath}/recyclarr/recyclarr.yml"), - FileUtils.NormalizePath($"/{paths.BasePath}/recyclarr/cache/sonarr/test.txt")); + expectedBase.File("settings.yml").FullName, + expectedBase.File("recyclarr.yml").FullName, + expectedBase.SubDirectory("cache").SubDirectory("sonarr").File("test.txt").FullName); } [Test, AutoMockData] diff --git a/src/Recyclarr.Tests/Migration/Steps/TestAppPaths.cs b/src/Recyclarr.Tests/Migration/Steps/TestAppPaths.cs deleted file mode 100644 index 1570528f..00000000 --- a/src/Recyclarr.Tests/Migration/Steps/TestAppPaths.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.IO.Abstractions; - -namespace Recyclarr.Tests.Migration.Steps; - -public class TestAppPaths : AppPaths -{ - public string BasePath { get; } - - public TestAppPaths(IFileSystem fs) - : base(fs) - { - BasePath = fs.Path.Combine("base", "path"); - SetAppDataPath(fs.Path.Combine(BasePath, DefaultAppDataDirectoryName)); - } -} diff --git a/src/Recyclarr.Tests/Recyclarr.Tests.csproj b/src/Recyclarr.Tests/Recyclarr.Tests.csproj index f81bd4a7..eb6f2a82 100644 --- a/src/Recyclarr.Tests/Recyclarr.Tests.csproj +++ b/src/Recyclarr.Tests/Recyclarr.Tests.csproj @@ -4,6 +4,7 @@ + diff --git a/src/Recyclarr.sln b/src/Recyclarr.sln index 277822e6..a4cd94ea 100644 --- a/src/Recyclarr.sln +++ b/src/Recyclarr.sln @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VersionControl", "VersionCo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VersionControl.Tests", "VersionControl.Tests\VersionControl.Tests.csproj", "{F81C7EA3-4ACA-4171-8A60-531F129A33C5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.TestLibrary", "Recyclarr.TestLibrary\Recyclarr.TestLibrary.csproj", "{77D1C695-94D4-46A9-8F12-41E54AF97750}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,10 @@ Global {F81C7EA3-4ACA-4171-8A60-531F129A33C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {F81C7EA3-4ACA-4171-8A60-531F129A33C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {F81C7EA3-4ACA-4171-8A60-531F129A33C5}.Release|Any CPU.Build.0 = Release|Any CPU + {77D1C695-94D4-46A9-8F12-41E54AF97750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77D1C695-94D4-46A9-8F12-41E54AF97750}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77D1C695-94D4-46A9-8F12-41E54AF97750}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77D1C695-94D4-46A9-8F12-41E54AF97750}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution EndGlobalSection diff --git a/src/Recyclarr.sln.DotSettings b/src/Recyclarr.sln.DotSettings index 0907736d..8b453bb6 100644 --- a/src/Recyclarr.sln.DotSettings +++ b/src/Recyclarr.sln.DotSettings @@ -1,12 +1,6 @@  - <?xml version="1.0" encoding="utf-16"?><Profile name="Corepoint Health"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSRemoveCodeRedundancies>True</CSRemoveCodeRedundancies><CSUseVar><BehavourStyle>DISABLED</BehavourStyle><LocalVariableStyle>IMPLICIT_WHEN_INITIALIZER_HAS_TYPE</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSUpdateFileHeader>False</CSUpdateFileHeader><VBOptimizeImports>False</VBOptimizeImports><VBShortenReferences>False</VBShortenReferences><XMLReformatCode>False</XMLReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><VBReformatCode>False</VBReformatCode><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="False" AddMissingParentheses="False" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="False" ArrangeCodeBodyStyle="False" ArrangeVarStyle="True" /><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSShortenReferences>True</CSShortenReferences><IDEA_SETTINGS>&lt;profile version="1.0"&gt; - &lt;option name="myName" value="Corepoint Health" /&gt; -&lt;/profile&gt;</IDEA_SETTINGS></Profile> - - True - - <?xml version="1.0" encoding="utf-16"?><Profile name="TrashUpdaterCleanup"><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeArgumentsStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" /><Xaml.RemoveRedundantNamespaceAlias>True</Xaml.RemoveRedundantNamespaceAlias><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CppAddOverrideSpecifier>True</CppAddOverrideSpecifier><CppReplaceImportDirective>True</CppReplaceImportDirective><CppSortMemberInitializers>True</CppSortMemberInitializers><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><IDEA_SETTINGS>&lt;profile version="1.0"&gt; - &lt;option name="myName" value="TrashUpdaterCleanup" /&gt; + <?xml version="1.0" encoding="utf-16"?><Profile name="Recyclarr Cleanup"><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeArgumentsStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" /><Xaml.RemoveRedundantNamespaceAlias>True</Xaml.RemoveRedundantNamespaceAlias><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CppAddOverrideSpecifier>True</CppAddOverrideSpecifier><CppReplaceImportDirective>True</CppReplaceImportDirective><CppSortMemberInitializers>True</CppSortMemberInitializers><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="Recyclarr Cleanup" /&gt; &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="false" level="WARNING" enabled_by_default="false" /&gt; &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="false" level="WARNING" enabled_by_default="false" /&gt; &lt;inspection_tool class="JSPrimitiveTypeWrapperUsage" enabled="false" level="WARNING" enabled_by_default="false" /&gt; @@ -60,10 +54,14 @@ &lt;Reformat&gt;false&lt;/Reformat&gt; &lt;/Language&gt; &lt;/profile&gt;</RIDER_SETTINGS><HtmlReformatCode>True</HtmlReformatCode><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor></Profile> - TrashUpdaterCleanup + + True + + + Recyclarr Cleanup True True True True True - True \ No newline at end of file + True diff --git a/src/Recyclarr/AppPaths.cs b/src/Recyclarr/AppPaths.cs index f24c8da3..1d307987 100644 --- a/src/Recyclarr/AppPaths.cs +++ b/src/Recyclarr/AppPaths.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; using TrashLib; @@ -6,27 +5,19 @@ namespace Recyclarr; public class AppPaths : IAppPaths { - private readonly IFileSystem _fs; - private string? _appDataPath; - - public AppPaths(IFileSystem fs) + public AppPaths(IDirectoryInfo appDataPath) { - _fs = fs; + AppDataDirectory = appDataPath; } - public string DefaultConfigFilename => "recyclarr.yml"; - public string DefaultAppDataDirectoryName => "recyclarr"; - - public bool IsAppDataPathValid => _appDataPath is not null; - public void SetAppDataPath(string path) => _appDataPath = path; + public static string DefaultConfigFilename => "recyclarr.yml"; + public static string DefaultAppDataDirectoryName => "recyclarr"; - [SuppressMessage("Design", "CA1024:Use properties where appropriate")] - public string GetAppDataPath() - => _appDataPath ?? throw new DirectoryNotFoundException("Application data directory not set!"); + public IDirectoryInfo AppDataDirectory { get; } - public string ConfigPath => _fs.Path.Combine(GetAppDataPath(), DefaultConfigFilename); - public string SettingsPath => _fs.Path.Combine(GetAppDataPath(), "settings.yml"); - public string LogDirectory => _fs.Path.Combine(GetAppDataPath(), "logs"); - public string RepoDirectory => _fs.Path.Combine(GetAppDataPath(), "repo"); - public string CacheDirectory => _fs.Path.Combine(GetAppDataPath(), "cache"); + public IFileInfo ConfigPath => AppDataDirectory.File(DefaultConfigFilename); + public IFileInfo SettingsPath => AppDataDirectory.File("settings.yml"); + public IDirectoryInfo LogDirectory => AppDataDirectory.SubDirectory("logs"); + public IDirectoryInfo RepoDirectory => AppDataDirectory.SubDirectory("repo"); + public IDirectoryInfo CacheDirectory => AppDataDirectory.SubDirectory("cache"); } diff --git a/src/Recyclarr/Command/BaseCommand.cs b/src/Recyclarr/Command/BaseCommand.cs new file mode 100644 index 00000000..620a0048 --- /dev/null +++ b/src/Recyclarr/Command/BaseCommand.cs @@ -0,0 +1,54 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Exceptions; +using CliFx.Infrastructure; +using JetBrains.Annotations; +using Recyclarr.Logging; +using Serilog; +using Serilog.Events; +using TrashLib; + +namespace Recyclarr.Command; + +public abstract class BaseCommand : ICommand +{ + // Not explicitly defined as a Command here because for legacy reasons, different subcommands expose this in + // different ways to the user. + public abstract string? AppDataDirectory { get; set; } + + [CommandOption("debug", 'd', Description = + "Display additional logs useful for development/debug purposes.")] + public bool Debug { get; [UsedImplicitly] set; } = false; + + public static ICompositionRoot? CompositionRoot { get; set; } + + public virtual async ValueTask ExecuteAsync(IConsole console) + { + // Must happen first because everything can use the logger. + var logLevel = Debug ? LogEventLevel.Debug : LogEventLevel.Information; + + if (CompositionRoot is null) + { + throw new CommandException("CompositionRoot must not be null"); + } + + using var container = CompositionRoot.Setup(AppDataDirectory, console, logLevel); + + var paths = container.Resolve(); + var janitor = container.Resolve(); + var log = container.Resolve(); + + log.Debug("App Data Dir: {AppData}", paths.AppDataDirectory); + + // Initialize other directories used throughout the application + paths.RepoDirectory.Create(); + paths.CacheDirectory.Create(); + paths.LogDirectory.Create(); + + await Process(container); + + janitor.DeleteOldestLogFiles(20); + } + + public abstract Task Process(IServiceLocatorProxy container); +} diff --git a/src/Recyclarr/Command/CreateConfigCommand.cs b/src/Recyclarr/Command/CreateConfigCommand.cs index 14063900..c02ae6fe 100644 --- a/src/Recyclarr/Command/CreateConfigCommand.cs +++ b/src/Recyclarr/Command/CreateConfigCommand.cs @@ -1,11 +1,8 @@ using System.IO.Abstractions; -using CliFx; using CliFx.Attributes; using CliFx.Exceptions; -using CliFx.Infrastructure; using Common; using JetBrains.Annotations; -using Recyclarr.Command.Initialization; using Serilog; using TrashLib; @@ -13,44 +10,35 @@ namespace Recyclarr.Command; [Command("create-config", Description = "Create a starter YAML configuration file")] [UsedImplicitly] -public class CreateConfigCommand : ICommand +public class CreateConfigCommand : BaseCommand { - private readonly IFileSystem _fs; - private readonly IAppPaths _paths; - private readonly IDefaultAppDataSetup _appDataSetup; - private readonly ILogger _log; - - public CreateConfigCommand(ILogger logger, IFileSystem fs, IAppPaths paths, IDefaultAppDataSetup appDataSetup) - { - _log = logger; - _fs = fs; - _paths = paths; - _appDataSetup = appDataSetup; - } - [CommandOption("path", 'p', Description = "Path where the new YAML file should be created. Must include the filename (e.g. path/to/config.yml). " + "File must not already exist. If not specified, uses the default path of `recyclarr.yml` in the app data " + "directory")] - public string? Path { get; set; } + public override string? AppDataDirectory { get; set; } - public ValueTask ExecuteAsync(IConsole console) + public override async Task Process(IServiceLocatorProxy container) { - _appDataSetup.SetupDefaultPath(null, true); + var fs = container.Resolve(); + var paths = container.Resolve(); + var log = container.Resolve(); var reader = new ResourceDataReader(typeof(Program)); var ymlData = reader.ReadData("config-template.yml"); - var path = _fs.Path.GetFullPath(Path ?? _paths.ConfigPath); + var configFile = AppDataDirectory is not null + ? fs.FileInfo.FromFileName(AppDataDirectory) + : paths.ConfigPath; - if (_fs.File.Exists(path)) + if (configFile.Exists) { - throw new CommandException($"The file {path} already exists. Please choose another path or " + + throw new CommandException($"The file {configFile} already exists. Please choose another path or " + "delete/move the existing file and run this command again."); } - _fs.Directory.CreateDirectory(_fs.Path.GetDirectoryName(path)); - _fs.File.WriteAllText(path, ymlData); - _log.Information("Created configuration at: {Path}", path); - return default; + fs.Directory.CreateDirectory(configFile.DirectoryName); + await using var stream = configFile.CreateText(); + await stream.WriteAsync(ymlData); + log.Information("Created configuration at: {Path}", configFile); } } diff --git a/src/Recyclarr/Command/Helpers/CacheStoragePath.cs b/src/Recyclarr/Command/Helpers/CacheStoragePath.cs index 08ccb77a..3e6b211c 100644 --- a/src/Recyclarr/Command/Helpers/CacheStoragePath.cs +++ b/src/Recyclarr/Command/Helpers/CacheStoragePath.cs @@ -1,15 +1,20 @@ +using System.IO.Abstractions; +using TrashLib; using TrashLib.Cache; namespace Recyclarr.Command.Helpers; public class CacheStoragePath : ICacheStoragePath { + private readonly IAppPaths _paths; private readonly IActiveServiceCommandProvider _serviceCommandProvider; - public CacheStoragePath(IActiveServiceCommandProvider serviceCommandProvider) + public CacheStoragePath(IAppPaths paths, IActiveServiceCommandProvider serviceCommandProvider) { + _paths = paths; _serviceCommandProvider = serviceCommandProvider; } - public string Path => _serviceCommandProvider.ActiveCommand.CacheStoragePath; + public string Path => _paths.CacheDirectory + .SubDirectory(_serviceCommandProvider.ActiveCommand.Name.ToLower()).FullName; } diff --git a/src/Recyclarr/Command/Helpers/CliTypeActivator.cs b/src/Recyclarr/Command/Helpers/CliTypeActivator.cs deleted file mode 100644 index fc4a2384..00000000 --- a/src/Recyclarr/Command/Helpers/CliTypeActivator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Autofac; - -namespace Recyclarr.Command.Helpers; - -internal static class CliTypeActivator -{ - public static object ResolveType(IContainer container, Type typeToResolve) - { - var instance = container.Resolve(typeToResolve); - if (instance.GetType().IsAssignableTo()) - { - var activeServiceProvider = container.Resolve(); - activeServiceProvider.ActiveCommand = (IServiceCommand) instance; - } - - return instance; - } -} diff --git a/src/Recyclarr/Command/IRadarrCommand.cs b/src/Recyclarr/Command/IRadarrCommand.cs deleted file mode 100644 index 81cfb493..00000000 --- a/src/Recyclarr/Command/IRadarrCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Recyclarr.Command; - -public interface IRadarrCommand : IServiceCommand -{ - bool ListCustomFormats { get; } -} diff --git a/src/Recyclarr/Command/IServiceCommand.cs b/src/Recyclarr/Command/IServiceCommand.cs index c3c5ffa0..3ca67b4d 100644 --- a/src/Recyclarr/Command/IServiceCommand.cs +++ b/src/Recyclarr/Command/IServiceCommand.cs @@ -5,6 +5,5 @@ public interface IServiceCommand bool Preview { get; } bool Debug { get; } ICollection Config { get; } - string CacheStoragePath { get; } string Name { get; } } diff --git a/src/Recyclarr/Command/ISonarrCommand.cs b/src/Recyclarr/Command/ISonarrCommand.cs deleted file mode 100644 index f2b1b731..00000000 --- a/src/Recyclarr/Command/ISonarrCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Recyclarr.Command; - -public interface ISonarrCommand : IServiceCommand -{ - bool ListReleaseProfiles { get; } - string? ListTerms { get; } -} diff --git a/src/Recyclarr/Command/Initialization/Cleanup/IServiceCleaner.cs b/src/Recyclarr/Command/Initialization/Cleanup/IServiceCleaner.cs deleted file mode 100644 index 2d260a21..00000000 --- a/src/Recyclarr/Command/Initialization/Cleanup/IServiceCleaner.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Recyclarr.Command.Initialization.Cleanup; - -public interface IServiceCleaner -{ - void Cleanup(); -} diff --git a/src/Recyclarr/Command/Initialization/Cleanup/OldLogFileCleaner.cs b/src/Recyclarr/Command/Initialization/Cleanup/OldLogFileCleaner.cs deleted file mode 100644 index 5e6a9b6d..00000000 --- a/src/Recyclarr/Command/Initialization/Cleanup/OldLogFileCleaner.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Recyclarr.Logging; - -namespace Recyclarr.Command.Initialization.Cleanup; - -internal class OldLogFileCleaner : IServiceCleaner -{ - private readonly ILogJanitor _janitor; - - public OldLogFileCleaner(ILogJanitor janitor) - { - _janitor = janitor; - } - - public void Cleanup() - { - _janitor.DeleteOldestLogFiles(20); - } -} diff --git a/src/Recyclarr/Command/Initialization/DefaultAppDataSetup.cs b/src/Recyclarr/Command/Initialization/DefaultAppDataSetup.cs index 2f4e476f..a26706c9 100644 --- a/src/Recyclarr/Command/Initialization/DefaultAppDataSetup.cs +++ b/src/Recyclarr/Command/Initialization/DefaultAppDataSetup.cs @@ -1,64 +1,58 @@ using System.IO.Abstractions; using CliFx.Exceptions; using Common; -using Serilog; using TrashLib; namespace Recyclarr.Command.Initialization; -public class DefaultAppDataSetup : IDefaultAppDataSetup +public class DefaultAppDataSetup { private readonly IEnvironment _env; - private readonly IAppPaths _paths; private readonly IFileSystem _fs; - private readonly ILogger _log; - public DefaultAppDataSetup(IEnvironment env, IAppPaths paths, IFileSystem fs, ILogger log) + public DefaultAppDataSetup(IEnvironment env, IFileSystem fs) { _env = env; - _paths = paths; _fs = fs; - _log = log; } - public void SetupDefaultPath(string? appDataDirectoryOverride, bool forceCreate) + public IAppPaths CreateAppPaths(string? appDataDirectoryOverride = null, bool forceCreate = true) + { + var appDir = GetAppDataDirectory(appDataDirectoryOverride, forceCreate); + return new AppPaths(_fs.DirectoryInfo.FromDirectoryName(appDir)); + } + + private string GetAppDataDirectory(string? appDataDirectoryOverride, bool forceCreate) { // If a specific app data directory is not provided, use the following environment variable to find the path. appDataDirectoryOverride ??= _env.GetEnvironmentVariable("RECYCLARR_APP_DATA"); - // If the user did not explicitly specify an app data directory, perform some system introspection to verify if - // the user has a home directory. - if (string.IsNullOrEmpty(appDataDirectoryOverride)) + // Ensure user-specified app data directory is created and use it. + if (!string.IsNullOrEmpty(appDataDirectoryOverride)) { - // If we can't even get the $HOME directory value, throw an exception. User must explicitly specify it with - // --app-data. - var home = _env.GetFolderPath(Environment.SpecialFolder.UserProfile); - if (string.IsNullOrEmpty(home)) - { - throw new CommandException( - "The system does not have a HOME directory, so the application cannot determine where to place " + - "data files. Please use the --app-data option to explicitly set a location for these files."); - } + return _fs.Directory.CreateDirectory(appDataDirectoryOverride).FullName; + } - // Set app data path to application directory value (e.g. `$HOME/.config` on Linux) and ensure it is - // created. - var appData = _env.GetFolderPath(Environment.SpecialFolder.ApplicationData, - forceCreate ? Environment.SpecialFolderOption.Create : Environment.SpecialFolderOption.None); + // If we can't even get the $HOME directory value, throw an exception. User must explicitly specify it with + // --app-data. + var home = _env.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrEmpty(home)) + { + throw new CommandException( + "The system does not have a HOME directory, so the application cannot determine where to place " + + "data files. Please use the --app-data option to explicitly set a location for these files."); + } - if (string.IsNullOrEmpty(appData)) - { - throw new DirectoryNotFoundException("Unable to find the default app data directory"); - } + // Set app data path to application directory value (e.g. `$HOME/.config` on Linux) and ensure it is + // created. + var appData = _env.GetFolderPath(Environment.SpecialFolder.ApplicationData, + forceCreate ? Environment.SpecialFolderOption.Create : Environment.SpecialFolderOption.None); - _paths.SetAppDataPath(_fs.Path.Combine(appData, _paths.DefaultAppDataDirectoryName)); - } - else + if (string.IsNullOrEmpty(appData)) { - // Ensure user-specified app data directory is created and use it. - var dir = _fs.Directory.CreateDirectory(appDataDirectoryOverride); - _paths.SetAppDataPath(dir.FullName); + throw new DirectoryNotFoundException("Unable to find the default app data directory"); } - _log.Debug("App Data Dir: {AppData}", _paths.GetAppDataPath()); + return _fs.Path.Combine(appData, AppPaths.DefaultAppDataDirectoryName); } } diff --git a/src/Recyclarr/Command/Initialization/IDefaultAppDataSetup.cs b/src/Recyclarr/Command/Initialization/IDefaultAppDataSetup.cs deleted file mode 100644 index f6714ce0..00000000 --- a/src/Recyclarr/Command/Initialization/IDefaultAppDataSetup.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Recyclarr.Command.Initialization; - -public interface IDefaultAppDataSetup -{ - void SetupDefaultPath(string? appDataDirectoryOverride, bool forceCreate); -} diff --git a/src/Recyclarr/Command/Initialization/IServiceInitializationAndCleanup.cs b/src/Recyclarr/Command/Initialization/IServiceInitializationAndCleanup.cs deleted file mode 100644 index c5e93916..00000000 --- a/src/Recyclarr/Command/Initialization/IServiceInitializationAndCleanup.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Recyclarr.Command.Initialization; - -public interface IServiceInitializationAndCleanup -{ - Task Execute(ServiceCommand cmd, Func logic); -} diff --git a/src/Recyclarr/Command/Initialization/Init/CheckMigrationNeeded.cs b/src/Recyclarr/Command/Initialization/Init/CheckMigrationNeeded.cs deleted file mode 100644 index b47d3e18..00000000 --- a/src/Recyclarr/Command/Initialization/Init/CheckMigrationNeeded.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Recyclarr.Migration; - -namespace Recyclarr.Command.Initialization.Init; - -internal class CheckMigrationNeeded : IServiceInitializer -{ - private readonly IMigrationExecutor _migration; - - public CheckMigrationNeeded(IMigrationExecutor migration) - { - _migration = migration; - } - - public void Initialize(ServiceCommand cmd) - { - _migration.CheckNeededMigrations(); - } -} diff --git a/src/Recyclarr/Command/Initialization/Init/IServiceInitializer.cs b/src/Recyclarr/Command/Initialization/Init/IServiceInitializer.cs deleted file mode 100644 index 2371d654..00000000 --- a/src/Recyclarr/Command/Initialization/Init/IServiceInitializer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Recyclarr.Command.Initialization.Init; - -public interface IServiceInitializer -{ - void Initialize(ServiceCommand cmd); -} diff --git a/src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs b/src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs deleted file mode 100644 index b435f978..00000000 --- a/src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.IO.Abstractions; -using TrashLib; - -namespace Recyclarr.Command.Initialization.Init; - -public class InitializeAppDataPath : IServiceInitializer -{ - private readonly IFileSystem _fs; - private readonly IAppPaths _paths; - private readonly IDefaultAppDataSetup _appDataSetup; - - public InitializeAppDataPath(IFileSystem fs, IAppPaths paths, IDefaultAppDataSetup appDataSetup) - { - _fs = fs; - _paths = paths; - _appDataSetup = appDataSetup; - } - - public void Initialize(ServiceCommand cmd) - { - _appDataSetup.SetupDefaultPath(cmd.AppDataDirectory, true); - - // Initialize other directories used throughout the application - _fs.Directory.CreateDirectory(_paths.RepoDirectory); - _fs.Directory.CreateDirectory(_paths.CacheDirectory); - _fs.Directory.CreateDirectory(_paths.LogDirectory); - } -} diff --git a/src/Recyclarr/Command/Initialization/Init/ServiceInitializer.cs b/src/Recyclarr/Command/Initialization/Init/ServiceInitializer.cs deleted file mode 100644 index 7122afd8..00000000 --- a/src/Recyclarr/Command/Initialization/Init/ServiceInitializer.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Common.Networking; -using Flurl.Http; -using Flurl.Http.Configuration; -using Newtonsoft.Json; -using Serilog; -using Serilog.Core; -using Serilog.Events; -using TrashLib; -using TrashLib.Config.Settings; -using TrashLib.Extensions; -using TrashLib.Repo; - -namespace Recyclarr.Command.Initialization.Init; - -internal class ServiceInitializer : IServiceInitializer -{ - private readonly ILogger _log; - private readonly LoggingLevelSwitch _loggingLevelSwitch; - private readonly ISettingsPersister _settingsPersister; - private readonly ISettingsProvider _settingsProvider; - private readonly IRepoUpdater _repoUpdater; - private readonly IConfigurationFinder _configFinder; - - public ServiceInitializer( - ILogger log, - LoggingLevelSwitch loggingLevelSwitch, - ISettingsPersister settingsPersister, - ISettingsProvider settingsProvider, - IRepoUpdater repoUpdater, - IConfigurationFinder configFinder) - { - _log = log; - _loggingLevelSwitch = loggingLevelSwitch; - _settingsPersister = settingsPersister; - _settingsProvider = settingsProvider; - _repoUpdater = repoUpdater; - _configFinder = configFinder; - } - - public void Initialize(ServiceCommand cmd) - { - // Must happen first because everything can use the logger. - _loggingLevelSwitch.MinimumLevel = cmd.Debug ? LogEventLevel.Debug : LogEventLevel.Information; - - // Has to happen right after logging because stuff below may use settings. - _settingsPersister.Load(); - - SetupHttp(); - _repoUpdater.UpdateRepo(); - - if (!cmd.Config.Any()) - { - cmd.Config = new[] {_configFinder.FindConfigPath()}; - } - } - - private void SetupHttp() - { - FlurlHttp.Configure(settings => - { - var jsonSettings = new JsonSerializerSettings - { - // This is important. If any DTOs are missing members, say, if Radarr or Sonarr adds one in a future - // version, this needs to fail to indicate that a software change is required. Otherwise, we lose - // state between when we request settings, and re-apply them again with a few properties modified. - MissingMemberHandling = MissingMemberHandling.Error, - - // This makes sure that null properties, such as maxSize and preferredSize in Radarr - // Quality Definitions, do not get written out to JSON request bodies. - NullValueHandling = NullValueHandling.Ignore - }; - - settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings); - FlurlLogging.SetupLogging(settings, _log); - - if (!_settingsProvider.Settings.EnableSslCertificateValidation) - { - _log.Warning( - "Security Risk: Certificate validation is being DISABLED because setting `enable_ssl_certificate_validation` is set to `false`"); - settings.HttpClientFactory = new UntrustedCertClientFactory(); - } - }); - } -} diff --git a/src/Recyclarr/Command/Initialization/InitializationAutofacModule.cs b/src/Recyclarr/Command/Initialization/InitializationAutofacModule.cs deleted file mode 100644 index 6f6bf87b..00000000 --- a/src/Recyclarr/Command/Initialization/InitializationAutofacModule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Autofac; -using Autofac.Extras.Ordering; -using Recyclarr.Command.Initialization.Cleanup; -using Recyclarr.Command.Initialization.Init; - -namespace Recyclarr.Command.Initialization; - -public class InitializationAutofacModule : Module -{ - protected override void Load(ContainerBuilder builder) - { - base.Load(builder); - builder.RegisterType().As(); - builder.RegisterType().As(); - - // Initialization Services - builder.RegisterTypes( - typeof(InitializeAppDataPath), - typeof(CheckMigrationNeeded), - typeof(ServiceInitializer)) - .As() - .OrderByRegistration(); - - // Cleanup Services - builder.RegisterTypes( - typeof(OldLogFileCleaner)) - .As() - .OrderByRegistration(); - } -} diff --git a/src/Recyclarr/Command/Initialization/ServiceInitializationAndCleanup.cs b/src/Recyclarr/Command/Initialization/ServiceInitializationAndCleanup.cs deleted file mode 100644 index 6b9c29db..00000000 --- a/src/Recyclarr/Command/Initialization/ServiceInitializationAndCleanup.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MoreLinq.Extensions; -using Recyclarr.Command.Initialization.Cleanup; -using Recyclarr.Command.Initialization.Init; - -namespace Recyclarr.Command.Initialization; - -public class ServiceInitializationAndCleanup : IServiceInitializationAndCleanup -{ - private readonly IOrderedEnumerable _initializers; - private readonly IOrderedEnumerable _cleaners; - - public ServiceInitializationAndCleanup( - IOrderedEnumerable initializers, - IOrderedEnumerable cleaners) - { - _initializers = initializers; - _cleaners = cleaners; - } - - public async Task Execute(ServiceCommand cmd, Func logic) - { - try - { - _initializers.ForEach(x => x.Initialize(cmd)); - - await logic(); - } - finally - { - _cleaners.ForEach(x => x.Cleanup()); - } - } -} diff --git a/src/Recyclarr/Command/MigrateCommand.cs b/src/Recyclarr/Command/MigrateCommand.cs index 251e17be..7159e3a7 100644 --- a/src/Recyclarr/Command/MigrateCommand.cs +++ b/src/Recyclarr/Command/MigrateCommand.cs @@ -1,43 +1,27 @@ using System.Text; -using CliFx; using CliFx.Attributes; using CliFx.Exceptions; -using CliFx.Infrastructure; using JetBrains.Annotations; -using Recyclarr.Command.Initialization; using Recyclarr.Migration; namespace Recyclarr.Command; [Command("migrate", Description = "Perform any migration steps that may be needed between versions")] [UsedImplicitly] -public class MigrateCommand : ICommand +public class MigrateCommand : BaseCommand { - private readonly IMigrationExecutor _migration; - private readonly IDefaultAppDataSetup _appDataSetup; + [CommandOption("app-data", Description = + "Explicitly specify the location of the recyclarr application data directory. " + + "Mainly for usage in Docker; not recommended for normal use.")] + public override string? AppDataDirectory { get; set; } - [CommandOption("debug", 'd', Description = - "Display additional logs useful for development/debug purposes.")] - public bool Debug { get; [UsedImplicitly] set; } = false; - - public MigrateCommand(IMigrationExecutor migration, IDefaultAppDataSetup appDataSetup) + public override Task Process(IServiceLocatorProxy container) { - _migration = migration; - _appDataSetup = appDataSetup; - } + var migration = container.Resolve(); - public ValueTask ExecuteAsync(IConsole console) - { - _appDataSetup.SetupDefaultPath(null, false); - PerformMigrations(); - return ValueTask.CompletedTask; - } - - private void PerformMigrations() - { try { - _migration.PerformAllMigrationSteps(Debug); + migration.PerformAllMigrationSteps(Debug); } catch (MigrationException e) { @@ -57,5 +41,7 @@ public class MigrateCommand : ICommand throw new CommandException(msg.ToString()); } + + return Task.CompletedTask; } } diff --git a/src/Recyclarr/Command/RadarrCommand.cs b/src/Recyclarr/Command/RadarrCommand.cs index 5a1e8d1d..5c797b5d 100644 --- a/src/Recyclarr/Command/RadarrCommand.cs +++ b/src/Recyclarr/Command/RadarrCommand.cs @@ -1,39 +1,53 @@ using CliFx.Attributes; using JetBrains.Annotations; -using Recyclarr.Command.Initialization; -using Recyclarr.Command.Services; +using Recyclarr.Config; +using Serilog; +using TrashLib.Extensions; +using TrashLib.Radarr.Config; +using TrashLib.Radarr.CustomFormat; +using TrashLib.Radarr.QualityDefinition; namespace Recyclarr.Command; [Command("radarr", Description = "Perform operations on a Radarr instance")] [UsedImplicitly] -internal class RadarrCommand : ServiceCommand, IRadarrCommand +internal class RadarrCommand : ServiceCommand { - private readonly Lazy _service; - private readonly string? _cacheStoragePath; + [CommandOption("list-custom-formats", Description = + "List available custom formats from the guide in YAML format.")] + public bool ListCustomFormats { get; [UsedImplicitly] set; } public override string Name => "Radarr"; - public sealed override string CacheStoragePath + public override async Task Process(IServiceLocatorProxy container) { - get => _cacheStoragePath ?? _service.Value.DefaultCacheStoragePath; - protected init => _cacheStoragePath = value; - } + await base.Process(container); - public RadarrCommand( - IServiceInitializationAndCleanup init, - Lazy service) - : base(init) - { - _service = service; - } + var lister = container.Resolve(); + var log = container.Resolve(); + var customFormatUpdaterFactory = container.Resolve>(); + var qualityUpdaterFactory = container.Resolve>(); + var configLoader = container.Resolve>(); - protected override async Task Process() - { - await _service.Value.Execute(this); - } + if (ListCustomFormats) + { + lister.ListCustomFormats(); + return; + } - [CommandOption("list-custom-formats", Description = - "List available custom formats from the guide in YAML format.")] - public bool ListCustomFormats { get; [UsedImplicitly] set; } + foreach (var config in configLoader.LoadMany(Config, "radarr")) + { + log.Information("Processing server {Url}", FlurlLogging.SanitizeUrl(config.BaseUrl)); + + if (config.QualityDefinition != null) + { + await qualityUpdaterFactory().Process(Preview, config); + } + + if (config.CustomFormats.Count > 0) + { + await customFormatUpdaterFactory().Process(Preview, config); + } + } + } } diff --git a/src/Recyclarr/Command/ServiceCommand.cs b/src/Recyclarr/Command/ServiceCommand.cs index a321ded0..c30dec72 100644 --- a/src/Recyclarr/Command/ServiceCommand.cs +++ b/src/Recyclarr/Command/ServiceCommand.cs @@ -1,23 +1,29 @@ -using CliFx; +using System.Text; using CliFx.Attributes; +using CliFx.Exceptions; using CliFx.Infrastructure; +using Common.Networking; +using Flurl.Http; +using Flurl.Http.Configuration; using JetBrains.Annotations; -using Recyclarr.Command.Initialization; +using Newtonsoft.Json; +using Recyclarr.Command.Helpers; +using Recyclarr.Migration; +using Serilog; +using TrashLib; +using TrashLib.Config.Settings; +using TrashLib.Extensions; +using TrashLib.Repo; +using YamlDotNet.Core; namespace Recyclarr.Command; -public abstract class ServiceCommand : ICommand, IServiceCommand +public abstract class ServiceCommand : BaseCommand, IServiceCommand { - private readonly IServiceInitializationAndCleanup _init; - [CommandOption("preview", 'p', Description = "Only display the processed markdown results without making any API calls.")] public bool Preview { get; [UsedImplicitly] set; } = false; - [CommandOption("debug", 'd', Description = - "Display additional logs useful for development/debug purposes.")] - public bool Debug { get; [UsedImplicitly] set; } = false; - [CommandOption("config", 'c', Description = "One or more YAML config files to use. All configs will be used and settings are additive. " + "If not specified, the script will look for `recyclarr.yml` in the same directory as the executable.")] @@ -26,18 +32,91 @@ public abstract class ServiceCommand : ICommand, IServiceCommand [CommandOption("app-data", Description = "Explicitly specify the location of the recyclarr application data directory. " + "Mainly for usage in Docker; not recommended for normal use.")] - public string? AppDataDirectory { get; [UsedImplicitly] set; } + public override string? AppDataDirectory { get; [UsedImplicitly] set; } - public abstract string CacheStoragePath { get; [UsedImplicitly] protected init; } public abstract string Name { get; } - protected ServiceCommand(IServiceInitializationAndCleanup init) + public sealed override async ValueTask ExecuteAsync(IConsole console) { - _init = init; + try + { + await base.ExecuteAsync(console); + } + catch (YamlException e) + { + var message = e.InnerException is not null ? e.InnerException.Message : e.Message; + var msg = new StringBuilder(); + msg.AppendLine($"Found Unrecognized YAML Property: {message}"); + msg.AppendLine("Please remove the property quoted in the above message from your YAML file"); + msg.AppendLine("Exiting due to invalid configuration"); + throw new CommandException(msg.ToString()); + } + catch (FlurlHttpException e) + { + throw new CommandException( + $"HTTP error while communicating with {Name}: {e.SanitizedExceptionMessage()}"); + } + catch (Exception e) when (e is not CommandException) + { + throw new CommandException(e.ToString()); + } } - public async ValueTask ExecuteAsync(IConsole console) - => await _init.Execute(this, Process); + public override Task Process(IServiceLocatorProxy container) + { + var log = container.Resolve(); + var settingsPersister = container.Resolve(); + var settingsProvider = container.Resolve(); + var repoUpdater = container.Resolve(); + var configFinder = container.Resolve(); + var commandProvider = container.Resolve(); + var migration = container.Resolve(); + + commandProvider.ActiveCommand = this; + + // Will throw if migration is required, otherwise just a warning is issued. + migration.CheckNeededMigrations(); + + // Stuff below may use settings. + settingsPersister.Load(); + + SetupHttp(log, settingsProvider); + repoUpdater.UpdateRepo(); + + if (!Config.Any()) + { + Config = new[] {configFinder.FindConfigPath().FullName}; + } - protected abstract Task Process(); + return Task.CompletedTask; + } + + private void SetupHttp(ILogger log, ISettingsProvider settingsProvider) + { + FlurlHttp.Configure(settings => + { + var jsonSettings = new JsonSerializerSettings + { + // This is important. If any DTOs are missing members, say, if Radarr or Sonarr adds one in a future + // version, this needs to fail to indicate that a software change is required. Otherwise, we lose + // state between when we request settings, and re-apply them again with a few properties modified. + MissingMemberHandling = MissingMemberHandling.Error, + + // This makes sure that null properties, such as maxSize and preferredSize in Radarr + // Quality Definitions, do not get written out to JSON request bodies. + NullValueHandling = NullValueHandling.Ignore + }; + + settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings); + FlurlLogging.SetupLogging(settings, log); + + if (!settingsProvider.Settings.EnableSslCertificateValidation) + { + log.Warning( + "Security Risk: Certificate validation is being DISABLED because setting " + + "`enable_ssl_certificate_validation` is set to `false`"); + settings.HttpClientFactory = new UntrustedCertClientFactory(); + } + }); + } } diff --git a/src/Recyclarr/Command/Services/RadarrService.cs b/src/Recyclarr/Command/Services/RadarrService.cs deleted file mode 100644 index d4d8bff4..00000000 --- a/src/Recyclarr/Command/Services/RadarrService.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.IO.Abstractions; -using Recyclarr.Config; -using Serilog; -using TrashLib; -using TrashLib.Extensions; -using TrashLib.Radarr.Config; -using TrashLib.Radarr.CustomFormat; -using TrashLib.Radarr.QualityDefinition; - -namespace Recyclarr.Command.Services; - -public class RadarrService : ServiceBase -{ - private readonly IConfigurationLoader _configLoader; - private readonly Func _customFormatUpdaterFactory; - private readonly ICustomFormatLister _lister; - private readonly IFileSystem _fs; - private readonly IAppPaths _paths; - private readonly ILogger _log; - private readonly Func _qualityUpdaterFactory; - - public RadarrService( - ILogger log, - IConfigurationLoader configLoader, - Func qualityUpdaterFactory, - Func customFormatUpdaterFactory, - ICustomFormatLister lister, - IFileSystem fs, - IAppPaths paths) - { - _log = log; - _configLoader = configLoader; - _qualityUpdaterFactory = qualityUpdaterFactory; - _customFormatUpdaterFactory = customFormatUpdaterFactory; - _lister = lister; - _fs = fs; - _paths = paths; - } - - public string DefaultCacheStoragePath => _fs.Path.Combine(_paths.CacheDirectory, "radarr"); - - protected override async Task Process(IRadarrCommand cmd) - { - if (cmd.ListCustomFormats) - { - _lister.ListCustomFormats(); - return; - } - - foreach (var config in _configLoader.LoadMany(cmd.Config, "radarr")) - { - _log.Information("Processing server {Url}", FlurlLogging.SanitizeUrl(config.BaseUrl)); - - if (config.QualityDefinition != null) - { - await _qualityUpdaterFactory().Process(cmd.Preview, config); - } - - if (config.CustomFormats.Count > 0) - { - await _customFormatUpdaterFactory().Process(cmd.Preview, config); - } - } - } -} diff --git a/src/Recyclarr/Command/Services/ServiceBase.cs b/src/Recyclarr/Command/Services/ServiceBase.cs deleted file mode 100644 index 976630b2..00000000 --- a/src/Recyclarr/Command/Services/ServiceBase.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Text; -using CliFx.Exceptions; -using Flurl.Http; -using TrashLib.Extensions; -using YamlDotNet.Core; - -namespace Recyclarr.Command.Services; - -/// -/// Mainly intended to handle common exception recovery logic between service command classes. -/// -public abstract class ServiceBase where T : IServiceCommand -{ - public async Task Execute(T cmd) - { - try - { - await Process(cmd); - } - catch (YamlException e) - { - var message = e.InnerException is not null ? e.InnerException.Message : e.Message; - var msg = new StringBuilder(); - msg.AppendLine($"Found Unrecognized YAML Property: {message}"); - msg.AppendLine("Please remove the property quoted in the above message from your YAML file"); - msg.AppendLine("Exiting due to invalid configuration"); - throw new CommandException(msg.ToString()); - } - catch (FlurlHttpException e) - { - throw new CommandException( - $"HTTP error while communicating with {cmd.Name}: {e.SanitizedExceptionMessage()}"); - } - catch (Exception e) when (e is not CommandException) - { - throw new CommandException(e.ToString()); - } - } - - protected abstract Task Process(T cmd); -} diff --git a/src/Recyclarr/Command/Services/SonarrService.cs b/src/Recyclarr/Command/Services/SonarrService.cs deleted file mode 100644 index e7bf0952..00000000 --- a/src/Recyclarr/Command/Services/SonarrService.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.IO.Abstractions; -using CliFx.Exceptions; -using Recyclarr.Config; -using Serilog; -using TrashLib; -using TrashLib.Extensions; -using TrashLib.Sonarr; -using TrashLib.Sonarr.Config; -using TrashLib.Sonarr.QualityDefinition; -using TrashLib.Sonarr.ReleaseProfile; - -namespace Recyclarr.Command.Services; - -public class SonarrService : ServiceBase -{ - private readonly ILogger _log; - private readonly IConfigurationLoader _configLoader; - private readonly Func _profileUpdaterFactory; - private readonly Func _qualityUpdaterFactory; - private readonly IReleaseProfileLister _lister; - private readonly IFileSystem _fs; - private readonly IAppPaths _paths; - - public SonarrService( - ILogger log, - IConfigurationLoader configLoader, - Func profileUpdaterFactory, - Func qualityUpdaterFactory, - IReleaseProfileLister lister, - IFileSystem fs, - IAppPaths paths) - { - _log = log; - _configLoader = configLoader; - _profileUpdaterFactory = profileUpdaterFactory; - _qualityUpdaterFactory = qualityUpdaterFactory; - _lister = lister; - _fs = fs; - _paths = paths; - } - - public string DefaultCacheStoragePath => _fs.Path.Combine(_paths.CacheDirectory, "sonarr"); - - protected override async Task Process(ISonarrCommand cmd) - { - if (cmd.ListReleaseProfiles) - { - _lister.ListReleaseProfiles(); - return; - } - - if (cmd.ListTerms != "empty") - { - if (!string.IsNullOrEmpty(cmd.ListTerms)) - { - _lister.ListTerms(cmd.ListTerms); - } - else - { - throw new CommandException( - "The --list-terms option was specified without a Release Profile Trash ID specified"); - } - - return; - } - - foreach (var config in _configLoader.LoadMany(cmd.Config, "sonarr")) - { - _log.Information("Processing server {Url}", FlurlLogging.SanitizeUrl(config.BaseUrl)); - - if (config.ReleaseProfiles.Count > 0) - { - await _profileUpdaterFactory().Process(cmd.Preview, config); - } - - if (config.QualityDefinition.HasValue) - { - await _qualityUpdaterFactory().Process(cmd.Preview, config); - } - } - } -} diff --git a/src/Recyclarr/Command/SonarrCommand.cs b/src/Recyclarr/Command/SonarrCommand.cs index 8e819277..33508c10 100644 --- a/src/Recyclarr/Command/SonarrCommand.cs +++ b/src/Recyclarr/Command/SonarrCommand.cs @@ -1,19 +1,20 @@ -using System.IO.Abstractions; using CliFx.Attributes; +using CliFx.Exceptions; using JetBrains.Annotations; -using Recyclarr.Command.Initialization; -using Recyclarr.Command.Services; -using TrashLib; +using Recyclarr.Config; +using Serilog; +using TrashLib.Extensions; +using TrashLib.Sonarr; +using TrashLib.Sonarr.Config; +using TrashLib.Sonarr.QualityDefinition; +using TrashLib.Sonarr.ReleaseProfile; namespace Recyclarr.Command; [Command("sonarr", Description = "Perform operations on a Sonarr instance")] [UsedImplicitly] -public class SonarrCommand : ServiceCommand, ISonarrCommand +public class SonarrCommand : ServiceCommand { - private readonly Lazy _service; - private readonly string? _cacheStoragePath; - [CommandOption("list-release-profiles", Description = "List available release profiles from the guide in YAML format.")] public bool ListReleaseProfiles { get; [UsedImplicitly] set; } @@ -25,26 +26,52 @@ public class SonarrCommand : ServiceCommand, ISonarrCommand "Note that not every release profile has terms that may be filtered.")] public string? ListTerms { get; [UsedImplicitly] set; } = "empty"; - public sealed override string CacheStoragePath - { - get => _cacheStoragePath ?? _service.Value.DefaultCacheStoragePath; - protected init => _cacheStoragePath = value; - } - public override string Name => "Sonarr"; - public SonarrCommand( - IServiceInitializationAndCleanup init, - Lazy service, - IFileSystem fs, - IAppPaths paths) - : base(init) + public override async Task Process(IServiceLocatorProxy container) { - _service = service; - } + await base.Process(container); - protected override async Task Process() - { - await _service.Value.Execute(this); + var lister = container.Resolve(); + var profileUpdaterFactory = container.Resolve>(); + var qualityUpdaterFactory = container.Resolve>(); + var configLoader = container.Resolve>(); + var log = container.Resolve(); + + if (ListReleaseProfiles) + { + lister.ListReleaseProfiles(); + return; + } + + if (ListTerms != "empty") + { + if (!string.IsNullOrEmpty(ListTerms)) + { + lister.ListTerms(ListTerms); + } + else + { + throw new CommandException( + "The --list-terms option was specified without a Release Profile Trash ID specified"); + } + + return; + } + + foreach (var config in configLoader.LoadMany(Config, "sonarr")) + { + log.Information("Processing server {Url}", FlurlLogging.SanitizeUrl(config.BaseUrl)); + + if (config.ReleaseProfiles.Count > 0) + { + await profileUpdaterFactory().Process(Preview, config); + } + + if (config.QualityDefinition.HasValue) + { + await qualityUpdaterFactory().Process(Preview, config); + } + } } } diff --git a/src/Recyclarr/CompositionRoot.cs b/src/Recyclarr/CompositionRoot.cs index 0a9953cb..b47f1e65 100644 --- a/src/Recyclarr/CompositionRoot.cs +++ b/src/Recyclarr/CompositionRoot.cs @@ -8,13 +8,11 @@ using CliFx.Infrastructure; using Common; using Recyclarr.Command.Helpers; using Recyclarr.Command.Initialization; -using Recyclarr.Command.Initialization.Init; -using Recyclarr.Command.Services; using Recyclarr.Config; using Recyclarr.Logging; using Recyclarr.Migration; using Serilog; -using Serilog.Core; +using Serilog.Events; using TrashLib; using TrashLib.Cache; using TrashLib.Config; @@ -27,25 +25,66 @@ using YamlDotNet.Serialization; namespace Recyclarr; -public static class CompositionRoot +internal class CompositionRoot : ICompositionRoot { - private static void SetupLogging(ContainerBuilder builder) + public IServiceLocatorProxy Setup(string? appDataDir, IConsole console, LogEventLevel logLevel) + { + return Setup(new ContainerBuilder(), appDataDir, console, logLevel); + } + + public IServiceLocatorProxy Setup(ContainerBuilder builder, string? appDataDir, IConsole console, + LogEventLevel logLevel) + { + builder.RegisterInstance(console).As(); + + RegisterAppPaths(builder, appDataDir); + RegisterLogger(builder, logLevel); + + builder.RegisterModule(); + builder.RegisterModule(); + builder.RegisterModule(); + builder.RegisterModule(); + + // Needed for Autofac.Extras.Ordering + builder.RegisterSource(); + + builder.RegisterModule(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + ConfigurationRegistrations(builder); + CommandRegistrations(builder); + + builder.Register(_ => AutoMapperConfig.Setup()).SingleInstance(); + + return new ServiceLocatorProxy(builder.Build()); + } + + private void RegisterLogger(ContainerBuilder builder, LogEventLevel logLevel) { builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().SingleInstance(); builder.RegisterType(); - builder.Register(c => c.Resolve().Create()) + builder.Register(c => c.Resolve().Create(logLevel)) .As() .SingleInstance(); } + private void RegisterAppPaths(ContainerBuilder builder, string? appDataDir) + { + builder.RegisterModule(); + builder.RegisterType().As(); + builder.RegisterType(); + + builder.Register(c => c.Resolve().CreateAppPaths(appDataDir)) + .As() + .SingleInstance(); + } + private static void ConfigurationRegistrations(ContainerBuilder builder) { builder.RegisterModule(); builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); builder.RegisterType().As(); builder.RegisterGeneric(typeof(ConfigurationLoader<>)) @@ -55,10 +94,6 @@ public static class CompositionRoot private static void CommandRegistrations(ContainerBuilder builder) { - builder.RegisterType(); - builder.RegisterType(); - builder.RegisterType().As(); - // Register all types deriving from CliFx's ICommand. These are all of our supported subcommands. builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .AssignableTo(); @@ -71,37 +106,4 @@ public static class CompositionRoot .As() .SingleInstance(); } - - public static IContainer Setup() - { - return Setup(new ContainerBuilder()); - } - - public static IContainer Setup(ContainerBuilder builder) - { - // Needed for Autofac.Extras.Ordering - builder.RegisterSource(); - - builder.RegisterType().As(); - builder.RegisterType().As().SingleInstance(); - - builder.RegisterModule(); - builder.RegisterType().As(); - builder.RegisterType().As(); - - ConfigurationRegistrations(builder); - CommandRegistrations(builder); - SetupLogging(builder); - - builder.RegisterModule(); - builder.RegisterModule(); - builder.RegisterModule(); - builder.RegisterModule(); - builder.RegisterModule(); - builder.RegisterModule(); - - builder.Register(_ => AutoMapperConfig.Setup()).SingleInstance(); - - return builder.Build(); - } } diff --git a/src/Recyclarr/ConfigurationFinder.cs b/src/Recyclarr/ConfigurationFinder.cs index a0ca5a73..f2d473c7 100644 --- a/src/Recyclarr/ConfigurationFinder.cs +++ b/src/Recyclarr/ConfigurationFinder.cs @@ -20,12 +20,13 @@ public class ConfigurationFinder : IConfigurationFinder _fs = fs; } - public string FindConfigPath() + public IFileInfo FindConfigPath() { var newPath = _paths.ConfigPath; - var oldPath = _fs.Path.Combine(_appContext.BaseDirectory, _paths.DefaultConfigFilename); + var oldPath = _fs.DirectoryInfo.FromDirectoryName(_appContext.BaseDirectory) + .File(AppPaths.DefaultConfigFilename); - if (!_fs.File.Exists(oldPath)) + if (!oldPath.Exists) { return newPath; } diff --git a/src/Recyclarr/ICompositionRoot.cs b/src/Recyclarr/ICompositionRoot.cs new file mode 100644 index 00000000..fa7bcfb5 --- /dev/null +++ b/src/Recyclarr/ICompositionRoot.cs @@ -0,0 +1,13 @@ +using Autofac; +using CliFx.Infrastructure; +using Serilog.Events; + +namespace Recyclarr; + +public interface ICompositionRoot +{ + IServiceLocatorProxy Setup(string? appDataDir, IConsole console, LogEventLevel logLevel); + + IServiceLocatorProxy Setup(ContainerBuilder builder, string? appDataDir, IConsole console, + LogEventLevel logLevel); +} diff --git a/src/Recyclarr/IServiceLocatorProxy.cs b/src/Recyclarr/IServiceLocatorProxy.cs new file mode 100644 index 00000000..2974af36 --- /dev/null +++ b/src/Recyclarr/IServiceLocatorProxy.cs @@ -0,0 +1,9 @@ +using Autofac; + +namespace Recyclarr; + +public interface IServiceLocatorProxy : IDisposable +{ + ILifetimeScope Container { get; } + T Resolve() where T : notnull; +} diff --git a/src/Recyclarr/Logging/DelayedFileSink.cs b/src/Recyclarr/Logging/DelayedFileSink.cs deleted file mode 100644 index 25fb0764..00000000 --- a/src/Recyclarr/Logging/DelayedFileSink.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.IO.Abstractions; -using Serilog.Events; -using Serilog.Formatting; -using Serilog.Formatting.Display; -using TrashLib; - -namespace Recyclarr.Logging; - -public sealed class DelayedFileSink : IDelayedFileSink -{ - private readonly IAppPaths _paths; - private readonly Lazy _stream; - private ITextFormatter? _formatter; - - public DelayedFileSink(IAppPaths paths, IFileSystem fs) - { - _paths = paths; - _stream = new Lazy(() => - { - var logPath = fs.Path.Combine(_paths.LogDirectory, $"trash_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); - return fs.File.CreateText(logPath); - }); - } - - public void Emit(LogEvent logEvent) - { - if (!_paths.IsAppDataPathValid) - { - return; - } - - _formatter?.Format(logEvent, _stream.Value); - _stream.Value.Flush(); - } - - public void Dispose() - { - if (_stream.IsValueCreated) - { - _stream.Value.Close(); - } - } - - public void SetTemplate(string template) - { - _formatter = new MessageTemplateTextFormatter(template); - } -} diff --git a/src/Recyclarr/Logging/IDelayedFileSink.cs b/src/Recyclarr/Logging/IDelayedFileSink.cs deleted file mode 100644 index 7a9893fe..00000000 --- a/src/Recyclarr/Logging/IDelayedFileSink.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Serilog.Core; - -namespace Recyclarr.Logging; - -public interface IDelayedFileSink : ILogEventSink, IDisposable -{ - void SetTemplate(string template); -} diff --git a/src/Recyclarr/Logging/LogJanitor.cs b/src/Recyclarr/Logging/LogJanitor.cs index 53516a59..b074233e 100644 --- a/src/Recyclarr/Logging/LogJanitor.cs +++ b/src/Recyclarr/Logging/LogJanitor.cs @@ -1,24 +1,19 @@ -using System.IO.Abstractions; using TrashLib; namespace Recyclarr.Logging; public class LogJanitor : ILogJanitor { - private readonly IFileSystem _fs; private readonly IAppPaths _paths; - public LogJanitor(IFileSystem fs, IAppPaths paths) + public LogJanitor(IAppPaths paths) { - _fs = fs; _paths = paths; } public void DeleteOldestLogFiles(int numberOfNewestToKeep) { - var dir = _fs.Directory.CreateDirectory(_paths.LogDirectory); - - foreach (var file in dir.GetFiles() + foreach (var file in _paths.LogDirectory.GetFiles() .OrderByDescending(f => f.Name) .Skip(numberOfNewestToKeep)) { diff --git a/src/Recyclarr/Logging/LoggerFactory.cs b/src/Recyclarr/Logging/LoggerFactory.cs index 995eac38..ec9a8403 100644 --- a/src/Recyclarr/Logging/LoggerFactory.cs +++ b/src/Recyclarr/Logging/LoggerFactory.cs @@ -1,30 +1,29 @@ +using System.IO.Abstractions; using Serilog; -using Serilog.Core; +using Serilog.Events; +using TrashLib; namespace Recyclarr.Logging; public class LoggerFactory { - private const string ConsoleTemplate = "[{Level:u3}] {Message:lj}{NewLine}{Exception}"; + private readonly IAppPaths _paths; - private readonly LoggingLevelSwitch _logLevel; - private readonly Func _fileSinkFactory; + private const string ConsoleTemplate = "[{Level:u3}] {Message:lj}{NewLine}{Exception}"; - public LoggerFactory(LoggingLevelSwitch logLevel, Func fileSinkFactory) + public LoggerFactory(IAppPaths paths) { - _logLevel = logLevel; - _fileSinkFactory = fileSinkFactory; + _paths = paths; } - public ILogger Create() + public ILogger Create(LogEventLevel level) { - var fileSink = _fileSinkFactory(); - fileSink.SetTemplate(ConsoleTemplate); + var logPath = _paths.LogDirectory.File($"trash_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); return new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Console(outputTemplate: ConsoleTemplate, levelSwitch: _logLevel) - .WriteTo.Sink(fileSink) + .MinimumLevel.Is(level) + .WriteTo.Console(outputTemplate: ConsoleTemplate) + .WriteTo.File(logPath.FullName) .CreateLogger(); } } diff --git a/src/Recyclarr/Migration/Steps/MigrateTrashUpdaterAppDataDir.cs b/src/Recyclarr/Migration/Steps/MigrateTrashUpdaterAppDataDir.cs index 73a153c5..223d74c5 100644 --- a/src/Recyclarr/Migration/Steps/MigrateTrashUpdaterAppDataDir.cs +++ b/src/Recyclarr/Migration/Steps/MigrateTrashUpdaterAppDataDir.cs @@ -13,34 +13,44 @@ namespace Recyclarr.Migration.Steps; public class MigrateTrashUpdaterAppDataDir : IMigrationStep { private readonly IFileSystem _fs; - private readonly Lazy _newPath, _oldPath; + private readonly IAppPaths _paths; public int Order => 20; public bool Required => true; public string Description - => $"Merge files from old app data directory `{GetOldPath()}` into `{GetNewPath()}` and delete old directory"; + => $"Merge files from old app data directory `{OldPath}` into `{NewPath}` and delete old directory"; public IReadOnlyCollection Remediation => new[] { - $"Check if `{GetNewPath()}` already exists. If so, manually copy all files from `{GetOldPath()}` and delete it to fix the error.", - $"Ensure Recyclarr has permission to recursively delete {GetOldPath()}", - $"Ensure Recyclarr has permission to create and move files into {GetNewPath()}" + $"Check if `{NewPath}` already exists. If so, manually copy all files from `{OldPath}` and delete it to fix the error.", + $"Ensure Recyclarr has permission to recursively delete {OldPath}", + $"Ensure Recyclarr has permission to create and move files into {NewPath}" }; - private string GetNewPath() => _newPath.Value; - private string GetOldPath() => _oldPath.Value; - public MigrateTrashUpdaterAppDataDir(IFileSystem fs, IAppPaths paths) { _fs = fs; + _paths = paths; + } - // Will be something like `/home/user/.config/recyclarr`. - _newPath = new Lazy(paths.GetAppDataPath); - _oldPath = new Lazy(() => _fs.Path.Combine(_fs.Path.GetDirectoryName(GetNewPath()), "trash-updater")); + public IDirectoryInfo NewPath + { + get + { + // Will be something like `/home/user/.config/recyclarr`. + var path = _paths.AppDataDirectory; + path.Refresh(); + return path; + } } - public bool CheckIfNeeded() => _fs.Directory.Exists(GetOldPath()); + public IDirectoryInfo OldPath => NewPath.Parent.SubDirectory("trash-updater"); + + public bool CheckIfNeeded() + { + return OldPath.Exists; + } public void Execute(IConsole? console) { @@ -48,31 +58,30 @@ public class MigrateTrashUpdaterAppDataDir : IMigrationStep MoveFile("recyclarr.yml"); MoveFile("settings.yml"); - var oldDir = _fs.DirectoryInfo.FromDirectoryName(GetOldPath()); - if (oldDir.Exists) + if (OldPath.Exists) { - oldDir.Delete(true); + OldPath.Delete(true); } } private void MoveDirectory(string directory, IConsole? console) { - var oldPath = _fs.Path.Combine(GetOldPath(), directory); - if (_fs.Directory.Exists(oldPath)) + var oldPath = OldPath.SubDirectory(directory); + if (oldPath.Exists) { _fs.MergeDirectory( oldPath, - _fs.Path.Combine(GetNewPath(), directory), + NewPath.SubDirectory(directory), console); } } private void MoveFile(string file) { - var recyclarrYaml = _fs.FileInfo.FromFileName(_fs.Path.Combine(GetOldPath(), file)); + var recyclarrYaml = OldPath.File(file); if (recyclarrYaml.Exists) { - recyclarrYaml.MoveTo(_fs.Path.Combine(GetNewPath(), file)); + recyclarrYaml.MoveTo(NewPath.File(file).FullName); } } } diff --git a/src/Recyclarr/Program.cs b/src/Recyclarr/Program.cs index e39848f4..b1b43885 100644 --- a/src/Recyclarr/Program.cs +++ b/src/Recyclarr/Program.cs @@ -1,47 +1,33 @@ using System.Diagnostics; using System.Text; using Autofac; -using Autofac.Core; using CliFx; -using CliFx.Infrastructure; -using Recyclarr.Command.Helpers; +using Recyclarr.Command; namespace Recyclarr; internal static class Program { - private static IContainer? _container; - private static string ExecutableName => Process.GetCurrentProcess().ProcessName; public static async Task Main() { - _container = CompositionRoot.Setup(); + BaseCommand.CompositionRoot = new CompositionRoot(); var status = await new CliApplicationBuilder() - .AddCommands(GetRegisteredCommandTypes()) + .AddCommands(GetAllCommandTypes()) .SetExecutableName(ExecutableName) .SetVersion(BuildVersion()) - .UseTypeActivator(type => CliTypeActivator.ResolveType(_container, type)) - .UseConsole(_container.Resolve()) .Build() .RunAsync(); return status; } - private static IEnumerable GetRegisteredCommandTypes() + private static IEnumerable GetAllCommandTypes() { - if (_container is null) - { - throw new NullReferenceException("DI Container was null during migration process"); - } - - return _container.ComponentRegistry.Registrations - .SelectMany(x => x.Services) - .OfType() - .Select(x => x.ServiceType) - .Where(x => x.IsAssignableTo()); + return typeof(Program).Assembly.GetTypes() + .Where(x => x.IsAssignableTo() && !x.IsAbstract); } private static string BuildVersion() diff --git a/src/Recyclarr/ServiceLocatorProxy.cs b/src/Recyclarr/ServiceLocatorProxy.cs new file mode 100644 index 00000000..cd877d81 --- /dev/null +++ b/src/Recyclarr/ServiceLocatorProxy.cs @@ -0,0 +1,23 @@ +using Autofac; + +namespace Recyclarr; + +public sealed class ServiceLocatorProxy : IServiceLocatorProxy +{ + public ServiceLocatorProxy(ILifetimeScope container) + { + Container = container; + } + + public ILifetimeScope Container { get; } + + public T Resolve() where T : notnull + { + return Container.Resolve(); + } + + public void Dispose() + { + Container.Dispose(); + } +} diff --git a/src/TestLibrary/AutoFixture/MockFileSystemSpecimenBuilder.cs b/src/TestLibrary/AutoFixture/MockFileSystemSpecimenBuilder.cs new file mode 100644 index 00000000..4feae967 --- /dev/null +++ b/src/TestLibrary/AutoFixture/MockFileSystemSpecimenBuilder.cs @@ -0,0 +1,27 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.Extensions; +using System.IO.Abstractions.TestingHelpers; +using AutoFixture; + +namespace TestLibrary.AutoFixture; + +public class MockFileSystemSpecimenBuilder : ICustomization +{ + public void Customize(IFixture fixture) + { + var fs = new MockFileSystem(); + fixture.Inject(fs); + + fixture.Customize(x => x.FromFactory(() => + { + var name = $"MockFile-{fixture.Create()}"; + return fs.CurrentDirectory().File(name); + })); + + fixture.Customize(x => x.FromFactory(() => + { + var name = $"MockDirectory-{fixture.Create()}"; + return fs.CurrentDirectory().SubDirectory(name); + })); + } +} diff --git a/src/TestLibrary/AutoFixture/NSubstituteFixture.cs b/src/TestLibrary/AutoFixture/NSubstituteFixture.cs index 6ac066dc..774b4d23 100644 --- a/src/TestLibrary/AutoFixture/NSubstituteFixture.cs +++ b/src/TestLibrary/AutoFixture/NSubstituteFixture.cs @@ -12,10 +12,9 @@ public static class NSubstituteFixture OmitAutoProperties = true }; - fixture.Customize(new AutoNSubstituteCustomization - { - ConfigureMembers = true - }); + fixture + .Customize(new AutoNSubstituteCustomization {ConfigureMembers = true}) + .Customize(new MockFileSystemSpecimenBuilder()); return fixture; } diff --git a/src/TestLibrary/MockFileSystemExtensions.cs b/src/TestLibrary/MockFileSystemExtensions.cs index 5d5d13b0..23fd8384 100644 --- a/src/TestLibrary/MockFileSystemExtensions.cs +++ b/src/TestLibrary/MockFileSystemExtensions.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; namespace TestLibrary; @@ -9,8 +10,18 @@ public static class MockFileSystemExtensions fs.AddFile(FileUtils.NormalizePath(path), new MockFileData("")); } + public static void AddFileNoData(this MockFileSystem fs, IFileInfo path) + { + fs.AddFile(path.FullName, new MockFileData("")); + } + public static void AddDirectory2(this MockFileSystem fs, string path) { fs.AddDirectory(FileUtils.NormalizePath(path)); } + + public static void AddDirectory(this MockFileSystem fs, IDirectoryInfo path) + { + fs.AddDirectory(path.FullName); + } } diff --git a/src/TestLibrary/TestLibrary.csproj b/src/TestLibrary/TestLibrary.csproj index 303fe95f..a44d1e0b 100644 --- a/src/TestLibrary/TestLibrary.csproj +++ b/src/TestLibrary/TestLibrary.csproj @@ -10,6 +10,7 @@ + diff --git a/src/TrashLib.Tests/Config/Settings/SettingsPersisterTest.cs b/src/TrashLib.Tests/Config/Settings/SettingsPersisterTest.cs index 9ba72a78..12017a91 100644 --- a/src/TrashLib.Tests/Config/Settings/SettingsPersisterTest.cs +++ b/src/TrashLib.Tests/Config/Settings/SettingsPersisterTest.cs @@ -3,6 +3,7 @@ using AutoFixture.NUnit3; using FluentAssertions; using NSubstitute; using NUnit.Framework; +using Recyclarr.TestLibrary; using TestLibrary.AutoFixture; using TrashLib.Config; using TrashLib.Config.Settings; @@ -17,26 +18,21 @@ public class SettingsPersisterTest [Test, AutoMockData] public void Load_should_create_settings_file_if_not_exists( [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem, - [Frozen] IAppPaths paths, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, SettingsPersister sut) { - paths.SettingsPath.Returns("test_path"); - sut.Load(); - fileSystem.AllFiles.Should().ContainSingle(x => x.EndsWith(paths.SettingsPath)); + fileSystem.AllFiles.Should().ContainSingle(x => x == paths.SettingsPath.FullName); } [Test, AutoMockData] public void Load_defaults_when_file_does_not_exist( - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem, [Frozen(Matching.ImplementedInterfaces)] YamlSerializerFactory serializerFactory, [Frozen(Matching.ImplementedInterfaces)] SettingsProvider settingsProvider, - [Frozen] IAppPaths paths, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, SettingsPersister sut) { - paths.SettingsPath.Returns("test_path"); - sut.Load(); var expectedSettings = new SettingsValues(); @@ -46,8 +42,8 @@ public class SettingsPersisterTest [Test, AutoMockData] public void Load_data_correctly_when_file_exists( [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem, - [Frozen] IYamlSerializerFactory serializerFactory, - [Frozen] IAppPaths paths, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, + [Frozen] IDeserializer deserializer, SettingsPersister sut) { // For this test, it doesn't really matter if the YAML data matches what SettingsValue expects; @@ -56,10 +52,7 @@ public class SettingsPersisterTest repository: clone_url: http://the_url.com "; - var deserializer = Substitute.For(); - serializerFactory.CreateDeserializer().Returns(deserializer); - paths.SettingsPath.Returns("test_path"); - fileSystem.AddFile(paths.SettingsPath, new MockFileData(expectedYamlData)); + fileSystem.AddFile(paths.SettingsPath.FullName, new MockFileData(expectedYamlData)); sut.Load(); diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParserTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParserTest.cs index b9878893..1b89155b 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParserTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParserTest.cs @@ -1,10 +1,9 @@ using System.IO.Abstractions; -using System.IO.Abstractions.Extensions; using System.IO.Abstractions.TestingHelpers; using AutoFixture.NUnit3; using FluentAssertions; -using NSubstitute; using NUnit.Framework; +using Recyclarr.TestLibrary; using TestLibrary.AutoFixture; using TrashLib.Radarr.CustomFormat.Guide; using TrashLib.TestLibrary; @@ -17,16 +16,15 @@ public class LocalRepoCustomFormatJsonParserTest { [Test, AutoMockData] public void Get_custom_format_json_works( - [Frozen] IAppPaths paths, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, LocalRepoCustomFormatJsonParser sut) { - var jsonDir = fs.CurrentDirectory() + var jsonDir = paths.RepoDirectory .SubDirectory("docs") .SubDirectory("json") .SubDirectory("radarr"); - paths.RepoDirectory.Returns(fs.CurrentDirectory().FullName); fs.AddFile(jsonDir.File("first.json").FullName, new MockFileData("{'name':'first','trash_id':'1'}")); fs.AddFile(jsonDir.File("second.json").FullName, new MockFileData("{'name':'second','trash_id':'2'}")); diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs index ce1dfcb7..a24a3f56 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParserTest.cs @@ -1,11 +1,10 @@ using System.IO.Abstractions; -using System.IO.Abstractions.Extensions; using System.IO.Abstractions.TestingHelpers; using AutoFixture.NUnit3; using FluentAssertions; using Newtonsoft.Json; -using NSubstitute; using NUnit.Framework; +using Recyclarr.TestLibrary; using TestLibrary; using TestLibrary.AutoFixture; using TrashLib.Sonarr.ReleaseProfile; @@ -19,8 +18,8 @@ public class LocalRepoReleaseProfileJsonParserTest { [Test, AutoMockData] public void Get_custom_format_json_works( - [Frozen] IAppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem, + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, LocalRepoReleaseProfileJsonParser sut) { static ReleaseProfileData MakeMockObject(string term) => new() @@ -39,9 +38,13 @@ public class LocalRepoReleaseProfileJsonParserTest var mockData1 = MakeMockObject("first"); var mockData2 = MakeMockObject("second"); - paths.RepoDirectory.Returns(""); - fileSystem.AddFile("docs/json/sonarr/first.json", MockFileData(mockData1)); - fileSystem.AddFile("docs/json/sonarr/second.json", MockFileData(mockData2)); + var baseDir = paths.RepoDirectory + .SubDirectory("docs") + .SubDirectory("json") + .SubDirectory("sonarr"); + + fs.AddFile(baseDir.File("first.json").FullName, MockFileData(mockData1)); + fs.AddFile(baseDir.File("second.json").FullName, MockFileData(mockData2)); var results = sut.GetReleaseProfileData(); @@ -55,11 +58,10 @@ public class LocalRepoReleaseProfileJsonParserTest [Test, AutoMockData] public void Json_exceptions_do_not_interrupt_parsing_other_files( [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - [Frozen] IAppPaths paths, + [Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths, LocalRepoReleaseProfileJsonParser sut) { - paths.RepoDirectory.Returns(""); - var rootPath = fs.CurrentDirectory() + var rootPath = paths.RepoDirectory .SubDirectory("docs") .SubDirectory("json") .SubDirectory("sonarr"); diff --git a/src/TrashLib.Tests/TrashLib.Tests.csproj b/src/TrashLib.Tests/TrashLib.Tests.csproj index 62202ad4..ac8eb575 100644 --- a/src/TrashLib.Tests/TrashLib.Tests.csproj +++ b/src/TrashLib.Tests/TrashLib.Tests.csproj @@ -4,6 +4,7 @@ + diff --git a/src/TrashLib/Config/Settings/SettingsPersister.cs b/src/TrashLib/Config/Settings/SettingsPersister.cs index 74888992..34649d9b 100644 --- a/src/TrashLib/Config/Settings/SettingsPersister.cs +++ b/src/TrashLib/Config/Settings/SettingsPersister.cs @@ -1,5 +1,3 @@ -using System.IO.Abstractions; - namespace TrashLib.Config.Settings; public class SettingsPersister : ISettingsPersister @@ -7,18 +5,15 @@ public class SettingsPersister : ISettingsPersister private readonly IAppPaths _paths; private readonly ISettingsProvider _settingsProvider; private readonly IYamlSerializerFactory _serializerFactory; - private readonly IFileSystem _fileSystem; public SettingsPersister( IAppPaths paths, ISettingsProvider settingsProvider, - IYamlSerializerFactory serializerFactory, - IFileSystem fileSystem) + IYamlSerializerFactory serializerFactory) { _paths = paths; _settingsProvider = settingsProvider; _serializerFactory = serializerFactory; - _fileSystem = fileSystem; } public void Load() @@ -30,12 +25,13 @@ public class SettingsPersister : ISettingsPersister private string LoadOrCreateSettingsFile() { - if (!_fileSystem.File.Exists(_paths.SettingsPath)) + if (!_paths.SettingsPath.Exists) { CreateDefaultSettingsFile(); } - return _fileSystem.File.ReadAllText(_paths.SettingsPath); + using var stream = _paths.SettingsPath.OpenText(); + return stream.ReadToEnd(); } private void CreateDefaultSettingsFile() @@ -47,6 +43,8 @@ public class SettingsPersister : ISettingsPersister "# For the settings file reference guide, visit the link to the wiki below:\n" + "# https://github.com/recyclarr/recyclarr/wiki/Settings-Reference\n"; - _fileSystem.File.WriteAllText(_paths.SettingsPath, fileData); + _paths.SettingsPath.Directory.Create(); + using var stream = _paths.SettingsPath.CreateText(); + stream.Write(fileData); } } diff --git a/src/TrashLib/IAppPaths.cs b/src/TrashLib/IAppPaths.cs index 12d73ef6..500f09fe 100644 --- a/src/TrashLib/IAppPaths.cs +++ b/src/TrashLib/IAppPaths.cs @@ -1,15 +1,13 @@ +using System.IO.Abstractions; + namespace TrashLib; public interface IAppPaths { - void SetAppDataPath(string path); - string GetAppDataPath(); - string ConfigPath { get; } - string SettingsPath { get; } - string LogDirectory { get; } - string RepoDirectory { get; } - string CacheDirectory { get; } - string DefaultConfigFilename { get; } - bool IsAppDataPathValid { get; } - string DefaultAppDataDirectoryName { get; } + IDirectoryInfo AppDataDirectory { get; } + IFileInfo ConfigPath { get; } + IFileInfo SettingsPath { get; } + IDirectoryInfo LogDirectory { get; } + IDirectoryInfo RepoDirectory { get; } + IDirectoryInfo CacheDirectory { get; } } diff --git a/src/TrashLib/IConfigurationFinder.cs b/src/TrashLib/IConfigurationFinder.cs index 72c4aefc..15af6255 100644 --- a/src/TrashLib/IConfigurationFinder.cs +++ b/src/TrashLib/IConfigurationFinder.cs @@ -1,6 +1,8 @@ +using System.IO.Abstractions; + namespace TrashLib; public interface IConfigurationFinder { - string FindConfigPath(); + IFileInfo FindConfigPath(); } diff --git a/src/TrashLib/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParser.cs b/src/TrashLib/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParser.cs index 1097b553..89749f28 100644 --- a/src/TrashLib/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParser.cs +++ b/src/TrashLib/Radarr/CustomFormat/Guide/LocalRepoCustomFormatJsonParser.cs @@ -11,20 +11,18 @@ namespace TrashLib.Radarr.CustomFormat.Guide; public class LocalRepoCustomFormatJsonParser : IRadarrGuideService { - private readonly IFileSystem _fs; private readonly IAppPaths _paths; private readonly ILogger _log; - public LocalRepoCustomFormatJsonParser(IFileSystem fs, IAppPaths paths, ILogger log) + public LocalRepoCustomFormatJsonParser(IAppPaths paths, ILogger log) { - _fs = fs; _paths = paths; _log = log; } public IEnumerable GetCustomFormatData() { - var jsonDir = _fs.DirectoryInfo.FromDirectoryName(_paths.RepoDirectory) + var jsonDir = _paths.RepoDirectory .SubDirectory("docs") .SubDirectory("json") .SubDirectory("radarr"); diff --git a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs index 37167321..f45725e3 100644 --- a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs +++ b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs @@ -6,22 +6,20 @@ namespace TrashLib.Radarr.QualityDefinition; internal class RadarrQualityDefinitionGuideParser : IRadarrQualityDefinitionGuideParser { - private readonly IFileSystem _fs; private readonly IAppPaths _paths; private readonly Regex _regexHeader = new(@"^#+", RegexOptions.Compiled); private readonly Regex _regexTableRow = new(@"\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|", RegexOptions.Compiled); - public RadarrQualityDefinitionGuideParser(IFileSystem fs, IAppPaths paths) + public RadarrQualityDefinitionGuideParser(IAppPaths paths) { - _fs = fs; _paths = paths; } public async Task GetMarkdownData() { - var repoDir = _fs.DirectoryInfo.FromDirectoryName(_paths.RepoDirectory); + var repoDir = _paths.RepoDirectory; var file = repoDir .SubDirectory("docs") .SubDirectory("Radarr") diff --git a/src/TrashLib/Repo/IRepoUpdater.cs b/src/TrashLib/Repo/IRepoUpdater.cs index a15852c0..4aba8784 100644 --- a/src/TrashLib/Repo/IRepoUpdater.cs +++ b/src/TrashLib/Repo/IRepoUpdater.cs @@ -1,7 +1,9 @@ +using System.IO.Abstractions; + namespace TrashLib.Repo; public interface IRepoUpdater { - string RepoPath { get; } + IDirectoryInfo RepoPath { get; } void UpdateRepo(); } diff --git a/src/TrashLib/Repo/RepoUpdater.cs b/src/TrashLib/Repo/RepoUpdater.cs index 7f8b5f7b..56493ddf 100644 --- a/src/TrashLib/Repo/RepoUpdater.cs +++ b/src/TrashLib/Repo/RepoUpdater.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions; using Common; using LibGit2Sharp; using Serilog; @@ -28,7 +29,7 @@ public class RepoUpdater : IRepoUpdater _settingsProvider = settingsProvider; } - public string RepoPath => _paths.RepoDirectory; + public IDirectoryInfo RepoPath => _paths.RepoDirectory; public void UpdateRepo() { @@ -38,7 +39,7 @@ public class RepoUpdater : IRepoUpdater if (exception is not null) { _log.Information("Deleting local git repo and retrying git operation..."); - _fileUtils.DeleteReadOnlyDirectory(RepoPath); + _fileUtils.DeleteReadOnlyDirectory(RepoPath.FullName); exception = CheckoutAndUpdateRepo(); if (exception is not null) @@ -58,7 +59,7 @@ public class RepoUpdater : IRepoUpdater try { - using var repo = _repositoryFactory.CreateAndCloneIfNeeded(cloneUrl, RepoPath, branch); + using var repo = _repositoryFactory.CreateAndCloneIfNeeded(cloneUrl, RepoPath.FullName, branch); repo.ForceCheckout(branch); repo.Fetch(); repo.ResetHard($"origin/{branch}"); diff --git a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs index afeafd9e..7d5a6f04 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs @@ -11,18 +11,16 @@ internal class SonarrQualityDefinitionGuideParser : ISonarrQualityDefinitionGuid private readonly Regex _regexTableRow = new(@"\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|", RegexOptions.Compiled); - private readonly IFileSystem _fs; private readonly IAppPaths _paths; - public SonarrQualityDefinitionGuideParser(IFileSystem fs, IAppPaths paths) + public SonarrQualityDefinitionGuideParser(IAppPaths paths) { - _fs = fs; _paths = paths; } public async Task GetMarkdownData() { - var repoDir = _fs.DirectoryInfo.FromDirectoryName(_paths.RepoDirectory); + var repoDir = _paths.RepoDirectory; var file = repoDir .SubDirectory("docs") .SubDirectory("Sonarr") diff --git a/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs b/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs index 05faf167..ebb37b68 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs @@ -9,14 +9,12 @@ namespace TrashLib.Sonarr.ReleaseProfile.Guide; public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService { - private readonly IFileSystem _fs; private readonly IAppPaths _paths; private readonly ILogger _log; private readonly Lazy> _data; - public LocalRepoReleaseProfileJsonParser(IFileSystem fs, IAppPaths paths, ILogger log) + public LocalRepoReleaseProfileJsonParser(IAppPaths paths, ILogger log) { - _fs = fs; _paths = paths; _log = log; _data = new Lazy>(GetReleaseProfileDataImpl); @@ -25,8 +23,12 @@ public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService private IEnumerable GetReleaseProfileDataImpl() { var converter = new TermDataConverter(); - var jsonDir = _fs.Path.Combine(_paths.RepoDirectory, "docs/json/sonarr"); - var tasks = _fs.Directory.GetFiles(jsonDir, "*.json") + var jsonDir = _paths.RepoDirectory + .SubDirectory("docs") + .SubDirectory("json") + .SubDirectory("sonarr"); + + var tasks = jsonDir.GetFiles("*.json") .Select(f => LoadAndParseFile(f, converter)); return Task.WhenAll(tasks).Result @@ -34,11 +36,12 @@ public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService .Choose(x => x is not null ? (true, x) : default); } - private async Task LoadAndParseFile(string file, params JsonConverter[] converters) + private async Task LoadAndParseFile(IFileInfo file, params JsonConverter[] converters) { try { - var json = await _fs.File.ReadAllTextAsync(file); + using var stream = file.OpenText(); + var json = await stream.ReadToEndAsync(); return JsonConvert.DeserializeObject(json, converters); } catch (JsonException e) @@ -53,11 +56,11 @@ public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService return null; } - private void HandleJsonException(JsonException exception, string file) + private void HandleJsonException(JsonException exception, IFileInfo file) { _log.Warning(exception, "Failed to parse Sonarr JSON file (This likely indicates a bug that should be " + - "reported in the TRaSH repo): {File}", _fs.Path.GetFileName(file)); + "reported in the TRaSH repo): {File}", file.Name); } public ReleaseProfileData? GetUnfilteredProfileById(string trashId)