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.
pull/76/head
Robert Dailey 2 years ago
parent e14357fd73
commit 035836db49

@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- 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).
- Create `$HOME/.config` (on Linux) if it does not exist.
[appdata]: https://github.com/recyclarr/recyclarr/wiki/File-Structure [appdata]: https://github.com/recyclarr/recyclarr/wiki/File-Structure

@ -1,4 +1,4 @@
namespace Common; namespace Common;
internal class DefaultEnvironment : IEnvironment internal class DefaultEnvironment : IEnvironment
{ {
@ -6,4 +6,9 @@ internal class DefaultEnvironment : IEnvironment
{ {
return Environment.GetFolderPath(folder); return Environment.GetFolderPath(folder);
} }
public string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option)
{
return Environment.GetFolderPath(folder, option);
}
} }

@ -1,6 +1,7 @@
namespace Common; namespace Common;
public interface IEnvironment public interface IEnvironment
{ {
public string GetFolderPath(Environment.SpecialFolder folder); public string GetFolderPath(Environment.SpecialFolder folder);
string GetFolderPath(Environment.SpecialFolder folder, Environment.SpecialFolderOption option);
} }

@ -1,4 +1,5 @@
using AutoFixture.NUnit3; using AutoFixture.NUnit3;
using Common;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Recyclarr.Command; using Recyclarr.Command;
@ -13,25 +14,29 @@ namespace Recyclarr.Tests.Command.Initialization.Init;
public class InitializeAppDataPathTest public class InitializeAppDataPathTest
{ {
[Test, AutoMockData] [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, [Frozen] IAppPaths paths,
SonarrCommand cmd, SonarrCommand cmd,
InitializeAppDataPath sut) InitializeAppDataPath sut)
{ {
env.GetFolderPath(Arg.Any<Environment.SpecialFolder>(), Arg.Any<Environment.SpecialFolderOption>())
.Returns("app_data");
sut.Initialize(cmd); sut.Initialize(cmd);
paths.DidNotReceiveWithAnyArgs().SetAppDataPath(default!); env.Received().GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create);
paths.Received().SetAppDataPath("app_data");
} }
[Test, AutoMockData] [Test, AutoMockData]
public void Override_path_if_not_null( public void Use_specified_app_data_if_user_provided(
[Frozen] IAppPaths paths, [Frozen] IAppPaths paths,
SonarrCommand cmd, SonarrCommand cmd,
InitializeAppDataPath sut) InitializeAppDataPath sut)
{ {
cmd.AppDataDirectory = "path"; cmd.AppDataDirectory = "path";
sut.Initialize(cmd); sut.Initialize(cmd);
paths.Received().SetAppDataPath("path"); paths.Received().SetAppDataPath("path");
} }
} }

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions; using System.IO.Abstractions;
using TrashLib; using TrashLib;
@ -6,6 +7,7 @@ namespace Recyclarr;
public class AppPaths : IAppPaths public class AppPaths : IAppPaths
{ {
private readonly IFileSystem _fs; private readonly IFileSystem _fs;
private string? _appDataPath;
public AppPaths(IFileSystem fs) public AppPaths(IFileSystem fs)
{ {
@ -14,12 +16,15 @@ public class AppPaths : IAppPaths
public string DefaultConfigFilename => "recyclarr.yml"; 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; } = ""; [SuppressMessage("Design", "CA1024:Use properties where appropriate")]
public string ConfigPath => _fs.Path.Combine(AppDataPath, DefaultConfigFilename); public string GetAppDataPath()
public string SettingsPath => _fs.Path.Combine(AppDataPath, "settings.yml"); => _appDataPath ?? throw new DirectoryNotFoundException("Application data directory not set!");
public string LogDirectory => _fs.Path.Combine(AppDataPath, "logs");
public string RepoDirectory => _fs.Path.Combine(AppDataPath, "repo"); public string ConfigPath => _fs.Path.Combine(GetAppDataPath(), DefaultConfigFilename);
public string CacheDirectory => _fs.Path.Combine(AppDataPath, "cache"); 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");
} }

@ -1,4 +1,6 @@
using System.IO.Abstractions; using System.IO.Abstractions;
using CliFx.Exceptions;
using Common;
using TrashLib; using TrashLib;
namespace Recyclarr.Command.Initialization.Init; namespace Recyclarr.Command.Initialization.Init;
@ -7,20 +9,40 @@ public class InitializeAppDataPath : IServiceInitializer
{ {
private readonly IFileSystem _fs; private readonly IFileSystem _fs;
private readonly IAppPaths _paths; private readonly IAppPaths _paths;
private readonly IEnvironment _env;
public InitializeAppDataPath(IFileSystem fs, IAppPaths paths) public InitializeAppDataPath(IFileSystem fs, IAppPaths paths, IEnvironment env)
{ {
_fs = fs; _fs = fs;
_paths = paths; _paths = paths;
_env = env;
} }
public void Initialize(ServiceCommand cmd) 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 (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; return;
} }
// Ensure user-specified app data directory is created and use it.
_fs.Directory.CreateDirectory(cmd.AppDataDirectory); _fs.Directory.CreateDirectory(cmd.AppDataDirectory);
_paths.SetAppDataPath(cmd.AppDataDirectory); _paths.SetAppDataPath(cmd.AppDataDirectory);
} }

@ -3,7 +3,7 @@ namespace TrashLib;
public interface IAppPaths public interface IAppPaths
{ {
void SetAppDataPath(string path); void SetAppDataPath(string path);
string AppDataPath { get; } string GetAppDataPath();
string ConfigPath { get; } string ConfigPath { get; }
string SettingsPath { get; } string SettingsPath { get; }
string LogDirectory { get; } string LogDirectory { get; }

Loading…
Cancel
Save