From 1e4138851d038435b947ec5a6f9ad42a0e70c367 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Mon, 23 Sep 2024 19:22:16 +0200 Subject: [PATCH] Add support for defining settings folder --- README.source.md | 15 ++++++ src/RepoM.Api/Bootstrapper.cs | 3 +- src/RepoM.Api/IO/AppDataPathConfig.cs | 6 +++ src/RepoM.Api/IO/AppDataPathProvider.cs | 22 ++++++++ .../IO/DefaultAppDataPathProvider.cs | 18 ------- src/RepoM.App/App.xaml.cs | 50 ++++++++++++++----- src/RepoM.App/Bootstrapper.cs | 13 +++-- src/RepoM.App/Configuration/Config.cs | 9 ++++ src/RepoM.App/Properties/launchSettings.json | 11 ++++ src/RepoM.App/RepoM.App.csproj | 8 +++ src/RepoM.App/appsettings.Development.json | 9 ++++ src/RepoM.App/appsettings.json | 5 ++ tests/RepoM.App.Tests/BootstrapperTests.cs | 9 ++-- 13 files changed, 135 insertions(+), 43 deletions(-) create mode 100644 src/RepoM.Api/IO/AppDataPathConfig.cs create mode 100644 src/RepoM.Api/IO/AppDataPathProvider.cs delete mode 100644 src/RepoM.Api/IO/DefaultAppDataPathProvider.cs create mode 100644 src/RepoM.App/Configuration/Config.cs create mode 100644 src/RepoM.App/Properties/launchSettings.json create mode 100644 src/RepoM.App/appsettings.Development.json create mode 100644 src/RepoM.App/appsettings.json diff --git a/README.source.md b/README.source.md index edcbf51d..f60d12e2 100644 --- a/README.source.md +++ b/README.source.md @@ -64,6 +64,21 @@ The repositories shown in RepoM are filtered using the search box in RepoM. But When RepoM starts for the first time, a configuration file wil be created. Most of the properties can be adjusted using the UI but, at this moment, one property must be altered manually. Read more over [here](docs/_old/Settings.md). +## Multi configuration + +By default, RepoM stores all configuration files in `%ADPPDATA%/RepoM`. As a user you can alter this location to support multi configurations which might be useful for different working environments. Also, for development or debug purposes, this might be very useful. + +To change the app settings location, you can + +- alter the `appsettings.json` file located in the same directory where the RepoM executable lives + +snippet: appsettings-appsettingspath + +- start RepoM using the commandline argument `--App:AppSettingsPath `. +- use environment variable `REPOM__APP_APPSETTINGSPATH`. + +If none is set, the default will be used. + ## Plugins RepoM uses plugins to extend functionality. At this moment, when a plugin is available in the installed directory, it will be found and can be enabled or disabled. This is done in the hamburger menu of RepoM. Enabling or disabling requires a restart of RepoM. diff --git a/src/RepoM.Api/Bootstrapper.cs b/src/RepoM.Api/Bootstrapper.cs index 237588eb..62450cc1 100644 --- a/src/RepoM.Api/Bootstrapper.cs +++ b/src/RepoM.Api/Bootstrapper.cs @@ -2,7 +2,6 @@ namespace RepoM.Api; using Microsoft.Extensions.Logging; using RepoM.Api.Common; -using RepoM.Api.IO; using RepoM.Api.Plugins; using SimpleInjector; using System.Collections.Generic; @@ -87,7 +86,7 @@ await container .RegisterPackagesAsync( assemblies, filename => new FileBasedPackageConfiguration( - DefaultAppDataPathProvider.Instance, + _appDataProvider, _fileSystem, _loggerFactory.CreateLogger(), filename)) diff --git a/src/RepoM.Api/IO/AppDataPathConfig.cs b/src/RepoM.Api/IO/AppDataPathConfig.cs new file mode 100644 index 00000000..cc7db179 --- /dev/null +++ b/src/RepoM.Api/IO/AppDataPathConfig.cs @@ -0,0 +1,6 @@ +namespace RepoM.Api.IO; + +public record struct AppDataPathConfig +{ + public string? AppSettingsPath { get; init; } +} diff --git a/src/RepoM.Api/IO/AppDataPathProvider.cs b/src/RepoM.Api/IO/AppDataPathProvider.cs new file mode 100644 index 00000000..2b5d6f4c --- /dev/null +++ b/src/RepoM.Api/IO/AppDataPathProvider.cs @@ -0,0 +1,22 @@ +namespace RepoM.Api.IO; + +using System; +using System.IO; +using RepoM.Core.Plugin.Common; + +public sealed class AppDataPathProvider : IAppDataPathProvider +{ + public AppDataPathProvider(AppDataPathConfig config) + { + ArgumentNullException.ThrowIfNull(config); + if (!string.IsNullOrWhiteSpace(config.AppSettingsPath)) + { + AppDataPath = Path.GetFullPath(config.AppSettingsPath); + return; + } + + AppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RepoM"); + } + + public string AppDataPath { get; } +} \ No newline at end of file diff --git a/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs b/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs deleted file mode 100644 index ca9795b4..00000000 --- a/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace RepoM.Api.IO; - -using System; -using System.IO; -using RepoM.Core.Plugin.Common; - -public class DefaultAppDataPathProvider : IAppDataPathProvider -{ - private static readonly string _applicationDataRepoM = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RepoM"); - - private DefaultAppDataPathProvider() - { - } - - public static DefaultAppDataPathProvider Instance { get; } = new(); - - public string AppDataPath => _applicationDataRepoM; -} \ No newline at end of file diff --git a/src/RepoM.App/App.xaml.cs b/src/RepoM.App/App.xaml.cs index c8770aaf..03810732 100644 --- a/src/RepoM.App/App.xaml.cs +++ b/src/RepoM.App/App.xaml.cs @@ -22,6 +22,7 @@ namespace RepoM.App; using Container = SimpleInjector.Container; using RepoM.App.Services.HotKey; using RepoM.Api; +using RepoM.App.Configuration; /// /// Interaction logic for App.xaml @@ -60,7 +61,7 @@ protected override async void OnStartup(StartupEventArgs e) typeof(FrameworkElement), new FrameworkPropertyMetadata(System.Windows.Markup.XmlLanguage.GetLanguage(System.Globalization.CultureInfo.CurrentCulture.IetfLanguageTag))); - Application.Current.Resources.MergedDictionaries[0] = ResourceDictionaryTranslationService.ResourceDictionary; + Current.Resources.MergedDictionaries[0] = ResourceDictionaryTranslationService.ResourceDictionary; _notifyIcon = FindResource("NotifyIcon") as TaskbarIcon; var fileSystem = new FileSystem(); @@ -69,22 +70,32 @@ protected override async void OnStartup(StartupEventArgs e) IHmacService hmacService = new HmacSha256Service(); IPluginFinder pluginFinder = new PluginFinder(fileSystem, hmacService); - IConfiguration config = SetupConfiguration(); + IConfiguration appDataPathConfiguration = SetupConfigurationX(e.Args); + + var appConfig = new Config(); + appDataPathConfiguration.Bind("App", appConfig); + var appDataPathConfig = new AppDataPathConfig + { + AppSettingsPath = appConfig.AppSettingsPath, + }; + var appDataProvider = new AppDataPathProvider(appDataPathConfig); + + IConfiguration config = SetupConfiguration(appDataProvider); ILoggerFactory loggerFactory = CreateLoggerFactory(config); ILogger logger = loggerFactory.CreateLogger(nameof(App)); logger.LogInformation("Started"); Bootstrapper.RegisterLogging(loggerFactory); - Bootstrapper.RegisterServices(fileSystem); - await Bootstrapper.RegisterPlugins(pluginFinder, fileSystem, loggerFactory).ConfigureAwait(true); + Bootstrapper.RegisterServices(fileSystem, appDataProvider); + await Bootstrapper.RegisterPlugins(pluginFinder, fileSystem, loggerFactory, appDataProvider).ConfigureAwait(true); + + var ensureStartup = new EnsureStartup(fileSystem, appDataProvider); + await ensureStartup.EnsureFilesAsync().ConfigureAwait(true); #if DEBUG Bootstrapper.Container.Verify(SimpleInjector.VerificationOption.VerifyAndDiagnose); #else Bootstrapper.Container.Options.EnableAutoVerification = false; #endif - - EnsureStartup ensureStartup = Bootstrapper.Container.GetInstance(); - await ensureStartup.EnsureFilesAsync().ConfigureAwait(true); UseRepositoryMonitor(Bootstrapper.Container); @@ -104,7 +115,24 @@ protected override async void OnStartup(StartupEventArgs e) logger.LogError(exception, "Could not start all modules."); } } - + + private static IConfiguration SetupConfigurationX(string[] args) + { + IConfigurationBuilder builder = new ConfigurationBuilder() + //.SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false); + +#if DEBUG + builder = builder.AddJsonFile("appsettings.Debug.json", optional: true, reloadOnChange: false); +#endif + + builder = builder + .AddEnvironmentVariables("REPOM_APP_") + .AddCommandLine(args); + + return builder.Build(); + } + protected override void OnExit(ExitEventArgs e) { _windowSizeService?.Unregister(); @@ -113,19 +141,17 @@ protected override void OnExit(ExitEventArgs e) _hotKeyService?.Unregister(); -// #pragma warning disable CA1416 // Validate platform compatibility _notifyIcon?.Dispose(); -// #pragma warning restore CA1416 // Validate platform compatibility ReleaseAndDisposeMutex(); base.OnExit(e); } - private static IConfiguration SetupConfiguration() + private static IConfiguration SetupConfiguration(AppDataPathProvider appDataProvider) { const string FILENAME = "appsettings.serilog.json"; - var fullFilename = Path.Combine(DefaultAppDataPathProvider.Instance.AppDataPath, FILENAME); + var fullFilename = Path.Combine(appDataProvider.AppDataPath, FILENAME); IConfigurationBuilder builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) diff --git a/src/RepoM.App/Bootstrapper.cs b/src/RepoM.App/Bootstrapper.cs index 5eb543ce..ba45ed71 100644 --- a/src/RepoM.App/Bootstrapper.cs +++ b/src/RepoM.App/Bootstrapper.cs @@ -41,7 +41,7 @@ internal static class Bootstrapper { public static readonly Container Container = new(); - public static void RegisterServices(IFileSystem fileSystem) + public static void RegisterServices(IFileSystem fileSystem, IAppDataPathProvider appDataProvider) { Container.RegisterInstance(MemoryCache.Default); Container.RegisterSingleton(() => Container.GetInstance()); @@ -51,7 +51,7 @@ public static void RegisterServices(IFileSystem fileSystem) Container.Register(Lifestyle.Singleton); Container.Register(Lifestyle.Singleton); Container.Register(Lifestyle.Singleton); - Container.RegisterInstance(DefaultAppDataPathProvider.Instance); + Container.RegisterInstance(appDataProvider); Container.Register(Lifestyle.Singleton); Container.Register(Lifestyle.Singleton); Container.Register(Lifestyle.Singleton); @@ -106,7 +106,7 @@ public static void RegisterServices(IFileSystem fileSystem) Container.Register, SumRepositoryComparerFactory>(Lifestyle.Singleton); Container.RegisterSingleton(); - Container.Register(typeof(ICommandExecutor<>), new[] { typeof(CoreBootstrapper).Assembly, }, Lifestyle.Singleton); + Container.Register(typeof(ICommandExecutor<>), [typeof(CoreBootstrapper).Assembly,], Lifestyle.Singleton); Container.RegisterDecorator( typeof(ICommandExecutor<>), typeof(LoggerCommandExecutorDecorator<>), @@ -114,19 +114,18 @@ public static void RegisterServices(IFileSystem fileSystem) Container.RegisterSingleton(); Container.RegisterSingleton(); - - Container.RegisterSingleton(); } public static async Task RegisterPlugins( IPluginFinder pluginFinder, IFileSystem fileSystem, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + IAppDataPathProvider appDataPathProvider) { Container.Register(Lifestyle.Singleton); Container.RegisterInstance(pluginFinder); - var coreBootstrapper = new CoreBootstrapper(pluginFinder, fileSystem, DefaultAppDataPathProvider.Instance, loggerFactory); + var coreBootstrapper = new CoreBootstrapper(pluginFinder, fileSystem, appDataPathProvider, loggerFactory); var baseDirectory = fileSystem.Path.Combine(AppDomain.CurrentDomain.BaseDirectory); await coreBootstrapper.LoadAndRegisterPluginsAsync(Container, baseDirectory).ConfigureAwait(false); } diff --git a/src/RepoM.App/Configuration/Config.cs b/src/RepoM.App/Configuration/Config.cs new file mode 100644 index 00000000..2278c0f9 --- /dev/null +++ b/src/RepoM.App/Configuration/Config.cs @@ -0,0 +1,9 @@ +namespace RepoM.App.Configuration; + +public class Config +{ + /// + /// Absolute or relative path to the app settings folder. + /// + public string? AppSettingsPath { get; init; } +} \ No newline at end of file diff --git a/src/RepoM.App/Properties/launchSettings.json b/src/RepoM.App/Properties/launchSettings.json new file mode 100644 index 00000000..e2b24205 --- /dev/null +++ b/src/RepoM.App/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "RepoM.App bin config": { + "commandName": "Project", + "commandLineArgs": "--App:AppSettingsPath RepoMConfig" + }, + "RepoM.App": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/src/RepoM.App/RepoM.App.csproj b/src/RepoM.App/RepoM.App.csproj index 524fa33b..3a2bd9e7 100644 --- a/src/RepoM.App/RepoM.App.csproj +++ b/src/RepoM.App/RepoM.App.csproj @@ -37,6 +37,7 @@ + @@ -64,6 +65,13 @@ + + Never + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/src/RepoM.App/appsettings.Development.json b/src/RepoM.App/appsettings.Development.json new file mode 100644 index 00000000..653aefd4 --- /dev/null +++ b/src/RepoM.App/appsettings.Development.json @@ -0,0 +1,9 @@ +// begin-snippet: appsettings-appsettingspath +{ + "App": { + // Absolute or relative path to the configuration directory. + // like: "AppSettingsPath": "C:/my-config/", + "AppSettingsPath": "MyConfig" + } +} +// end-snippet \ No newline at end of file diff --git a/src/RepoM.App/appsettings.json b/src/RepoM.App/appsettings.json new file mode 100644 index 00000000..fb9c2bfb --- /dev/null +++ b/src/RepoM.App/appsettings.json @@ -0,0 +1,5 @@ +{ + "App": { + "AppSettingsPath": null + } +} diff --git a/tests/RepoM.App.Tests/BootstrapperTests.cs b/tests/RepoM.App.Tests/BootstrapperTests.cs index 948b608f..160a363b 100644 --- a/tests/RepoM.App.Tests/BootstrapperTests.cs +++ b/tests/RepoM.App.Tests/BootstrapperTests.cs @@ -5,6 +5,7 @@ namespace RepoM.App.Tests; using FakeItEasy; using FluentAssertions; using Microsoft.Extensions.Logging; +using RepoM.Core.Plugin.Common; using SimpleInjector; using Xunit; using Sut = RepoM.App.Bootstrapper; @@ -17,8 +18,8 @@ public void Container_ShouldAlwaysBeSameInstance() // arrange // act - var result1 = Sut.Container; - var result2 = Sut.Container; + Container result1 = Sut.Container; + Container result2 = Sut.Container; // assert result1.Should().NotBeNull().And.Subject.Should().BeSameAs(result2); @@ -30,7 +31,7 @@ public void RegisterServices_ShouldNotThrow() // arrange // act - Action act = () => Sut.RegisterServices(A.Dummy()); + Action act = () => Sut.RegisterServices(A.Dummy(), A.Dummy()); // assert act.Should().NotThrow(); @@ -55,7 +56,7 @@ public void RegisterServices_ShouldResultInValidContainer() // arrange // act - Sut.RegisterServices(A.Dummy()); + Sut.RegisterServices(A.Dummy(), A.Dummy()); Sut.RegisterLogging(A.Fake()); // assert