feat: Support `*.yaml` extension

pull/201/head
Robert Dailey 12 months ago
parent 4f0e365dd5
commit a8aaca42cc

@ -13,6 +13,11 @@ changes you may need to make.
[breaking5]: https://recyclarr.dev/wiki/upgrade-guide/v5.0
### Added
- The `*.yaml` extension is now accepted for all YAML files (e.g. `settings.yaml`, `recyclarr.yaml`)
in addition to `*.yml` (which was already supported).
### Changed
- API Key is now sent via the `X-Api-Key` header instead of the `apikey` query parameter. This

@ -45,6 +45,7 @@
<PackageVersion Include="AutoFixture.NUnit3" Version="4.18.0" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
<PackageVersion Include="FluentAssertions" Version="6.11.0" />
<PackageVersion Include="FluentAssertions.Analyzers" Version="0.17.2" />
<PackageVersion Include="FluentAssertions.Json" Version="6.1.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.2.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
@ -63,4 +64,4 @@
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>
</Project>

@ -26,7 +26,10 @@ public class ConfigCreationProcessor : IConfigCreationProcessor
public async Task Process(string? configFilePath)
{
var configFile = configFilePath is null ? _paths.ConfigPath : _fs.FileInfo.New(configFilePath);
var configFile = configFilePath is null
? _paths.AppDataDirectory.File("recyclarr.yml")
: _fs.FileInfo.New(configFilePath);
if (configFile.Exists)
{
throw new FileExistsException(configFile.FullName);

@ -0,0 +1,16 @@
namespace Recyclarr.Common;
public class ConflictingYamlFilesException : Exception
{
public ConflictingYamlFilesException(IEnumerable<string> supportedFiles)
: base(BuildMessage(supportedFiles))
{
}
private static string BuildMessage(IEnumerable<string> supportedFiles)
{
return
"Expected only 1 of the following files to exist, but found more than one: " +
string.Join(", ", supportedFiles);
}
}

@ -73,4 +73,20 @@ public static class FileSystemExtensions
return subdirectories.Aggregate(dir,
(d, s) => d.FileSystem.DirectoryInfo.New(d.FileSystem.Path.Combine(d.FullName, s)));
}
public static IFileInfo? YamlFile(this IDirectoryInfo dir, string yamlFilenameNoExtension)
{
var supportedFiles = new[] {$"{yamlFilenameNoExtension}.yml", $"{yamlFilenameNoExtension}.yaml"};
var configs = supportedFiles
.Select(dir.File)
.Where(x => x.Exists)
.ToList();
if (configs.Count > 1)
{
throw new ConflictingYamlFilesException(supportedFiles);
}
return configs.FirstOrDefault();
}
}

@ -8,6 +8,7 @@
<PackageReference Include="Serilog" />
<PackageReference Include="Spectre.Console" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Extensions" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" />
<PackageReference Include="TestableIO.System.IO.Abstractions" />
<PackageReference Include="YamlDotNet" />

@ -11,14 +11,9 @@ public class AppPaths : IAppPaths
AppDataDirectory = appDataPath;
}
public static string DefaultConfigFilename => "recyclarr.yml";
public static string DefaultAppDataDirectoryName => "recyclarr";
public IDirectoryInfo AppDataDirectory { get; }
public IFileInfo ConfigPath => AppDataDirectory.File(DefaultConfigFilename);
public IFileInfo SettingsPath => AppDataDirectory.File("settings.yml");
public IFileInfo SecretsPath => AppDataDirectory.File("secrets.yml");
public IDirectoryInfo LogDirectory => AppDataDirectory.SubDir("logs", "cli");
public IDirectoryInfo ReposDirectory => AppDataDirectory.SubDir("repositories");
public IDirectoryInfo CacheDirectory => AppDataDirectory.SubDir("cache");

@ -1,4 +1,5 @@
using System.IO.Abstractions;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Parsing.ErrorHandling;
using Recyclarr.TrashLib.Startup;
@ -19,12 +20,15 @@ public class ConfigurationFinder : IConfigurationFinder
if (_paths.ConfigsDirectory.Exists)
{
configs.AddRange(_paths.ConfigsDirectory.EnumerateFiles("*.yml"));
var extensions = new[] {"*.yml", "*.yaml"};
var files = extensions.SelectMany(x => _paths.ConfigsDirectory.EnumerateFiles(x));
configs.AddRange(files);
}
if (_paths.ConfigPath.Exists)
var configPath = _paths.AppDataDirectory.YamlFile("recyclarr");
if (configPath is not null)
{
configs.Add(_paths.ConfigPath);
configs.Add(configPath);
}
return configs;

@ -1,4 +1,5 @@
using System.Collections.Immutable;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Startup;
using YamlDotNet.Serialization;
@ -21,9 +22,10 @@ public class SecretsProvider : ISecretsProvider
{
var result = new Dictionary<string, string>();
if (_paths.SecretsPath.Exists)
var yamlPath = _paths.AppDataDirectory.YamlFile("secrets");
if (yamlPath is not null)
{
using var stream = _paths.SecretsPath.OpenText();
using var stream = yamlPath.OpenText();
var deserializer = new DeserializerBuilder().Build();
var dict = deserializer.Deserialize<Dictionary<string, string>?>(stream);
if (dict is not null)

@ -1,3 +1,4 @@
using System.IO.Abstractions;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Parsing.ErrorHandling;
using Recyclarr.TrashLib.Config.Yaml;
@ -23,14 +24,15 @@ public class SettingsProvider : ISettingsProvider
private SettingsValues LoadOrCreateSettingsFile(IYamlSerializerFactory serializerFactory)
{
if (!_paths.SettingsPath.Exists)
var yamlPath = _paths.AppDataDirectory.YamlFile("settings");
if (yamlPath is null)
{
CreateDefaultSettingsFile();
yamlPath = CreateDefaultSettingsFile();
}
try
{
using var stream = _paths.SettingsPath.OpenText();
using var stream = yamlPath.OpenText();
var deserializer = serializerFactory.CreateDeserializer();
return deserializer.Deserialize<SettingsValues?>(stream.ReadToEnd()) ?? new SettingsValues();
}
@ -46,7 +48,7 @@ public class SettingsProvider : ISettingsProvider
}
}
private void CreateDefaultSettingsFile()
private IFileInfo CreateDefaultSettingsFile()
{
const string fileData =
"# yaml-language-server: $schema=https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/settings-schema.json\n" +
@ -55,8 +57,10 @@ public class SettingsProvider : ISettingsProvider
"# For the settings file reference guide, visit the link to the wiki below:\n" +
"# https://recyclarr.dev/wiki/yaml/settings-reference/\n";
_paths.SettingsPath.CreateParentDirectory();
using var stream = _paths.SettingsPath.CreateText();
var settingsFile = _paths.AppDataDirectory.File("settings.yml");
settingsFile.CreateParentDirectory();
using var stream = settingsFile.CreateText();
stream.Write(fileData);
return settingsFile;
}
}

@ -5,9 +5,6 @@ namespace Recyclarr.TrashLib.Startup;
public interface IAppPaths
{
IDirectoryInfo AppDataDirectory { get; }
IFileInfo ConfigPath { get; }
IFileInfo SettingsPath { get; }
IFileInfo SecretsPath { get; }
IDirectoryInfo LogDirectory { get; }
IDirectoryInfo ReposDirectory { get; }
IDirectoryInfo CacheDirectory { get; }

@ -13,6 +13,7 @@
<PackageReference Include="AutoFixture.NUnit3" PrivateAssets="All" />
<PackageReference Include="coverlet.collector" PrivateAssets="All" />
<PackageReference Include="FluentAssertions" PrivateAssets="All" />
<PackageReference Include="FluentAssertions.Analyzers" PrivateAssets="All" />
<PackageReference Include="FluentAssertions.Json" PrivateAssets="All" />
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="All" />
<PackageReference Include="Microsoft.NET.Test.Sdk" PrivateAssets="All" />

@ -26,7 +26,7 @@ public class BaseCommandSetupIntegrationTest : CliIntegrationFixture
{
const int maxFiles = 25;
Fs.AddFile(Paths.SettingsPath.FullName, new MockFileData($@"
Fs.AddFile(Paths.AppDataDirectory.File("settings.yml").FullName, new MockFileData($@"
log_janitor:
max_files: {maxFiles}
"));

@ -63,7 +63,8 @@ public class QualitySizeGuidePhaseTest
_ = sut.Execute(config);
config.QualityDefinition?.PreferredRatio.Should().Be(decimal.Parse(expectedPreferred));
config.QualityDefinition.Should().NotBeNull();
config.QualityDefinition!.PreferredRatio.Should().Be(decimal.Parse(expectedPreferred));
}
[Test, AutoMockData]
@ -91,8 +92,8 @@ public class QualitySizeGuidePhaseTest
});
var result = sut.Execute(config);
result?.Qualities.Should().BeEquivalentTo(new[]
result.Should().NotBeNull();
result!.Qualities.Should().BeEquivalentTo(new[]
{
new QualitySizeItem("quality1", 0, 100, 50)
},
@ -127,8 +128,8 @@ public class QualitySizeGuidePhaseTest
});
var result = sut.Execute(config);
result?.Qualities.Should().BeEquivalentTo(new[]
result.Should().NotBeNull();
result!.Qualities.Should().BeEquivalentTo(new[]
{
new QualitySizeItem("quality1", 0, 100, 90)
},

@ -17,7 +17,7 @@ public class ConfigCreationProcessorTest : CliIntegrationFixture
await sut.Process(null);
var file = Fs.GetFile(Paths.ConfigPath);
var file = Fs.GetFile(Paths.AppDataDirectory.File("recyclarr.yml"));
file.Should().NotBeNull();
file.Contents.Should().NotBeEmpty();
}

@ -1,3 +1,4 @@
using System.IO.Abstractions;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Settings;
@ -19,7 +20,7 @@ repositories:
clone_url: http://the_url.com
";
Fs.AddFile(Paths.SettingsPath, new MockFileData(yamlData));
Fs.AddFile(Paths.AppDataDirectory.File("settings.yml"), new MockFileData(yamlData));
var settings = sut.Settings;

@ -1,4 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.Text.RegularExpressions;
using Recyclarr.Common.Extensions;
using Recyclarr.TestLibrary;
@ -107,4 +108,35 @@ public class FileSystemExtensionsTest
act.Should().Throw<IOException>();
}
[Test]
public void Return_null_when_no_yaml_files_exist()
{
var fs = new MockFileSystem();
var result = fs.CurrentDirectory().YamlFile("test");
result.Should().BeNull();
}
[TestCase("test.yml")]
[TestCase("test.yaml")]
public void Return_non_null_when_single_yaml_file_exists(string yamlFilename)
{
var fs = new MockFileSystem();
fs.AddEmptyFile(yamlFilename);
var result = fs.CurrentDirectory().YamlFile("test");
result.Should().NotBeNull();
result!.Name.Should().Be(yamlFilename);
}
[Test]
public void Throw_when_both_files_exist()
{
var fs = new MockFileSystem();
fs.AddEmptyFile("test.yml");
fs.AddEmptyFile("test.yaml");
var act = () => fs.CurrentDirectory().YamlFile("test");
act.Should().Throw<ConflictingYamlFilesException>();
}
}

@ -14,9 +14,9 @@ public class ConfigurationFinderTest
{
return new[]
{
paths.ConfigPath,
paths.AppDataDirectory.File("recyclarr.yml"),
paths.ConfigsDirectory.File("b.yml"),
paths.ConfigsDirectory.File("c.yml")
paths.ConfigsDirectory.File("c.yaml")
};
}
@ -97,11 +97,12 @@ public class ConfigurationFinderTest
[Frozen(Matching.ImplementedInterfaces)] AppPaths paths,
ConfigurationFinder sut)
{
fs.AddEmptyFile(paths.ConfigPath);
var configFile = paths.AppDataDirectory.File("recyclarr.yml");
fs.AddEmptyFile(configFile);
var result = sut.GetConfigFiles(Array.Empty<IFileInfo>());
result.Should().ContainSingle(x => x.FullName == paths.ConfigPath.FullName);
result.Should().ContainSingle(x => x.FullName == configFile.FullName);
}
[Test, AutoMockData]

@ -1,3 +1,4 @@
using System.IO.Abstractions;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.TestLibrary;
@ -30,7 +31,7 @@ api_key: 95283e6b156c42f3af8a9b16173f876b
secret_rp: 1234567
";
Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml));
Fs.AddFile(Paths.AppDataDirectory.File("secrets.yml").FullName, new MockFileData(secretsYml));
var expected = new[]
{
new
@ -67,7 +68,7 @@ sonarr:
const string secretsYml = "no_api_key: 95283e6b156c42f3af8a9b16173f876b";
Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml));
Fs.AddFile(Paths.AppDataDirectory.File("recyclarr.yml").FullName, new MockFileData(secretsYml));
var result = configLoader.Load(() => new StringReader(testYml), SupportedServices.Sonarr);
result.Should().BeEmpty();
@ -120,7 +121,7 @@ sonarr:
const string secretsYml = @"bogus_profile: 95283e6b156c42f3af8a9b16173f876b";
Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml));
Fs.AddFile(Paths.AppDataDirectory.File("recyclarr.yml").FullName, new MockFileData(secretsYml));
var result = configLoader.Load(() => new StringReader(testYml), SupportedServices.Sonarr);
result.Should().BeEmpty();
}

@ -1,3 +1,4 @@
using System.IO.Abstractions;
using Recyclarr.TrashLib.Config.Yaml;
using Recyclarr.TrashLib.Settings;
using Recyclarr.TrashLib.Startup;
@ -16,7 +17,7 @@ public class SettingsPersisterTest
{
_ = sut.Settings;
fileSystem.AllFiles.Should().ContainSingle(paths.SettingsPath.FullName);
fileSystem.AllFiles.Should().ContainSingle(paths.AppDataDirectory.File("settings.yml").FullName);
}
[Test, AutoMockData]

Loading…
Cancel
Save