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)