From a8cce8164eaf2e05daf3f954acf84b6224a24a65 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sun, 26 Jun 2022 18:26:51 -0500 Subject: [PATCH] refactor: Redo initialization logic Initialization logic has been completely overhauled. The previous implementation was based on an approach that prioritized keeping the composition root in the Program class. However, I wasn't happy with this. CliFx inevitably wants to be the effective entry point to the application. This means that the Program class should be as dumb as possible. The motivation for all this rework is the Recyclarr GUI. I need to be able to share more initialization code between the projects. Along with the initialization logic changes, I unintentionally interleaved in another, completely unrelated refactoring. The IAppPaths class now uses `IFileInfo` / `IDirectoryInfo` instead of `string` for everything. This greatly simplified the implementation of that interface and reduced dependencies and complexity across the code base. However, those changes were vast and required rewriting/fixing a lot of unit tests. --- .../Extensions/FileSystemExtensionsTest.cs | 12 +- src/Common/Extensions/FileSystemExtensions.cs | 14 +-- src/Directory.Build.targets | 10 +- .../Recyclarr.TestLibrary.csproj | 5 + src/Recyclarr.TestLibrary/TestAppPaths.cs | 12 ++ .../Command/BaseCommandTest.cs | 51 ++++++++ .../Command/CreateConfigCommandTest.cs | 29 +++-- .../Command/Helpers/CliTypeActivatorTest.cs | 61 ---------- .../Initialization/DefaultAppDataSetupTest.cs | 48 +++++--- .../Init/InitializeAppDataPathTest.cs | 37 ------ .../ServiceInitializationAndCleanupTest.cs | 49 -------- ...arrServiceTest.cs => SonarrCommandTest.cs} | 53 +++++---- src/Recyclarr.Tests/CompositionRootTest.cs | 10 +- .../ConfigurationFinderTest.cs | 42 +++---- src/Recyclarr.Tests/LogJanitorTest.cs | 24 ++-- .../Logging/DelayedFileSinkTest.cs | 29 ----- .../Migration/MigrationExecutorTest.cs | 3 +- .../MigrateTrashUpdaterAppDataDirTest.cs | 31 ++--- .../Migration/Steps/TestAppPaths.cs | 15 --- src/Recyclarr.Tests/Recyclarr.Tests.csproj | 1 + src/Recyclarr.sln | 6 + src/Recyclarr.sln.DotSettings | 18 ++- src/Recyclarr/AppPaths.cs | 29 ++--- src/Recyclarr/Command/BaseCommand.cs | 54 +++++++++ src/Recyclarr/Command/CreateConfigCommand.cs | 42 +++---- .../Command/Helpers/CacheStoragePath.cs | 9 +- .../Command/Helpers/CliTypeActivator.cs | 18 --- src/Recyclarr/Command/IRadarrCommand.cs | 6 - src/Recyclarr/Command/IServiceCommand.cs | 1 - src/Recyclarr/Command/ISonarrCommand.cs | 7 -- .../Initialization/Cleanup/IServiceCleaner.cs | 6 - .../Cleanup/OldLogFileCleaner.cs | 18 --- .../Initialization/DefaultAppDataSetup.cs | 64 +++++----- .../Initialization/IDefaultAppDataSetup.cs | 6 - .../IServiceInitializationAndCleanup.cs | 6 - .../Init/CheckMigrationNeeded.cs | 18 --- .../Init/IServiceInitializer.cs | 6 - .../Init/InitializeAppDataPath.cs | 28 ----- .../Initialization/Init/ServiceInitializer.cs | 84 ------------- .../InitializationAutofacModule.cs | 30 ----- .../ServiceInitializationAndCleanup.cs | 33 ------ src/Recyclarr/Command/MigrateCommand.cs | 34 ++---- src/Recyclarr/Command/RadarrCommand.cs | 60 ++++++---- src/Recyclarr/Command/ServiceCommand.cs | 111 +++++++++++++++--- .../Command/Services/RadarrService.cs | 65 ---------- src/Recyclarr/Command/Services/ServiceBase.cs | 41 ------- .../Command/Services/SonarrService.cs | 82 ------------- src/Recyclarr/Command/SonarrCommand.cs | 77 ++++++++---- src/Recyclarr/CompositionRoot.cs | 94 +++++++-------- src/Recyclarr/ConfigurationFinder.cs | 7 +- src/Recyclarr/ICompositionRoot.cs | 13 ++ src/Recyclarr/IServiceLocatorProxy.cs | 9 ++ src/Recyclarr/Logging/DelayedFileSink.cs | 48 -------- src/Recyclarr/Logging/IDelayedFileSink.cs | 8 -- src/Recyclarr/Logging/LogJanitor.cs | 9 +- src/Recyclarr/Logging/LoggerFactory.cs | 25 ++-- .../Steps/MigrateTrashUpdaterAppDataDir.cs | 49 ++++---- src/Recyclarr/Program.cs | 26 +--- src/Recyclarr/ServiceLocatorProxy.cs | 23 ++++ .../MockFileSystemSpecimenBuilder.cs | 27 +++++ .../AutoFixture/NSubstituteFixture.cs | 7 +- src/TestLibrary/MockFileSystemExtensions.cs | 11 ++ src/TestLibrary/TestLibrary.csproj | 1 + .../Config/Settings/SettingsPersisterTest.cs | 21 ++-- .../LocalRepoCustomFormatJsonParserTest.cs | 8 +- .../LocalRepoReleaseProfileJsonParserTest.cs | 22 ++-- src/TrashLib.Tests/TrashLib.Tests.csproj | 1 + .../Config/Settings/SettingsPersister.cs | 16 ++- src/TrashLib/IAppPaths.cs | 18 ++- src/TrashLib/IConfigurationFinder.cs | 4 +- .../Guide/LocalRepoCustomFormatJsonParser.cs | 6 +- .../RadarrQualityDefinitionGuideParser.cs | 6 +- src/TrashLib/Repo/IRepoUpdater.cs | 4 +- src/TrashLib/Repo/RepoUpdater.cs | 7 +- .../SonarrQualityDefinitionGuideParser.cs | 6 +- .../LocalRepoReleaseProfileJsonParser.cs | 21 ++-- 76 files changed, 800 insertions(+), 1192 deletions(-) create mode 100644 src/Recyclarr.TestLibrary/Recyclarr.TestLibrary.csproj create mode 100644 src/Recyclarr.TestLibrary/TestAppPaths.cs create mode 100644 src/Recyclarr.Tests/Command/BaseCommandTest.cs delete mode 100644 src/Recyclarr.Tests/Command/Helpers/CliTypeActivatorTest.cs delete mode 100644 src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs delete mode 100644 src/Recyclarr.Tests/Command/Initialization/ServiceInitializationAndCleanupTest.cs rename src/Recyclarr.Tests/Command/{Services/SonarrServiceTest.cs => SonarrCommandTest.cs} (51%) delete mode 100644 src/Recyclarr.Tests/Logging/DelayedFileSinkTest.cs delete mode 100644 src/Recyclarr.Tests/Migration/Steps/TestAppPaths.cs create mode 100644 src/Recyclarr/Command/BaseCommand.cs delete mode 100644 src/Recyclarr/Command/Helpers/CliTypeActivator.cs delete mode 100644 src/Recyclarr/Command/IRadarrCommand.cs delete mode 100644 src/Recyclarr/Command/ISonarrCommand.cs delete mode 100644 src/Recyclarr/Command/Initialization/Cleanup/IServiceCleaner.cs delete mode 100644 src/Recyclarr/Command/Initialization/Cleanup/OldLogFileCleaner.cs delete mode 100644 src/Recyclarr/Command/Initialization/IDefaultAppDataSetup.cs delete mode 100644 src/Recyclarr/Command/Initialization/IServiceInitializationAndCleanup.cs delete mode 100644 src/Recyclarr/Command/Initialization/Init/CheckMigrationNeeded.cs delete mode 100644 src/Recyclarr/Command/Initialization/Init/IServiceInitializer.cs delete mode 100644 src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs delete mode 100644 src/Recyclarr/Command/Initialization/Init/ServiceInitializer.cs delete mode 100644 src/Recyclarr/Command/Initialization/InitializationAutofacModule.cs delete mode 100644 src/Recyclarr/Command/Initialization/ServiceInitializationAndCleanup.cs delete mode 100644 src/Recyclarr/Command/Services/RadarrService.cs delete mode 100644 src/Recyclarr/Command/Services/ServiceBase.cs delete mode 100644 src/Recyclarr/Command/Services/SonarrService.cs create mode 100644 src/Recyclarr/ICompositionRoot.cs create mode 100644 src/Recyclarr/IServiceLocatorProxy.cs delete mode 100644 src/Recyclarr/Logging/DelayedFileSink.cs delete mode 100644 src/Recyclarr/Logging/IDelayedFileSink.cs create mode 100644 src/Recyclarr/ServiceLocatorProxy.cs create mode 100644 src/TestLibrary/AutoFixture/MockFileSystemSpecimenBuilder.cs 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)