fix: Improve app data migration logic

Smarter migration logic that does a directory merge instead of a
straight move. This is designed to fail less in cases like the
`recyclarr` directory already existing.
pull/76/head
Robert Dailey 2 years ago
parent d50e08b1e3
commit d499537f91

@ -28,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Version information in help output has been fixed. - Version information in help output has been fixed.
- If a HOME directory is not available, throw an error to the user (use `--app-data` instead). - If a HOME directory is not available, throw an error to the user (use `--app-data` instead).
- Create `$HOME/.config` (on Linux) if it does not exist. - Create `$HOME/.config` (on Linux) if it does not exist.
- Smarter migration logic in the `trash-updater` migration step that does a directory merge instead
of a straight move. This is designed to fail less in cases such as `recyclarr` directory already
existing.
[appdata]: https://github.com/recyclarr/recyclarr/wiki/File-Structure [appdata]: https://github.com/recyclarr/recyclarr/wiki/File-Structure

@ -5,6 +5,7 @@ using FluentAssertions;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Recyclarr.Command; using Recyclarr.Command;
using Recyclarr.Command.Initialization;
using Recyclarr.Command.Initialization.Init; using Recyclarr.Command.Initialization.Init;
using TestLibrary.AutoFixture; using TestLibrary.AutoFixture;
using TrashLib; using TrashLib;
@ -20,9 +21,11 @@ public class InitializeAppDataPathTest
[Frozen] IEnvironment env, [Frozen] IEnvironment env,
[Frozen] IAppPaths paths, [Frozen] IAppPaths paths,
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] DefaultAppDataSetup appDataSetup,
SonarrCommand cmd, SonarrCommand cmd,
InitializeAppDataPath sut) InitializeAppDataPath sut)
{ {
paths.DefaultAppDataDirectoryName.Returns("recyclarr");
env.GetFolderPath(Arg.Any<Environment.SpecialFolder>(), Arg.Any<Environment.SpecialFolderOption>()) env.GetFolderPath(Arg.Any<Environment.SpecialFolder>(), Arg.Any<Environment.SpecialFolderOption>())
.Returns("app_data"); .Returns("app_data");

@ -12,14 +12,15 @@ namespace Recyclarr.Tests.Migration.Steps;
[Parallelizable(ParallelScope.All)] [Parallelizable(ParallelScope.All)]
public class MigrateTrashUpdaterAppDataDirTest public class MigrateTrashUpdaterAppDataDirTest
{ {
private static readonly string BasePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); private const string BasePath = "base_path";
[Test, AutoMockData] [Test, AutoMockData]
public void Migration_check_returns_true_if_trash_updater_dir_exists( public void Migration_check_returns_true_if_trash_updater_dir_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths,
MigrateTrashUpdaterAppDataDir sut) MigrateTrashUpdaterAppDataDir sut)
{ {
fs.AddDirectory(Path.Combine(BasePath, "trash-updater")); fs.AddDirectory(fs.Path.Combine(paths.BasePath, "trash-updater"));
sut.CheckIfNeeded().Should().BeTrue(); sut.CheckIfNeeded().Should().BeTrue();
} }
@ -39,7 +40,7 @@ public class MigrateTrashUpdaterAppDataDirTest
fs.AddDirectory(Path.Combine(BasePath, "trash-updater")); fs.AddDirectory(Path.Combine(BasePath, "trash-updater"));
fs.AddDirectory(Path.Combine(BasePath, "recyclarr")); fs.AddDirectory(Path.Combine(BasePath, "recyclarr"));
var act = () => sut.Execute(); var act = sut.Execute;
act.Should().Throw<IOException>(); act.Should().Throw<IOException>();
} }
@ -47,12 +48,15 @@ public class MigrateTrashUpdaterAppDataDirTest
[Test, AutoMockData] [Test, AutoMockData]
public void Migration_success( public void Migration_success(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths,
MigrateTrashUpdaterAppDataDir sut) MigrateTrashUpdaterAppDataDir sut)
{ {
fs.AddDirectory(Path.Combine(BasePath, "trash-updater")); // Add file instead of directory since the migration step only operates on files
fs.AddFile(fs.Path.Combine(paths.BasePath, "trash-updater", "1", "2", "test.txt"), new MockFileData(""));
sut.Execute(); sut.Execute();
fs.AllDirectories.Should().ContainSingle(x => Regex.IsMatch(x, @"[/\\]recyclarr$")); fs.AllDirectories.Should().NotContain(x => x.Contains("trash-updater"));
fs.AllFiles.Should().Contain(x => Regex.IsMatch(x, @"[/\\]recyclarr[/\\]1[/\\]2[/\\]test.txt$"));
} }
} }

@ -0,0 +1,15 @@
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));
}
}

@ -15,6 +15,7 @@ public class AppPaths : IAppPaths
} }
public string DefaultConfigFilename => "recyclarr.yml"; public string DefaultConfigFilename => "recyclarr.yml";
public string DefaultAppDataDirectoryName => "recyclarr";
public bool IsAppDataPathValid => _appDataPath is not null; public bool IsAppDataPathValid => _appDataPath is not null;
public void SetAppDataPath(string path) => _appDataPath = path; public void SetAppDataPath(string path) => _appDataPath = path;

@ -0,0 +1,32 @@
using System.IO.Abstractions;
using Common;
using TrashLib;
namespace Recyclarr.Command.Initialization;
public class DefaultAppDataSetup : IDefaultAppDataSetup
{
private readonly IEnvironment _env;
private readonly IAppPaths _paths;
private readonly IFileSystem _fs;
public DefaultAppDataSetup(IEnvironment env, IAppPaths paths, IFileSystem fs)
{
_env = env;
_paths = paths;
_fs = fs;
}
public void SetupDefaultPath(bool forceCreate = false)
{
var appData = _env.GetFolderPath(Environment.SpecialFolder.ApplicationData,
forceCreate ? Environment.SpecialFolderOption.Create : Environment.SpecialFolderOption.None);
if (string.IsNullOrEmpty(appData))
{
throw new DirectoryNotFoundException("Unable to find the default app data directory");
}
_paths.SetAppDataPath(_fs.Path.Combine(appData, _paths.DefaultAppDataDirectoryName));
}
}

@ -0,0 +1,6 @@
namespace Recyclarr.Command.Initialization;
public interface IDefaultAppDataSetup
{
void SetupDefaultPath(bool forceCreate = false);
}

@ -10,12 +10,18 @@ public class InitializeAppDataPath : IServiceInitializer
private readonly IFileSystem _fs; private readonly IFileSystem _fs;
private readonly IAppPaths _paths; private readonly IAppPaths _paths;
private readonly IEnvironment _env; private readonly IEnvironment _env;
private readonly IDefaultAppDataSetup _appDataSetup;
public InitializeAppDataPath(IFileSystem fs, IAppPaths paths, IEnvironment env) public InitializeAppDataPath(
IFileSystem fs,
IAppPaths paths,
IEnvironment env,
IDefaultAppDataSetup appDataSetup)
{ {
_fs = fs; _fs = fs;
_paths = paths; _paths = paths;
_env = env; _env = env;
_appDataSetup = appDataSetup;
} }
public void Initialize(ServiceCommand cmd) public void Initialize(ServiceCommand cmd)
@ -36,11 +42,7 @@ public class InitializeAppDataPath : IServiceInitializer
// Set app data path to application directory value (e.g. `$HOME/.config` on Linux) and ensure it is // Set app data path to application directory value (e.g. `$HOME/.config` on Linux) and ensure it is
// created. // created.
var appData = _env.GetFolderPath( _appDataSetup.SetupDefaultPath(true);
Environment.SpecialFolder.ApplicationData,
Environment.SpecialFolderOption.Create);
_paths.SetAppDataPath(_fs.Path.Combine(appData, "recyclarr"));
} }
else else
{ {

@ -11,12 +11,13 @@ public class InitializationAutofacModule : Module
{ {
base.Load(builder); base.Load(builder);
builder.RegisterType<ServiceInitializationAndCleanup>().As<IServiceInitializationAndCleanup>(); builder.RegisterType<ServiceInitializationAndCleanup>().As<IServiceInitializationAndCleanup>();
builder.RegisterType<DefaultAppDataSetup>().As<IDefaultAppDataSetup>();
// Initialization Services // Initialization Services
builder.RegisterTypes( builder.RegisterTypes(
typeof(InitializeAppDataPath), typeof(InitializeAppDataPath),
typeof(ServiceInitializer), typeof(CheckMigrationNeeded),
typeof(CheckMigrationNeeded)) typeof(ServiceInitializer))
.As<IServiceInitializer>() .As<IServiceInitializer>()
.OrderByRegistration(); .OrderByRegistration();

@ -4,6 +4,7 @@ using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Infrastructure; using CliFx.Infrastructure;
using JetBrains.Annotations; using JetBrains.Annotations;
using Recyclarr.Command.Initialization;
using Recyclarr.Migration; using Recyclarr.Migration;
namespace Recyclarr.Command; namespace Recyclarr.Command;
@ -13,14 +14,17 @@ namespace Recyclarr.Command;
public class MigrateCommand : ICommand public class MigrateCommand : ICommand
{ {
private readonly IMigrationExecutor _migration; private readonly IMigrationExecutor _migration;
private readonly IDefaultAppDataSetup _appDataSetup;
public MigrateCommand(IMigrationExecutor migration) public MigrateCommand(IMigrationExecutor migration, IDefaultAppDataSetup appDataSetup)
{ {
_migration = migration; _migration = migration;
_appDataSetup = appDataSetup;
} }
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
_appDataSetup.SetupDefaultPath();
PerformMigrations(); PerformMigrations();
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }

@ -11,7 +11,11 @@ public class MigrationAutofacModule : Module
builder.RegisterType<MigrationExecutor>().As<IMigrationExecutor>(); builder.RegisterType<MigrationExecutor>().As<IMigrationExecutor>();
// Migration Steps // Migration Steps
builder.RegisterType<MigrateTrashYml>().As<IMigrationStep>(); builder.RegisterTypes
builder.RegisterType<MigrateTrashUpdaterAppDataDir>().As<IMigrationStep>(); (
typeof(MigrateTrashYml),
typeof(MigrateTrashUpdaterAppDataDir)
)
.As<IMigrationStep>();
} }
} }

@ -1,5 +1,7 @@
using System.IO.Abstractions; using System.IO.Abstractions;
using Common.Extensions;
using JetBrains.Annotations; using JetBrains.Annotations;
using TrashLib;
namespace Recyclarr.Migration.Steps; namespace Recyclarr.Migration.Steps;
@ -9,37 +11,35 @@ namespace Recyclarr.Migration.Steps;
[UsedImplicitly] [UsedImplicitly]
public class MigrateTrashUpdaterAppDataDir : IMigrationStep public class MigrateTrashUpdaterAppDataDir : IMigrationStep
{ {
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fs;
private readonly Lazy<string> _newPath, _oldPath;
private readonly string _oldPath =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "trash-updater");
// Do not use AppPaths class here since that may change yet again in the future and break this migration step.
private readonly string _newPath =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "recyclarr");
public int Order => 20; public int Order => 20;
public string Description { get; }
public IReadOnlyCollection<string> Remediation { get; }
public bool Required => true; public bool Required => true;
public MigrateTrashUpdaterAppDataDir(IFileSystem fileSystem) public string Description
=> $"Merge files from old app data directory `{GetOldPath()}` into `{GetNewPath()}` and delete old directory";
public IReadOnlyCollection<string> Remediation => new[]
{ {
_fileSystem = fileSystem; $"Check if `{GetNewPath()}` already exists. If so, manually copy all files from `{GetOldPath()}` and delete it to fix the error.",
Remediation = new[] $"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 settings you want and then delete `{_oldPath}` to fix the error.", };
$"Ensure Recyclarr has permission to recursively delete {_oldPath}",
$"Ensure Recyclarr has permission to create {_newPath}"
};
Description = $"Rename app data directory from `{_oldPath}` to `{_newPath}`";
}
public bool CheckIfNeeded() => _fileSystem.Directory.Exists(_oldPath); private string GetNewPath() => _newPath.Value;
private string GetOldPath() => _oldPath.Value;
public void Execute() public MigrateTrashUpdaterAppDataDir(IFileSystem fs, IAppPaths paths)
{ {
_fileSystem.Directory.Move(_oldPath, _newPath); _fs = fs;
// Will be something like `/home/user/.config/recyclarr`.
_newPath = new Lazy<string>(paths.GetAppDataPath);
_oldPath = new Lazy<string>(() => _fs.Path.Combine(_fs.Path.GetDirectoryName(GetNewPath()), "trash-updater"));
} }
public bool CheckIfNeeded() => _fs.Directory.Exists(GetOldPath());
public void Execute() => _fs.MergeDirectory(GetOldPath(), GetNewPath());
} }

@ -11,4 +11,5 @@ public interface IAppPaths
string CacheDirectory { get; } string CacheDirectory { get; }
string DefaultConfigFilename { get; } string DefaultConfigFilename { get; }
bool IsAppDataPathValid { get; } bool IsAppDataPathValid { get; }
string DefaultAppDataDirectoryName { get; }
} }

Loading…
Cancel
Save