diff --git a/Recyclarr.sln.DotSettings b/Recyclarr.sln.DotSettings index 0ca91d85..8a20513b 100644 --- a/Recyclarr.sln.DotSettings +++ b/Recyclarr.sln.DotSettings @@ -114,5 +114,6 @@ True True True + True True True \ No newline at end of file diff --git a/src/Recyclarr.Cli/Migration/MigrationAutofacModule.cs b/src/Recyclarr.Cli/Migration/MigrationAutofacModule.cs index 34f1ca67..1dfb49ef 100644 --- a/src/Recyclarr.Cli/Migration/MigrationAutofacModule.cs +++ b/src/Recyclarr.Cli/Migration/MigrationAutofacModule.cs @@ -1,4 +1,5 @@ using Autofac; +using Autofac.Extras.Ordering; using Recyclarr.Cli.Migration.Steps; namespace Recyclarr.Cli.Migration; @@ -11,8 +12,10 @@ public class MigrationAutofacModule : Module builder.RegisterType().As(); // Migration Steps - builder.RegisterAssemblyTypes(ThisAssembly) - .AssignableTo() - .As(); + builder.RegisterTypes( + typeof(MoveOsxAppDataDotnet8), + typeof(DeleteRepoDirMigrationStep)) + .As() + .OrderByRegistration(); } } diff --git a/src/Recyclarr.Cli/Migration/MigrationExecutor.cs b/src/Recyclarr.Cli/Migration/MigrationExecutor.cs index fad43541..da3b8f7e 100644 --- a/src/Recyclarr.Cli/Migration/MigrationExecutor.cs +++ b/src/Recyclarr.Cli/Migration/MigrationExecutor.cs @@ -3,20 +3,11 @@ using Spectre.Console; namespace Recyclarr.Cli.Migration; -public class MigrationExecutor : IMigrationExecutor +public class MigrationExecutor(IOrderedEnumerable migrationSteps, IAnsiConsole console) + : IMigrationExecutor { - private readonly IAnsiConsole _console; - private readonly List _migrationSteps; - - public MigrationExecutor(IEnumerable migrationSteps, IAnsiConsole console) - { - _console = console; - _migrationSteps = migrationSteps.OrderBy(x => x.Order).ToList(); - } - - private void PerformMigrationStepsImpl(bool withDiagnostics, IEnumerable migrationSteps) + public void PerformAllMigrationSteps(bool withDiagnostics) { - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator foreach (var step in migrationSteps) { // Do not use LINQ to filter using CheckIfNeeded(). If it returns true, then 'Execute()' must be invoked to @@ -29,25 +20,20 @@ public class MigrationExecutor : IMigrationExecutor try { - step.Execute(withDiagnostics ? _console : null); + step.Execute(withDiagnostics ? console : null); } catch (Exception e) when (e is not MigrationException) { throw new MigrationException(e, step.Description, step.Remediation); } - _console.WriteLine($"Migrate: {step.Description}"); + console.WriteLine($"Migrate: {step.Description}"); } } - public void PerformAllMigrationSteps(bool withDiagnostics) - { - PerformMigrationStepsImpl(withDiagnostics, _migrationSteps); - } - public void CheckNeededMigrations() { - var neededMigrationSteps = _migrationSteps.Where(x => x.CheckIfNeeded()).ToList(); + var neededMigrationSteps = migrationSteps.Where(x => x.CheckIfNeeded()).ToList(); if (neededMigrationSteps.Count == 0) { return; @@ -58,11 +44,11 @@ public class MigrationExecutor : IMigrationExecutor foreach (var step in neededMigrationSteps) { var requiredText = step.Required ? "Required" : "Not Required"; - _console.WriteLine($"Migration Needed ({requiredText}): {step.Description}"); + console.WriteLine($"Migration Needed ({requiredText}): {step.Description}"); wereAnyRequired |= step.Required; } - _console.WriteLine( + console.WriteLine( "\nRun the `migrate` subcommand to perform the above migration steps automatically\n"); if (wereAnyRequired) diff --git a/src/Recyclarr.Cli/Migration/Steps/DeleteRepoDirMigrationStep.cs b/src/Recyclarr.Cli/Migration/Steps/DeleteRepoDirMigrationStep.cs index da74b705..295fbc68 100644 --- a/src/Recyclarr.Cli/Migration/Steps/DeleteRepoDirMigrationStep.cs +++ b/src/Recyclarr.Cli/Migration/Steps/DeleteRepoDirMigrationStep.cs @@ -9,7 +9,6 @@ namespace Recyclarr.Cli.Migration.Steps; [UsedImplicitly] public class DeleteRepoDirMigrationStep(IAppPaths paths) : IMigrationStep { - public int Order => 1; public string Description => "Delete old repo directory"; public IReadOnlyCollection Remediation => new[] { diff --git a/src/Recyclarr.Cli/Migration/Steps/IMigrationStep.cs b/src/Recyclarr.Cli/Migration/Steps/IMigrationStep.cs index ad3102b5..e3cfa11f 100644 --- a/src/Recyclarr.Cli/Migration/Steps/IMigrationStep.cs +++ b/src/Recyclarr.Cli/Migration/Steps/IMigrationStep.cs @@ -4,11 +4,6 @@ namespace Recyclarr.Cli.Migration.Steps; public interface IMigrationStep { - /// - /// Determines the order in which this migration step will run. - /// - int Order { get; } - /// /// A description printed to the user so that they understand the purpose of this migration step, and /// what it does. diff --git a/src/Recyclarr.Cli/Migration/Steps/MoveOsxAppDataDotnet8.cs b/src/Recyclarr.Cli/Migration/Steps/MoveOsxAppDataDotnet8.cs new file mode 100644 index 00000000..cb72023a --- /dev/null +++ b/src/Recyclarr.Cli/Migration/Steps/MoveOsxAppDataDotnet8.cs @@ -0,0 +1,43 @@ +using System.IO.Abstractions; +using JetBrains.Annotations; +using Recyclarr.Common.Extensions; +using Recyclarr.Platform; +using Spectre.Console; + +namespace Recyclarr.Cli.Migration.Steps; + +[UsedImplicitly] +public class MoveOsxAppDataDotnet8( + IAppPaths paths, + IEnvironment env, + IRuntimeInformation runtimeInfo, + IFileSystem fs) + : IMigrationStep +{ + public string Description => "Migrate OSX app data to 'Library/Application Support'"; + public IReadOnlyCollection Remediation => new[] + { + $"Ensure Recyclarr has permission to move {OldAppDataDir} to {NewAppDataDir} and try again", + $"Move {OldAppDataDir} to {NewAppDataDir} manually if Recyclarr can't do it" + }; + + public bool Required => true; + + private IDirectoryInfo OldAppDataDir => fs.DirectoryInfo + .New(env.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .SubDir(".config", AppPaths.DefaultAppDataDirectoryName); + + private IDirectoryInfo NewAppDataDir => paths.AppDataDirectory; + + public bool CheckIfNeeded() + { + return runtimeInfo.IsPlatformOsx() && OldAppDataDir.Exists; + } + + public void Execute(IAnsiConsole? console) + { + NewAppDataDir.Create(); + OldAppDataDir.MoveTo(NewAppDataDir.FullName); + console?.WriteLine($"Moved app settings dir from '{OldAppDataDir}' to '{NewAppDataDir}'"); + } +} diff --git a/src/Recyclarr.Common/CommonAutofacModule.cs b/src/Recyclarr.Common/CommonAutofacModule.cs index 75da07ba..ace46949 100644 --- a/src/Recyclarr.Common/CommonAutofacModule.cs +++ b/src/Recyclarr.Common/CommonAutofacModule.cs @@ -8,7 +8,6 @@ public class CommonAutofacModule : Module protected override void Load(ContainerBuilder builder) { base.Load(builder); - builder.RegisterType().As(); builder.RegisterType().As(); } } diff --git a/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesDeserializer.cs b/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesDeserializer.cs index 72197242..cde00c1c 100644 --- a/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesDeserializer.cs +++ b/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesDeserializer.cs @@ -1,5 +1,5 @@ using System.Diagnostics.CodeAnalysis; -using Recyclarr.Common; +using Recyclarr.Platform; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; diff --git a/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesYamlBehavior.cs b/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesYamlBehavior.cs index ed1b4ea8..43afbba3 100644 --- a/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesYamlBehavior.cs +++ b/src/Recyclarr.Config/EnvironmentVariables/EnvironmentVariablesYamlBehavior.cs @@ -1,5 +1,5 @@ using JetBrains.Annotations; -using Recyclarr.Common; +using Recyclarr.Platform; using Recyclarr.Yaml; using YamlDotNet.Serialization; diff --git a/src/Recyclarr.Platform/DefaultAppDataSetup.cs b/src/Recyclarr.Platform/DefaultAppDataSetup.cs index e7f146c6..7e547469 100644 --- a/src/Recyclarr.Platform/DefaultAppDataSetup.cs +++ b/src/Recyclarr.Platform/DefaultAppDataSetup.cs @@ -1,17 +1,19 @@ using System.IO.Abstractions; -using Recyclarr.Common; namespace Recyclarr.Platform; -public class DefaultAppDataSetup(IEnvironment env, IFileSystem fs) +public class DefaultAppDataSetup( + IEnvironment env, + IFileSystem fs, + IRuntimeInformation runtimeInfo) { - public IAppPaths CreateAppPaths(string? appDataDirectoryOverride = null, bool forceCreate = true) + public IAppPaths CreateAppPaths(string? appDataDirectoryOverride = null) { - var appDir = GetAppDataDirectory(appDataDirectoryOverride, forceCreate); + var appDir = GetAppDataDirectory(appDataDirectoryOverride); return new AppPaths(fs.DirectoryInfo.New(appDir)); } - private string GetAppDataDirectory(string? appDataDirectoryOverride, bool forceCreate) + private string GetAppDataDirectory(string? appDataDirectoryOverride) { // If a specific app data directory is not provided, use the following environment variable to find the path. appDataDirectoryOverride ??= env.GetEnvironmentVariable("RECYCLARR_APP_DATA"); @@ -22,26 +24,40 @@ public class DefaultAppDataSetup(IEnvironment env, IFileSystem fs) return fs.Directory.CreateDirectory(appDataDirectoryOverride).FullName; } - // 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)) + // 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, + Environment.SpecialFolderOption.Create); + + if (string.IsNullOrEmpty(appData)) { throw new NoHomeDirectoryException( - "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"); + "Unable to find or create the default app data directory. The application cannot determine where " + + "to place data files. Please use the --app-data option to explicitly set a location for these files."); } - // 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); + appData = fs.Path.Combine(appData, AppPaths.DefaultAppDataDirectoryName); - if (string.IsNullOrEmpty(appData)) + try + { + if (runtimeInfo.IsPlatformOsx()) + { + var oldAppData = fs.Path.Combine(env.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", + AppPaths.DefaultAppDataDirectoryName); + if (fs.DirectoryInfo.New(oldAppData).Exists) + { + // Attempt to move the directory for the user. If this cannot be done, then the MoveOsxAppDataDotnet8 + // migration step (which is required) will force the issue to the user and provide remediation steps. + fs.Directory.Move(oldAppData, appData); + } + } + } + catch (IOException) { - throw new DirectoryNotFoundException("Unable to find the default app data directory"); + // Ignore failures here because we'll let the migration step take care of it. } - return fs.Path.Combine(appData, AppPaths.DefaultAppDataDirectoryName); + return appData; } } diff --git a/src/Recyclarr.Common/DefaultEnvironment.cs b/src/Recyclarr.Platform/DefaultEnvironment.cs similarity index 93% rename from src/Recyclarr.Common/DefaultEnvironment.cs rename to src/Recyclarr.Platform/DefaultEnvironment.cs index 7b166f57..3957a634 100644 --- a/src/Recyclarr.Common/DefaultEnvironment.cs +++ b/src/Recyclarr.Platform/DefaultEnvironment.cs @@ -1,4 +1,4 @@ -namespace Recyclarr.Common; +namespace Recyclarr.Platform; public class DefaultEnvironment : IEnvironment { diff --git a/src/Recyclarr.Platform/DefaultRuntimeInformation.cs b/src/Recyclarr.Platform/DefaultRuntimeInformation.cs new file mode 100644 index 00000000..5f507955 --- /dev/null +++ b/src/Recyclarr.Platform/DefaultRuntimeInformation.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Recyclarr.Platform; + +public class DefaultRuntimeInformation : IRuntimeInformation +{ + public bool IsPlatformOsx() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } +} diff --git a/src/Recyclarr.Common/IEnvironment.cs b/src/Recyclarr.Platform/IEnvironment.cs similarity index 88% rename from src/Recyclarr.Common/IEnvironment.cs rename to src/Recyclarr.Platform/IEnvironment.cs index 2a2a343d..c8aa0926 100644 --- a/src/Recyclarr.Common/IEnvironment.cs +++ b/src/Recyclarr.Platform/IEnvironment.cs @@ -1,4 +1,4 @@ -namespace Recyclarr.Common; +namespace Recyclarr.Platform; public interface IEnvironment { diff --git a/src/Recyclarr.Platform/IRuntimeInformation.cs b/src/Recyclarr.Platform/IRuntimeInformation.cs new file mode 100644 index 00000000..50139f33 --- /dev/null +++ b/src/Recyclarr.Platform/IRuntimeInformation.cs @@ -0,0 +1,6 @@ +namespace Recyclarr.Platform; + +public interface IRuntimeInformation +{ + bool IsPlatformOsx(); +} diff --git a/src/Recyclarr.Platform/PlatformAutofacModule.cs b/src/Recyclarr.Platform/PlatformAutofacModule.cs index dbe56c17..0ec06399 100644 --- a/src/Recyclarr.Platform/PlatformAutofacModule.cs +++ b/src/Recyclarr.Platform/PlatformAutofacModule.cs @@ -13,6 +13,8 @@ public class PlatformAutofacModule : Module private static void RegisterAppPaths(ContainerBuilder builder) { builder.RegisterType(); + builder.RegisterType().As(); + builder.RegisterType().As(); builder.Register(c => { diff --git a/tests/Recyclarr.Cli.Tests/Migration/MigrationExecutorTest.cs b/tests/Recyclarr.Cli.Tests/Migration/MigrationExecutorTest.cs index 0ec60421..b6602f83 100644 --- a/tests/Recyclarr.Cli.Tests/Migration/MigrationExecutorTest.cs +++ b/tests/Recyclarr.Cli.Tests/Migration/MigrationExecutorTest.cs @@ -1,3 +1,4 @@ +using Autofac.Extras.Ordering; using Recyclarr.Cli.Migration; using Recyclarr.Cli.Migration.Steps; using Spectre.Console.Testing; @@ -13,7 +14,7 @@ public class MigrationExecutorTest { using var console = new TestConsole(); var step = Substitute.For(); - var executor = new MigrationExecutor(new[] {step}, console); + var executor = new MigrationExecutor(new[] {step}.AsOrdered(), console); step.CheckIfNeeded().Returns(false); @@ -28,7 +29,7 @@ public class MigrationExecutorTest { using var console = new TestConsole(); var step = Substitute.For(); - var executor = new MigrationExecutor(new[] {step}, console); + var executor = new MigrationExecutor(new[] {step}.AsOrdered(), console); step.CheckIfNeeded().Returns(true); @@ -50,18 +51,14 @@ public class MigrationExecutorTest Substitute.For() }; - steps[0].Order.Returns(20); - steps[1].Order.Returns(10); - steps[2].Order.Returns(30); - - var executor = new MigrationExecutor(steps, console); + var executor = new MigrationExecutor(steps.AsOrdered(), console); executor.PerformAllMigrationSteps(false); Received.InOrder(() => { - steps[1].CheckIfNeeded(); steps[0].CheckIfNeeded(); + steps[1].CheckIfNeeded(); steps[2].CheckIfNeeded(); }); } @@ -71,7 +68,7 @@ public class MigrationExecutorTest { using var console = new TestConsole(); var step = Substitute.For(); - var executor = new MigrationExecutor(new[] {step}, console); + var executor = new MigrationExecutor(new[] {step}.AsOrdered(), console); step.CheckIfNeeded().Returns(true); step.When(x => x.Execute(null)).Throw(new ArgumentException("test message")); @@ -86,7 +83,7 @@ public class MigrationExecutorTest { using var console = new TestConsole(); var step = Substitute.For(); - var executor = new MigrationExecutor(new[] {step}, console); + var executor = new MigrationExecutor(new[] {step}.AsOrdered(), console); var exception = new MigrationException(new ArgumentException(), "a", new[] {"b"}); step.CheckIfNeeded().Returns(true); diff --git a/tests/Recyclarr.IntegrationTests/ConfigurationLoaderEnvVarTest.cs b/tests/Recyclarr.IntegrationTests/ConfigurationLoaderEnvVarTest.cs index 57b4da21..7ce42b3d 100644 --- a/tests/Recyclarr.IntegrationTests/ConfigurationLoaderEnvVarTest.cs +++ b/tests/Recyclarr.IntegrationTests/ConfigurationLoaderEnvVarTest.cs @@ -1,5 +1,5 @@ -using Recyclarr.Common; using Recyclarr.Config.Parsing; +using Recyclarr.Platform; namespace Recyclarr.IntegrationTests; diff --git a/tests/Recyclarr.Tests.TestLibrary/TestAppPaths.cs b/tests/Recyclarr.Tests.TestLibrary/TestAppPaths.cs index 73677207..173c6642 100644 --- a/tests/Recyclarr.Tests.TestLibrary/TestAppPaths.cs +++ b/tests/Recyclarr.Tests.TestLibrary/TestAppPaths.cs @@ -3,4 +3,4 @@ using Recyclarr.Platform; namespace Recyclarr.Tests.TestLibrary; -public sealed class TestAppPaths(IFileSystem fs) : AppPaths(fs.CurrentDirectory().SubDirectory("recyclarr")); +public sealed class TestAppPaths(IFileSystem fs) : AppPaths(fs.CurrentDirectory().SubDirectory("app")); diff --git a/tests/Recyclarr.Tests/Platform/DefaultAppDataSetupTest.cs b/tests/Recyclarr.Tests/Platform/DefaultAppDataSetupTest.cs index 23fe16bf..52cd2e1b 100644 --- a/tests/Recyclarr.Tests/Platform/DefaultAppDataSetupTest.cs +++ b/tests/Recyclarr.Tests/Platform/DefaultAppDataSetupTest.cs @@ -1,5 +1,4 @@ using System.IO.Abstractions; -using Recyclarr.Common; using Recyclarr.Platform; namespace Recyclarr.Tests.Platform; @@ -42,26 +41,7 @@ public class DefaultAppDataSetupTest } [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.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( + public void Creation_uses_correct_behavior( [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen] IEnvironment env, DefaultAppDataSetup sut)