From 035836db49260ba03101ac113920cb817505404f Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Tue, 24 May 2022 09:49:16 -0500 Subject: [PATCH] fix: Better safeguards for HOME directory usage - Attempt to detect if `HOME` is defined and available. If not, error out. - Attempt to create `$HOME/.config` if `$HOME` is available. - If logic in code attempts to grab the app data dir path before it's set up, an exception is thrown. --- CHANGELOG.md | 2 ++ src/Common/DefaultEnvironment.cs | 7 +++++- src/Common/IEnvironment.cs | 3 ++- .../Init/InitializeAppDataPathTest.cs | 13 ++++++---- src/Recyclarr/AppPaths.cs | 19 +++++++++------ .../Init/InitializeAppDataPath.cs | 24 ++++++++++++++++++- src/TrashLib/IAppPaths.cs | 2 +- 7 files changed, 55 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a83c9ce..6d734342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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). +- Create `$HOME/.config` (on Linux) if it does not exist. [appdata]: https://github.com/recyclarr/recyclarr/wiki/File-Structure diff --git a/src/Common/DefaultEnvironment.cs b/src/Common/DefaultEnvironment.cs index 986e326f..e77d052d 100644 --- a/src/Common/DefaultEnvironment.cs +++ b/src/Common/DefaultEnvironment.cs @@ -1,4 +1,4 @@ -namespace Common; +namespace Common; internal class DefaultEnvironment : IEnvironment { @@ -6,4 +6,9 @@ internal class DefaultEnvironment : IEnvironment { return Environment.GetFolderPath(folder); } + + public string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option) + { + return Environment.GetFolderPath(folder, option); + } } diff --git a/src/Common/IEnvironment.cs b/src/Common/IEnvironment.cs index d05bced3..51fd387d 100644 --- a/src/Common/IEnvironment.cs +++ b/src/Common/IEnvironment.cs @@ -1,6 +1,7 @@ -namespace Common; +namespace Common; public interface IEnvironment { public string GetFolderPath(Environment.SpecialFolder folder); + string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option); } diff --git a/src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs b/src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs index 43196b74..9142513e 100644 --- a/src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs +++ b/src/Recyclarr.Tests/Command/Initialization/Init/InitializeAppDataPathTest.cs @@ -1,4 +1,5 @@ using AutoFixture.NUnit3; +using Common; using NSubstitute; using NUnit.Framework; using Recyclarr.Command; @@ -13,25 +14,29 @@ namespace Recyclarr.Tests.Command.Initialization.Init; public class InitializeAppDataPathTest { [Test, AutoMockData] - public void Do_not_override_path_if_null( + public void Use_default_app_data_if_not_specified( + [Frozen] IEnvironment env, [Frozen] IAppPaths paths, SonarrCommand cmd, InitializeAppDataPath sut) { + env.GetFolderPath(Arg.Any(), Arg.Any()) + .Returns("app_data"); + sut.Initialize(cmd); - paths.DidNotReceiveWithAnyArgs().SetAppDataPath(default!); + env.Received().GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create); + paths.Received().SetAppDataPath("app_data"); } [Test, AutoMockData] - public void Override_path_if_not_null( + public void Use_specified_app_data_if_user_provided( [Frozen] IAppPaths paths, SonarrCommand cmd, InitializeAppDataPath sut) { cmd.AppDataDirectory = "path"; sut.Initialize(cmd); - paths.Received().SetAppDataPath("path"); } } diff --git a/src/Recyclarr/AppPaths.cs b/src/Recyclarr/AppPaths.cs index 1b485de9..af4b6772 100644 --- a/src/Recyclarr/AppPaths.cs +++ b/src/Recyclarr/AppPaths.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; using TrashLib; @@ -6,6 +7,7 @@ namespace Recyclarr; public class AppPaths : IAppPaths { private readonly IFileSystem _fs; + private string? _appDataPath; public AppPaths(IFileSystem fs) { @@ -14,12 +16,15 @@ public class AppPaths : IAppPaths public string DefaultConfigFilename => "recyclarr.yml"; - public void SetAppDataPath(string path) => AppDataPath = path; + public void SetAppDataPath(string path) => _appDataPath = path; - public string AppDataPath { get; private set; } = ""; - public string ConfigPath => _fs.Path.Combine(AppDataPath, DefaultConfigFilename); - public string SettingsPath => _fs.Path.Combine(AppDataPath, "settings.yml"); - public string LogDirectory => _fs.Path.Combine(AppDataPath, "logs"); - public string RepoDirectory => _fs.Path.Combine(AppDataPath, "repo"); - public string CacheDirectory => _fs.Path.Combine(AppDataPath, "cache"); + [SuppressMessage("Design", "CA1024:Use properties where appropriate")] + public string GetAppDataPath() + => _appDataPath ?? throw new DirectoryNotFoundException("Application data directory not set!"); + + 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"); } diff --git a/src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs b/src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs index 521e67be..7d1adc4a 100644 --- a/src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs +++ b/src/Recyclarr/Command/Initialization/Init/InitializeAppDataPath.cs @@ -1,4 +1,6 @@ using System.IO.Abstractions; +using CliFx.Exceptions; +using Common; using TrashLib; namespace Recyclarr.Command.Initialization.Init; @@ -7,20 +9,40 @@ public class InitializeAppDataPath : IServiceInitializer { private readonly IFileSystem _fs; private readonly IAppPaths _paths; + private readonly IEnvironment _env; - public InitializeAppDataPath(IFileSystem fs, IAppPaths paths) + public InitializeAppDataPath(IFileSystem fs, IAppPaths paths, IEnvironment env) { _fs = fs; _paths = paths; + _env = env; } public void Initialize(ServiceCommand cmd) { + // 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(cmd.AppDataDirectory)) { + // 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."); + } + + // Set app data path to application directory value (e.g. `$HOME/.config` on Linux) and ensure it is + // created. + _paths.SetAppDataPath(_env.GetFolderPath(Environment.SpecialFolder.ApplicationData, + Environment.SpecialFolderOption.Create)); + return; } + // Ensure user-specified app data directory is created and use it. _fs.Directory.CreateDirectory(cmd.AppDataDirectory); _paths.SetAppDataPath(cmd.AppDataDirectory); } diff --git a/src/TrashLib/IAppPaths.cs b/src/TrashLib/IAppPaths.cs index 01e1388c..929f6d68 100644 --- a/src/TrashLib/IAppPaths.cs +++ b/src/TrashLib/IAppPaths.cs @@ -3,7 +3,7 @@ namespace TrashLib; public interface IAppPaths { void SetAppDataPath(string path); - string AppDataPath { get; } + string GetAppDataPath(); string ConfigPath { get; } string SettingsPath { get; } string LogDirectory { get; }