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.gui
parent
acd452f300
commit
a8cce8164e
@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Recyclarr\Recyclarr.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -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"))
|
||||
{
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<string> Config => new List<string>();
|
||||
public string CacheStoragePath => "";
|
||||
public string Name => "";
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Resolve_NonServiceCommandType_NoActiveCommandSet()
|
||||
{
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterType<NonServiceCommandType>();
|
||||
var container = CompositionRoot.Setup(builder);
|
||||
|
||||
var createdType = CliTypeActivator.ResolveType(container, typeof(NonServiceCommandType));
|
||||
|
||||
Action act = () => _ = container.Resolve<IActiveServiceCommandProvider>().ActiveCommand;
|
||||
|
||||
createdType.Should().BeOfType<NonServiceCommandType>();
|
||||
act.Should()
|
||||
.Throw<InvalidOperationException>()
|
||||
.WithMessage("The active command has not yet been determined");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Resolve_ServiceCommandType_ActiveCommandSet()
|
||||
{
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterType<StubCommand>();
|
||||
var container = CompositionRoot.Setup(builder);
|
||||
|
||||
var createdType = CliTypeActivator.ResolveType(container, typeof(StubCommand));
|
||||
var activeCommand = container.Resolve<IActiveServiceCommandProvider>().ActiveCommand;
|
||||
|
||||
activeCommand.Should().BeSameAs(createdType);
|
||||
activeCommand.Should().BeOfType<StubCommand>();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<IServiceInitializer>().OrderBy(_ => 1),
|
||||
new[] {cleaner}.OrderBy(_ => 1));
|
||||
|
||||
var act = () => sut.Execute(cmd, () => throw new NullReferenceException());
|
||||
|
||||
await act.Should().ThrowAsync<NullReferenceException>();
|
||||
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<NullReferenceException>();
|
||||
cleaner.Received().Cleanup();
|
||||
}
|
||||
}
|
@ -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!);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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<IAppPaths>();
|
||||
var janitor = container.Resolve<ILogJanitor>();
|
||||
var log = container.Resolve<ILogger>();
|
||||
|
||||
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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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<IServiceCommand>())
|
||||
{
|
||||
var activeServiceProvider = container.Resolve<IActiveServiceCommandProvider>();
|
||||
activeServiceProvider.ActiveCommand = (IServiceCommand) instance;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.Command;
|
||||
|
||||
public interface IRadarrCommand : IServiceCommand
|
||||
{
|
||||
bool ListCustomFormats { get; }
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace Recyclarr.Command;
|
||||
|
||||
public interface ISonarrCommand : IServiceCommand
|
||||
{
|
||||
bool ListReleaseProfiles { get; }
|
||||
string? ListTerms { get; }
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.Command.Initialization.Cleanup;
|
||||
|
||||
public interface IServiceCleaner
|
||||
{
|
||||
void Cleanup();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.Command.Initialization;
|
||||
|
||||
public interface IDefaultAppDataSetup
|
||||
{
|
||||
void SetupDefaultPath(string? appDataDirectoryOverride, bool forceCreate);
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.Command.Initialization;
|
||||
|
||||
public interface IServiceInitializationAndCleanup
|
||||
{
|
||||
Task Execute(ServiceCommand cmd, Func<Task> logic);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.Command.Initialization.Init;
|
||||
|
||||
public interface IServiceInitializer
|
||||
{
|
||||
void Initialize(ServiceCommand cmd);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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<ServiceInitializationAndCleanup>().As<IServiceInitializationAndCleanup>();
|
||||
builder.RegisterType<DefaultAppDataSetup>().As<IDefaultAppDataSetup>();
|
||||
|
||||
// Initialization Services
|
||||
builder.RegisterTypes(
|
||||
typeof(InitializeAppDataPath),
|
||||
typeof(CheckMigrationNeeded),
|
||||
typeof(ServiceInitializer))
|
||||
.As<IServiceInitializer>()
|
||||
.OrderByRegistration();
|
||||
|
||||
// Cleanup Services
|
||||
builder.RegisterTypes(
|
||||
typeof(OldLogFileCleaner))
|
||||
.As<IServiceCleaner>()
|
||||
.OrderByRegistration();
|
||||
}
|
||||
}
|
@ -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<IServiceInitializer> _initializers;
|
||||
private readonly IOrderedEnumerable<IServiceCleaner> _cleaners;
|
||||
|
||||
public ServiceInitializationAndCleanup(
|
||||
IOrderedEnumerable<IServiceInitializer> initializers,
|
||||
IOrderedEnumerable<IServiceCleaner> cleaners)
|
||||
{
|
||||
_initializers = initializers;
|
||||
_cleaners = cleaners;
|
||||
}
|
||||
|
||||
public async Task Execute(ServiceCommand cmd, Func<Task> logic)
|
||||
{
|
||||
try
|
||||
{
|
||||
_initializers.ForEach(x => x.Initialize(cmd));
|
||||
|
||||
await logic();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cleaners.ForEach(x => x.Cleanup());
|
||||
}
|
||||
}
|
||||
}
|
@ -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<RadarrService> _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<RadarrService> service)
|
||||
: base(init)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
var lister = container.Resolve<ICustomFormatLister>();
|
||||
var log = container.Resolve<ILogger>();
|
||||
var customFormatUpdaterFactory = container.Resolve<Func<ICustomFormatUpdater>>();
|
||||
var qualityUpdaterFactory = container.Resolve<Func<IRadarrQualityDefinitionUpdater>>();
|
||||
var configLoader = container.Resolve<IConfigurationLoader<RadarrConfiguration>>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<IRadarrCommand>
|
||||
{
|
||||
private readonly IConfigurationLoader<RadarrConfiguration> _configLoader;
|
||||
private readonly Func<ICustomFormatUpdater> _customFormatUpdaterFactory;
|
||||
private readonly ICustomFormatLister _lister;
|
||||
private readonly IFileSystem _fs;
|
||||
private readonly IAppPaths _paths;
|
||||
private readonly ILogger _log;
|
||||
private readonly Func<IRadarrQualityDefinitionUpdater> _qualityUpdaterFactory;
|
||||
|
||||
public RadarrService(
|
||||
ILogger log,
|
||||
IConfigurationLoader<RadarrConfiguration> configLoader,
|
||||
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
|
||||
Func<ICustomFormatUpdater> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
using System.Text;
|
||||
using CliFx.Exceptions;
|
||||
using Flurl.Http;
|
||||
using TrashLib.Extensions;
|
||||
using YamlDotNet.Core;
|
||||
|
||||
namespace Recyclarr.Command.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Mainly intended to handle common exception recovery logic between service command classes.
|
||||
/// </summary>
|
||||
public abstract class ServiceBase<T> 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);
|
||||
}
|
@ -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<ISonarrCommand>
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
private readonly IConfigurationLoader<SonarrConfiguration> _configLoader;
|
||||
private readonly Func<IReleaseProfileUpdater> _profileUpdaterFactory;
|
||||
private readonly Func<ISonarrQualityDefinitionUpdater> _qualityUpdaterFactory;
|
||||
private readonly IReleaseProfileLister _lister;
|
||||
private readonly IFileSystem _fs;
|
||||
private readonly IAppPaths _paths;
|
||||
|
||||
public SonarrService(
|
||||
ILogger log,
|
||||
IConfigurationLoader<SonarrConfiguration> configLoader,
|
||||
Func<IReleaseProfileUpdater> profileUpdaterFactory,
|
||||
Func<ISonarrQualityDefinitionUpdater> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Autofac;
|
||||
|
||||
namespace Recyclarr;
|
||||
|
||||
public interface IServiceLocatorProxy : IDisposable
|
||||
{
|
||||
ILifetimeScope Container { get; }
|
||||
T Resolve<T>() where T : notnull;
|
||||
}
|
@ -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<StreamWriter> _stream;
|
||||
private ITextFormatter? _formatter;
|
||||
|
||||
public DelayedFileSink(IAppPaths paths, IFileSystem fs)
|
||||
{
|
||||
_paths = paths;
|
||||
_stream = new Lazy<StreamWriter>(() =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
using Serilog.Core;
|
||||
|
||||
namespace Recyclarr.Logging;
|
||||
|
||||
public interface IDelayedFileSink : ILogEventSink, IDisposable
|
||||
{
|
||||
void SetTemplate(string template);
|
||||
}
|
@ -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<IDelayedFileSink> _fileSinkFactory;
|
||||
private const string ConsoleTemplate = "[{Level:u3}] {Message:lj}{NewLine}{Exception}";
|
||||
|
||||
public LoggerFactory(LoggingLevelSwitch logLevel, Func<IDelayedFileSink> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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<T>() where T : notnull
|
||||
{
|
||||
return Container.Resolve<T>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Container.Dispose();
|
||||
}
|
||||
}
|
@ -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<IFileInfo>(x => x.FromFactory(() =>
|
||||
{
|
||||
var name = $"MockFile-{fixture.Create<string>()}";
|
||||
return fs.CurrentDirectory().File(name);
|
||||
}));
|
||||
|
||||
fixture.Customize<IDirectoryInfo>(x => x.FromFactory(() =>
|
||||
{
|
||||
var name = $"MockDirectory-{fixture.Create<string>()}";
|
||||
return fs.CurrentDirectory().SubDirectory(name);
|
||||
}));
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace TrashLib;
|
||||
|
||||
public interface IConfigurationFinder
|
||||
{
|
||||
string FindConfigPath();
|
||||
IFileInfo FindConfigPath();
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace TrashLib.Repo;
|
||||
|
||||
public interface IRepoUpdater
|
||||
{
|
||||
string RepoPath { get; }
|
||||
IDirectoryInfo RepoPath { get; }
|
||||
void UpdateRepo();
|
||||
}
|
||||
|
Loading…
Reference in new issue