From 0c49071a3be1b0108e750781c350a4cb214605c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Wed, 26 Jul 2023 10:24:22 +0200 Subject: [PATCH] Admin mode WIP --- .gitignore | 4 +- .../ViewModels/TabViewModel.cs | 13 +- .../DependencyInjection.cs | 15 +- .../FileTime.App.DependencyInjection.csproj | 1 + src/Core/FileTime.Core.Services/Tab.cs | 11 +- .../FileTime.Core.Timeline.csproj | 4 + .../LocalCommandExecutor.cs | 10 +- src/FileTime.sln | 72 +++++++ .../Configuration/MainConfiguration.cs | 13 +- .../FileTime.GuiApp.Abstractions.csproj | 1 + .../Avalonia/FileTime.GuiApp.App/App.axaml.cs | 4 +- .../FileTime.GuiApp.App.csproj | 3 + .../Avalonia/FileTime.GuiApp.App/Startup.cs | 5 +- .../Assets/material/security.svg | 1 + .../FileTime.GuiApp/FileTime.GuiApp.csproj | 1 + .../ViewModels/IMainWindowViewModel.cs | 4 +- .../ViewModels/MainWindowViewModel.cs | 2 + .../FileTime.GuiApp/Views/MainWindow.axaml | 22 +- .../DeclarativeProperty/DebounceProperty.cs | 4 +- .../DeclarativePropertyExtensions.cs | 5 +- .../DeclarativeProperty/ThrottleProperty.cs | 7 +- .../DeclarativeProperty/TimingPropertyBase.cs | 6 +- .../LocalContentProviderConstants.cs | 6 + .../FileTime.Providers.Local.csproj | 1 + .../LocalContentProvider.cs | 2 +- .../LocalContentWriter.cs | 6 +- .../LocalContentWriterFactory.cs | 4 +- .../LocalItemCreator.cs | 42 +++- .../FileTime.Providers.Local/Startup.cs | 2 +- .../AdminElevationConfiguration.cs | 9 + ...e.Providers.LocalAdmin.Abstractions.csproj | 15 ++ .../IAdminContentAccessorFactory.cs | 9 + .../IAdminContentProvider.cs | 8 + .../IAdminElevationManager.cs | 12 ++ .../AdminContentAccessorFactory.cs | 36 ++++ .../AdminContentProvider.cs | 30 +++ .../AdminElevationManager.cs | 197 ++++++++++++++++++ .../FileTime.Providers.LocalAdmin.csproj | 29 +++ .../FileTime.Providers.LocalAdmin/Startup.cs | 20 ++ ...eTime.Providers.Remote.Abstractions.csproj | 15 ++ .../IRemoteContentProvider.cs | 8 + .../IRemoteItemCreator.cs | 12 ++ .../FileTime.Providers.Remote.csproj | 15 ++ .../RemoteContentProvider.cs | 24 +++ .../RemoteItemCreator.cs | 24 +++ .../FileTime.Providers.Remote/Startup.cs | 16 ++ 46 files changed, 695 insertions(+), 55 deletions(-) create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp/Assets/material/security.svg create mode 100644 src/Providers/FileTime.Providers.Local.Abstractions/LocalContentProviderConstants.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin.Abstractions/AdminElevationConfiguration.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin.Abstractions/FileTime.Providers.LocalAdmin.Abstractions.csproj create mode 100644 src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentProvider.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminElevationManager.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs create mode 100644 src/Providers/FileTime.Providers.LocalAdmin/FileTime.Providers.LocalAdmin.csproj create mode 100644 src/Providers/FileTime.Providers.LocalAdmin/Startup.cs create mode 100644 src/Providers/FileTime.Providers.Remote.Abstractions/FileTime.Providers.Remote.Abstractions.csproj create mode 100644 src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteContentProvider.cs create mode 100644 src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemCreator.cs create mode 100644 src/Providers/FileTime.Providers.Remote/FileTime.Providers.Remote.csproj create mode 100644 src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs create mode 100644 src/Providers/FileTime.Providers.Remote/RemoteItemCreator.cs create mode 100644 src/Providers/FileTime.Providers.Remote/Startup.cs diff --git a/.gitignore b/.gitignore index 422421c..4d7f85e 100644 --- a/.gitignore +++ b/.gitignore @@ -412,4 +412,6 @@ FodyWeavers.xsd appdata -**/.idea \ No newline at end of file +**/.idea + +appsettings.Local.json \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs index 56146ad..0f5c0f7 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -23,10 +23,9 @@ namespace FileTime.App.Core.ViewModels; public partial class TabViewModel : ITabViewModel { private readonly IServiceProvider _serviceProvider; - private readonly IItemNameConverterService _itemNameConverterService; private readonly IAppState _appState; - private readonly IRxSchedulerService _rxSchedulerService; private readonly ITimelessContentProvider _timelessContentProvider; + private readonly IRefreshSmoothnessCalculator _refreshSmoothnessCalculator; private readonly SourceList _markedItems = new(); private readonly List _disposables = new(); private bool _disposed; @@ -50,19 +49,17 @@ public partial class TabViewModel : ITabViewModel public TabViewModel( IServiceProvider serviceProvider, - IItemNameConverterService itemNameConverterService, IAppState appState, - IRxSchedulerService rxSchedulerService, - ITimelessContentProvider timelessContentProvider) + ITimelessContentProvider timelessContentProvider, + IRefreshSmoothnessCalculator refreshSmoothnessCalculator) { _serviceProvider = serviceProvider; - _itemNameConverterService = itemNameConverterService; _appState = appState; MarkedItems = _markedItems.Connect().StartWithEmpty(); IsSelected = _appState.SelectedTab.Select(s => s == this); - _rxSchedulerService = rxSchedulerService; _timelessContentProvider = timelessContentProvider; + _refreshSmoothnessCalculator = refreshSmoothnessCalculator; } public void Init(ITab tab, int tabNumber) @@ -103,7 +100,7 @@ public partial class TabViewModel : ITabViewModel CurrentSelectedItemAsContainer = CurrentSelectedItem.Map(i => i as IContainerViewModel); SelectedsChildren = CurrentSelectedItem - .Debounce(TimeSpan.FromMilliseconds(200), resetTimer: true) + .Debounce(() => _refreshSmoothnessCalculator.RefreshDelay, resetTimer: true) .DistinctUntilChanged() .Map(item => { diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 893b6d5..2f411a1 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -12,6 +12,9 @@ using FileTime.Core.ContentAccess; using FileTime.Core.Services; using FileTime.Core.Timeline; using FileTime.Providers.Local; +using FileTime.Providers.LocalAdmin; +using FileTime.Providers.Remote; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -19,7 +22,7 @@ namespace FileTime.App.DependencyInjection; public static class DependencyInjection { - public static IServiceCollection RegisterDefaultServices(IServiceCollection? serviceCollection = null) + public static IServiceCollection RegisterDefaultServices(IConfigurationRoot configuration, IServiceCollection? serviceCollection = null) { serviceCollection ??= new ServiceCollection(); @@ -31,7 +34,7 @@ public static class DependencyInjection //TODO: check local/remote context serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); - + serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); serviceCollection.TryAddTransient(); @@ -41,18 +44,18 @@ public static class DependencyInjection return serviceCollection .AddCoreAppServices() - .AddLocalServices() + .AddLocalProviderServices() + .AddLocalAdminProviderServices(configuration) + .AddRemoteProviderServices() .RegisterCommands() .AddDefaultCommandHandlers(); } private static IServiceCollection RegisterCommands(this IServiceCollection serviceCollection) - { - return serviceCollection + => serviceCollection .AddCommands() .AddTransient() .AddTransient() .AddTransient() .AddTransient(); - } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj index d34c92d..1596156 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj +++ b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index 8f528eb..e158bd4 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -15,7 +15,6 @@ public class Tab : ITab { private readonly ITimelessContentProvider _timelessContentProvider; private readonly ITabEvents _tabEvents; - private readonly IRefreshSmoothnessCalculator _refreshSmoothnessCalculator; private readonly DeclarativeProperty _currentLocation = new(null); private readonly BehaviorSubject _currentLocationForced = new(null); private readonly DeclarativeProperty _currentRequestItem = new(null); @@ -38,7 +37,6 @@ public class Tab : ITab { _timelessContentProvider = timelessContentProvider; _tabEvents = tabEvents; - _refreshSmoothnessCalculator = refreshSmoothnessCalculator; _currentPointInTime = null!; _timelessContentProvider.CurrentPointInTime.Subscribe(p => _currentPointInTime = p); @@ -54,7 +52,8 @@ public class Tab : ITab return Task.CompletedTask; }); - CurrentItems = CurrentLocation.Map((container, _) => + CurrentItems = CurrentLocation + .Map((container, _) => { var items = container is null ? (ObservableCollection?) null @@ -63,7 +62,7 @@ public class Tab : ITab } ); - + CurrentSelectedItem = DeclarativePropertyHelpers.CombineLatest( CurrentItems.Watch, IItem>(), _currentRequestItem.DistinctUntilChanged(), @@ -77,8 +76,8 @@ public class Tab : ITab CurrentSelectedItem.Subscribe((v) => { - _refreshSmoothnessCalculator.RegisterChange(); - _refreshSmoothnessCalculator.RecalculateSmoothness(); + refreshSmoothnessCalculator.RegisterChange(); + refreshSmoothnessCalculator.RecalculateSmoothness(); }); CurrentSelectedItem.Subscribe(async (s, _) => diff --git a/src/Core/FileTime.Core.Timeline/FileTime.Core.Timeline.csproj b/src/Core/FileTime.Core.Timeline/FileTime.Core.Timeline.csproj index c665078..1bb7c92 100644 --- a/src/Core/FileTime.Core.Timeline/FileTime.Core.Timeline.csproj +++ b/src/Core/FileTime.Core.Timeline/FileTime.Core.Timeline.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/src/Core/FileTime.Core.Timeline/LocalCommandExecutor.cs b/src/Core/FileTime.Core.Timeline/LocalCommandExecutor.cs index ea35571..a0d1798 100644 --- a/src/Core/FileTime.Core.Timeline/LocalCommandExecutor.cs +++ b/src/Core/FileTime.Core.Timeline/LocalCommandExecutor.cs @@ -1,15 +1,18 @@ using FileTime.Core.Command; +using Microsoft.Extensions.Logging; namespace FileTime.Core.Timeline; public class LocalCommandExecutor : ILocalCommandExecutor { private readonly ICommandRunner _commandRunner; + private readonly ILogger _logger; public event EventHandler? CommandFinished; - public LocalCommandExecutor(ICommandRunner commandRunner) + public LocalCommandExecutor(ICommandRunner commandRunner, ILogger logger) { _commandRunner = commandRunner; + _logger = logger; } public void ExecuteCommand(ICommand command) @@ -27,7 +30,10 @@ public class LocalCommandExecutor : ILocalCommandExecutor { await _commandRunner.RunCommandAsync(context.Command); } - catch (Exception ex) { } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing command {Command}", context.Command.GetType().Name); + } CommandFinished?.Invoke(this, context.Command); } diff --git a/src/FileTime.sln b/src/FileTime.sln index 7cf5ff5..2e31973 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -85,6 +85,28 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeclarativeProperty", "Libr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Defer", "Library\Defer\Defer.csproj", "{609FFADA-C221-4E41-B377-C6AF56C0A900}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{778AAF38-20FF-438C-A9C3-60850C8B5A27}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server", "Server\FileTime.Server\FileTime.Server.csproj", "{190762DB-7353-4F02-AD42-E29C36DEE218}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server.App", "Server\FileTime.Server.App\FileTime.Server.App.csproj", "{2E2B6F68-D20C-4F07-A3C1-95E7D8F1DAF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server.App.Abstractions", "Server\FileTime.Server.App.Abstractions\FileTime.Server.App.Abstractions.csproj", "{85B04DA4-6B2C-4772-B915-EB4C53CA31A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Providers.LocalAdmin", "Providers\FileTime.Providers.LocalAdmin\FileTime.Providers.LocalAdmin.csproj", "{28914EA2-396A-444F-A5DF-4072497E48D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Providers.LocalAdmin.Abstractions", "Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj", "{2FB2C2EF-ADBA-48AE-AD3A-D51625C044CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Providers.Remote", "Providers\FileTime.Providers.Remote\FileTime.Providers.Remote.csproj", "{5E721AB2-9107-4F36-A602-70D5E00FAC0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Providers.Remote.Abstractions", "Providers\FileTime.Providers.Remote.Abstractions\FileTime.Providers.Remote.Abstractions.csproj", "{72072CA3-954D-4D97-BE70-928021D8F66E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server.Common", "Server\FileTime.Server.Common\FileTime.Server.Common.csproj", "{181BC62C-EDEA-4DD1-8837-A4B1CBF8BE42}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server.Common.Abstractions", "Server\FileTime.Server.Common.Abstractions\FileTime.Server.Common.Abstractions.csproj", "{C82B417F-94E6-4D4D-B261-0CAF40551D5E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server.Web", "Server\FileTime.Server.Web\FileTime.Server.Web.csproj", "{9062F7D2-34DE-44B7-A2D6-8B3AFDEBB606}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -223,6 +245,46 @@ Global {609FFADA-C221-4E41-B377-C6AF56C0A900}.Debug|Any CPU.Build.0 = Debug|Any CPU {609FFADA-C221-4E41-B377-C6AF56C0A900}.Release|Any CPU.ActiveCfg = Release|Any CPU {609FFADA-C221-4E41-B377-C6AF56C0A900}.Release|Any CPU.Build.0 = Release|Any CPU + {190762DB-7353-4F02-AD42-E29C36DEE218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {190762DB-7353-4F02-AD42-E29C36DEE218}.Debug|Any CPU.Build.0 = Debug|Any CPU + {190762DB-7353-4F02-AD42-E29C36DEE218}.Release|Any CPU.ActiveCfg = Release|Any CPU + {190762DB-7353-4F02-AD42-E29C36DEE218}.Release|Any CPU.Build.0 = Release|Any CPU + {2E2B6F68-D20C-4F07-A3C1-95E7D8F1DAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E2B6F68-D20C-4F07-A3C1-95E7D8F1DAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E2B6F68-D20C-4F07-A3C1-95E7D8F1DAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E2B6F68-D20C-4F07-A3C1-95E7D8F1DAF0}.Release|Any CPU.Build.0 = Release|Any CPU + {85B04DA4-6B2C-4772-B915-EB4C53CA31A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85B04DA4-6B2C-4772-B915-EB4C53CA31A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85B04DA4-6B2C-4772-B915-EB4C53CA31A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85B04DA4-6B2C-4772-B915-EB4C53CA31A2}.Release|Any CPU.Build.0 = Release|Any CPU + {28914EA2-396A-444F-A5DF-4072497E48D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28914EA2-396A-444F-A5DF-4072497E48D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28914EA2-396A-444F-A5DF-4072497E48D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28914EA2-396A-444F-A5DF-4072497E48D0}.Release|Any CPU.Build.0 = Release|Any CPU + {2FB2C2EF-ADBA-48AE-AD3A-D51625C044CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FB2C2EF-ADBA-48AE-AD3A-D51625C044CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FB2C2EF-ADBA-48AE-AD3A-D51625C044CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FB2C2EF-ADBA-48AE-AD3A-D51625C044CB}.Release|Any CPU.Build.0 = Release|Any CPU + {5E721AB2-9107-4F36-A602-70D5E00FAC0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E721AB2-9107-4F36-A602-70D5E00FAC0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E721AB2-9107-4F36-A602-70D5E00FAC0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E721AB2-9107-4F36-A602-70D5E00FAC0C}.Release|Any CPU.Build.0 = Release|Any CPU + {72072CA3-954D-4D97-BE70-928021D8F66E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72072CA3-954D-4D97-BE70-928021D8F66E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72072CA3-954D-4D97-BE70-928021D8F66E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72072CA3-954D-4D97-BE70-928021D8F66E}.Release|Any CPU.Build.0 = Release|Any CPU + {181BC62C-EDEA-4DD1-8837-A4B1CBF8BE42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {181BC62C-EDEA-4DD1-8837-A4B1CBF8BE42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {181BC62C-EDEA-4DD1-8837-A4B1CBF8BE42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {181BC62C-EDEA-4DD1-8837-A4B1CBF8BE42}.Release|Any CPU.Build.0 = Release|Any CPU + {C82B417F-94E6-4D4D-B261-0CAF40551D5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C82B417F-94E6-4D4D-B261-0CAF40551D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C82B417F-94E6-4D4D-B261-0CAF40551D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C82B417F-94E6-4D4D-B261-0CAF40551D5E}.Release|Any CPU.Build.0 = Release|Any CPU + {9062F7D2-34DE-44B7-A2D6-8B3AFDEBB606}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9062F7D2-34DE-44B7-A2D6-8B3AFDEBB606}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9062F7D2-34DE-44B7-A2D6-8B3AFDEBB606}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9062F7D2-34DE-44B7-A2D6-8B3AFDEBB606}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -262,6 +324,16 @@ Global {D0EC224E-F043-4657-BD6A-1ADE52DFF8B5} = {01F231DE-4A65-435F-B4BB-77EE5221890C} {D5C85B69-6345-4FAA-A00C-3A73BA677664} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {609FFADA-C221-4E41-B377-C6AF56C0A900} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {190762DB-7353-4F02-AD42-E29C36DEE218} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} + {2E2B6F68-D20C-4F07-A3C1-95E7D8F1DAF0} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} + {85B04DA4-6B2C-4772-B915-EB4C53CA31A2} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} + {28914EA2-396A-444F-A5DF-4072497E48D0} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3} + {2FB2C2EF-ADBA-48AE-AD3A-D51625C044CB} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3} + {5E721AB2-9107-4F36-A602-70D5E00FAC0C} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3} + {72072CA3-954D-4D97-BE70-928021D8F66E} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3} + {181BC62C-EDEA-4DD1-8837-A4B1CBF8BE42} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} + {C82B417F-94E6-4D4D-B261-0CAF40551D5E} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} + {9062F7D2-34DE-44B7-A2D6-8B3AFDEBB606} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs index 5e0761c..68719e0 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs @@ -1,5 +1,6 @@ using Avalonia.Input; using FileTime.App.Core.UserCommand; +using FileTime.Providers.LocalAdmin; namespace FileTime.GuiApp.Configuration; @@ -7,17 +8,21 @@ public static class MainConfiguration { private static readonly Lazy> _defaultKeybindings = new(InitDefaultKeyBindings); - public static Dictionary Configuration { get; } + public static Dictionary Configuration { get; } static MainConfiguration() { - Configuration = new(); + Configuration = new() + { + {AdminElevationConfiguration.SectionName + ":" + nameof(AdminElevationConfiguration.ServerExecutablePath), "FileTime.Server.exe"}, + }; + PopulateDefaultEditorPrograms(Configuration); PopulateDefaultKeyBindings(Configuration, _defaultKeybindings.Value, SectionNames.KeybindingSectionName + ":" + nameof(KeyBindingConfiguration.DefaultKeyBindings)); } - private static void PopulateDefaultKeyBindings(Dictionary configuration, + private static void PopulateDefaultKeyBindings(Dictionary configuration, List commandBindingConfigs, string basePath) { for (var i = 0; i < commandBindingConfigs.Count; i++) @@ -113,7 +118,7 @@ public static class MainConfiguration //new CommandBindingConfiguration(ConfigCommand.ToggleAdvancedIcons, new[] { Key.Z, Key.I }), }; - private static void PopulateDefaultEditorPrograms(Dictionary configuration) + private static void PopulateDefaultEditorPrograms(Dictionary configuration) { var editorPrograms = new List() { diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/FileTime.GuiApp.Abstractions.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/FileTime.GuiApp.Abstractions.csproj index 2b58a7e..7abadc8 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/FileTime.GuiApp.Abstractions.csproj +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/FileTime.GuiApp.Abstractions.csproj @@ -22,6 +22,7 @@ + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs index 2a88927..3cc81c6 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs @@ -8,6 +8,7 @@ using FileTime.App.Search; using FileTime.GuiApp.Font; using FileTime.GuiApp.ViewModels; using FileTime.GuiApp.Views; +using FileTime.Server.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -19,7 +20,8 @@ public class App : Application { var configuration = Startup.CreateConfiguration(); DI.ServiceProvider = DependencyInjection - .RegisterDefaultServices() + .RegisterDefaultServices(configuration: configuration) + .AddRemoteServices() .AddFrequencyNavigation() .AddCommandPalette() .AddSearch() diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj index 3389591..158a96a 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj @@ -52,4 +52,7 @@ + + + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs index d8d1c0b..b1e2366 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Runtime.InteropServices; using FileTime.App.Core.Services; @@ -14,7 +13,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Serilog; -using Serilog.Configuration; namespace FileTime.GuiApp.App; @@ -25,7 +23,8 @@ public static class Startup var configurationBuilder = new ConfigurationBuilder() .AddInMemoryCollection(MainConfiguration.Configuration) .AddJsonFile("appsettings.json", optional: true) - .AddJsonFile($"appsettings.{Program.EnvironmentName}.json", true); + .AddJsonFile($"appsettings.{Program.EnvironmentName}.json", true) + .AddJsonFile("appsettings.Local.json", optional: true); var configurationDirectory = new DirectoryInfo(Path.Combine(Program.AppDataRoot, "config")); if (configurationDirectory.Exists) diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Assets/material/security.svg b/src/GuiApp/Avalonia/FileTime.GuiApp/Assets/material/security.svg new file mode 100644 index 0000000..233489b --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Assets/material/security.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj index dbe2965..4608030 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj @@ -42,6 +42,7 @@ + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/IMainWindowViewModel.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/IMainWindowViewModel.cs index 523fc19..f763989 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/IMainWindowViewModel.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/IMainWindowViewModel.cs @@ -2,6 +2,7 @@ using FileTime.App.Core.Services; using FileTime.App.FrequencyNavigation.Services; using FileTime.GuiApp.Services; +using FileTime.Providers.LocalAdmin; namespace FileTime.GuiApp.ViewModels; @@ -13,5 +14,6 @@ public interface IMainWindowViewModel : IMainWindowViewModelBase IDialogService DialogService { get; } IFrequencyNavigationService FrequencyNavigationService { get; } ICommandPaletteService CommandPaletteService { get; } - public IRefreshSmoothnessCalculator RefreshSmoothnessCalculator { get; } + IRefreshSmoothnessCalculator RefreshSmoothnessCalculator { get; } + IAdminElevationManager AdminElevationManager { get; } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs index b5ff2ca..159ffae 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/ViewModels/MainWindowViewModel.cs @@ -9,6 +9,7 @@ using FileTime.Core.Models; using FileTime.Core.Timeline; using FileTime.GuiApp.Services; using FileTime.Providers.Local; +using FileTime.Providers.LocalAdmin; using Microsoft.Extensions.Logging; using MvvmGen; @@ -29,6 +30,7 @@ namespace FileTime.GuiApp.ViewModels; [Inject(typeof(IFrequencyNavigationService), PropertyAccessModifier = AccessModifier.Public)] [Inject(typeof(ICommandPaletteService), PropertyAccessModifier = AccessModifier.Public)] [Inject(typeof(IRefreshSmoothnessCalculator), PropertyAccessModifier = AccessModifier.Public)] +[Inject(typeof(IAdminElevationManager), PropertyAccessModifier = AccessModifier.Public)] public partial class MainWindowViewModel : IMainWindowViewModel { public bool Loading => false; diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml index e22f8a5..8f299eb 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml @@ -62,10 +62,24 @@ - - - - + + + + + + + + + diff --git a/src/Library/DeclarativeProperty/DebounceProperty.cs b/src/Library/DeclarativeProperty/DebounceProperty.cs index 424a442..2e21b20 100644 --- a/src/Library/DeclarativeProperty/DebounceProperty.cs +++ b/src/Library/DeclarativeProperty/DebounceProperty.cs @@ -10,7 +10,7 @@ public sealed class DebounceProperty : TimingPropertyBase public DebounceProperty( IDeclarativeProperty from, - TimeSpan interval, + Func interval, Action? setValueHook = null) : base(from, interval, setValueHook) { } @@ -33,7 +33,7 @@ public sealed class DebounceProperty : TimingPropertyBase { try { - while (DateTime.Now - _startTime < Interval) + while (DateTime.Now - _startTime < Interval()) { await Task.Delay(WaitInterval, newToken); } diff --git a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs index 6e189ba..c280152 100644 --- a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs +++ b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs @@ -7,7 +7,10 @@ namespace DeclarativeProperty; public static class DeclarativePropertyExtensions { public static IDeclarativeProperty Debounce(this IDeclarativeProperty from, TimeSpan interval, bool resetTimer = false) - => new DebounceProperty(from, interval){ResetTimer = resetTimer}; + => new DebounceProperty(from, () => interval) {ResetTimer = resetTimer}; + + public static IDeclarativeProperty Debounce(this IDeclarativeProperty from, Func interval, bool resetTimer = false) + => new DebounceProperty(from, interval) {ResetTimer = resetTimer}; public static IDeclarativeProperty DistinctUntilChanged(this IDeclarativeProperty from) => new DistinctUntilChangedProperty(from); diff --git a/src/Library/DeclarativeProperty/ThrottleProperty.cs b/src/Library/DeclarativeProperty/ThrottleProperty.cs index 600cccb..a494967 100644 --- a/src/Library/DeclarativeProperty/ThrottleProperty.cs +++ b/src/Library/DeclarativeProperty/ThrottleProperty.cs @@ -7,7 +7,7 @@ public class ThrottleProperty : TimingPropertyBase public ThrottleProperty( IDeclarativeProperty from, - TimeSpan interval, + Func interval, Action? setValueHook = null) : base(from, interval, setValueHook) { } @@ -15,7 +15,8 @@ public class ThrottleProperty : TimingPropertyBase protected override Task SetValue(T? next, CancellationToken cancellationToken = default) { _debounceCts?.Cancel(); - if (DateTime.Now - _lastFired > Interval) + var interval = Interval(); + if (DateTime.Now - _lastFired > interval) { _lastFired = DateTime.Now; // Note: Recursive chains can happen. Awaiting this can cause a deadlock. @@ -28,7 +29,7 @@ public class ThrottleProperty : TimingPropertyBase { try { - await Task.Delay(Interval, _debounceCts.Token); + await Task.Delay(interval, _debounceCts.Token); await FireIfNeededAsync( next, () => { _lastFired = DateTime.Now; }, diff --git a/src/Library/DeclarativeProperty/TimingPropertyBase.cs b/src/Library/DeclarativeProperty/TimingPropertyBase.cs index 075e21f..ddaaa89 100644 --- a/src/Library/DeclarativeProperty/TimingPropertyBase.cs +++ b/src/Library/DeclarativeProperty/TimingPropertyBase.cs @@ -3,11 +3,11 @@ public abstract class TimingPropertyBase : DeclarativePropertyBase { private readonly SemaphoreSlim _semaphore = new(1, 1); - protected TimeSpan Interval { get; } + protected Func Interval { get; } protected TimingPropertyBase( IDeclarativeProperty from, - TimeSpan interval, + Func interval, Action? setValueHook = null) : base(from.Value, setValueHook) { Interval = interval; @@ -65,7 +65,7 @@ public abstract class TimingPropertyBase : DeclarativePropertyBase CancellationToken timingCancellationToken = default, CancellationToken cancellationToken = default) { - await Task.Delay(Interval, timingCancellationToken); + await Task.Delay(Interval(), timingCancellationToken); var shouldFire = WithLock(() => { if (timingCancellationToken.IsCancellationRequested) diff --git a/src/Providers/FileTime.Providers.Local.Abstractions/LocalContentProviderConstants.cs b/src/Providers/FileTime.Providers.Local.Abstractions/LocalContentProviderConstants.cs new file mode 100644 index 0000000..46af30e --- /dev/null +++ b/src/Providers/FileTime.Providers.Local.Abstractions/LocalContentProviderConstants.cs @@ -0,0 +1,6 @@ +namespace FileTime.Providers.Local; + +public static class LocalContentProviderConstants +{ + public const string ContentProviderId = "local"; +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/FileTime.Providers.Local.csproj b/src/Providers/FileTime.Providers.Local/FileTime.Providers.Local.csproj index 908b309..cec897a 100644 --- a/src/Providers/FileTime.Providers.Local/FileTime.Providers.Local.csproj +++ b/src/Providers/FileTime.Providers.Local/FileTime.Providers.Local.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index 4df7f60..41c7175 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -14,7 +14,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo private readonly ITimelessContentProvider _timelessContentProvider; private readonly bool _isCaseInsensitive; - public LocalContentProvider(ITimelessContentProvider timelessContentProvider) : base("local") + public LocalContentProvider(ITimelessContentProvider timelessContentProvider) : base(LocalContentProviderConstants.ContentProviderId) { _timelessContentProvider = timelessContentProvider; _isCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); diff --git a/src/Providers/FileTime.Providers.Local/LocalContentWriter.cs b/src/Providers/FileTime.Providers.Local/LocalContentWriter.cs index a8d72eb..da7c8a7 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentWriter.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentWriter.cs @@ -6,7 +6,7 @@ public class LocalContentWriter : IContentWriter { private readonly FileStream _writerStream; private readonly BinaryWriter _binaryWriter; - private bool disposed; + private bool _disposed; public int PreferredBufferSize => 1024 * 1024; public LocalContentWriter(FileStream writerStream) @@ -47,7 +47,7 @@ public class LocalContentWriter : IContentWriter private void Dispose(bool disposing) { - if (!disposed) + if (!_disposed) { if (disposing) { @@ -55,6 +55,6 @@ public class LocalContentWriter : IContentWriter _binaryWriter.Dispose(); } } - disposed = true; + _disposed = true; } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalContentWriterFactory.cs b/src/Providers/FileTime.Providers.Local/LocalContentWriterFactory.cs index dfcc715..f2e0c4a 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentWriterFactory.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentWriterFactory.cs @@ -6,5 +6,7 @@ namespace FileTime.Providers.Local; public class LocalContentWriterFactory : IContentWriterFactory { public Task CreateContentWriterAsync(IElement element) - => Task.FromResult((IContentWriter)new LocalContentWriter(File.OpenWrite(element.NativePath!.Path))); + { + return Task.FromResult((IContentWriter) new LocalContentWriter(File.OpenWrite(element.NativePath!.Path))); + } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalItemCreator.cs b/src/Providers/FileTime.Providers.Local/LocalItemCreator.cs index a9c3d3b..21e4f6d 100644 --- a/src/Providers/FileTime.Providers.Local/LocalItemCreator.cs +++ b/src/Providers/FileTime.Providers.Local/LocalItemCreator.cs @@ -1,23 +1,57 @@ using FileTime.Core.ContentAccess; using FileTime.Core.Models; +using FileTime.Providers.LocalAdmin; namespace FileTime.Providers.Local; public class LocalItemCreator : ItemCreatorBase { - public override Task CreateContainerAsync(ILocalContentProvider contentProvider, FullName fullName) + private readonly IAdminContentAccessorFactory _adminContentAccessorFactory; + private readonly IAdminContentProvider _adminContentProvider; + + public LocalItemCreator( + IAdminContentAccessorFactory adminContentAccessorFactory, + IAdminContentProvider adminContentProvider) + { + _adminContentAccessorFactory = adminContentAccessorFactory; + _adminContentProvider = adminContentProvider; + } + + public override async Task CreateContainerAsync(ILocalContentProvider contentProvider, FullName fullName) { var path = contentProvider.GetNativePath(fullName).Path; - if (!Directory.Exists(path)) Directory.CreateDirectory(path); + if (Directory.Exists(path)) return; - return Task.CompletedTask; + try + { + Directory.CreateDirectory(path); + } + catch (UnauthorizedAccessException) + { + if (!_adminContentAccessorFactory.IsAdminModeSupported) throw; + + var adminContentAccessor = await _adminContentAccessorFactory.CreateAdminItemCreatorAsync(); + await adminContentAccessor.CreateContainerAsync(_adminContentProvider, fullName); + } } public override async Task CreateElementAsync(ILocalContentProvider contentProvider, FullName fullName) { var path = contentProvider.GetNativePath(fullName).Path; - await using (File.Create(path)) + if (File.Exists(path)) return; + + try { + await using (File.Create(path)) + { + } + } + catch (UnauthorizedAccessException) + { + if (!_adminContentAccessorFactory.IsAdminModeSupported) throw; + + var adminContentAccessor = await _adminContentAccessorFactory.CreateAdminItemCreatorAsync(); + await adminContentAccessor.CreateElementAsync(_adminContentProvider, fullName); } } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/Startup.cs b/src/Providers/FileTime.Providers.Local/Startup.cs index 1994c8b..33df98c 100644 --- a/src/Providers/FileTime.Providers.Local/Startup.cs +++ b/src/Providers/FileTime.Providers.Local/Startup.cs @@ -6,7 +6,7 @@ namespace FileTime.Providers.Local; public static class Startup { - public static IServiceCollection AddLocalServices(this IServiceCollection serviceCollection) + public static IServiceCollection AddLocalProviderServices(this IServiceCollection serviceCollection) { serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(sp => sp.GetRequiredService()); diff --git a/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/AdminElevationConfiguration.cs b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/AdminElevationConfiguration.cs new file mode 100644 index 0000000..4844c29 --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/AdminElevationConfiguration.cs @@ -0,0 +1,9 @@ +namespace FileTime.Providers.LocalAdmin; + +public class AdminElevationConfiguration +{ + public const string SectionName = "AdminElevation"; + public string ServerExecutablePath { get; set; } + public int? ServerPort { get; set; } + public bool? StartProcess { get; set; } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/FileTime.Providers.LocalAdmin.Abstractions.csproj b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/FileTime.Providers.LocalAdmin.Abstractions.csproj new file mode 100644 index 0000000..51adeda --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/FileTime.Providers.LocalAdmin.Abstractions.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + FileTime.Providers.LocalAdmin + + + + + + + + diff --git a/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs new file mode 100644 index 0000000..b124332 --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs @@ -0,0 +1,9 @@ +using FileTime.Providers.Remote; + +namespace FileTime.Providers.LocalAdmin; + +public interface IAdminContentAccessorFactory +{ + bool IsAdminModeSupported { get; } + Task CreateAdminItemCreatorAsync(); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentProvider.cs b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentProvider.cs new file mode 100644 index 0000000..43d848e --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentProvider.cs @@ -0,0 +1,8 @@ +using FileTime.Core.ContentAccess; + +namespace FileTime.Providers.LocalAdmin; + +public interface IAdminContentProvider : IContentProvider +{ + +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminElevationManager.cs b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminElevationManager.cs new file mode 100644 index 0000000..be1c805 --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminElevationManager.cs @@ -0,0 +1,12 @@ +using FileTime.Server.Common; + +namespace FileTime.Providers.LocalAdmin; + +public interface IAdminElevationManager +{ + bool IsAdminModeSupported { get; } + bool IsAdminInstanceRunning { get; } + Task CreateConnectionAsync(); + string ProviderName { get; } + Task CreateAdminInstanceIfNecessaryAsync(string? confirmationMessage = null); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs new file mode 100644 index 0000000..7d7c32c --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using FileTime.Providers.Remote; +using InitableService; + +namespace FileTime.Providers.LocalAdmin; + +public class AdminContentAccessorFactory : IAdminContentAccessorFactory +{ + private readonly IAdminElevationManager _adminElevationManager; + private readonly IServiceProvider _serviceProvider; + + public AdminContentAccessorFactory( + IAdminElevationManager adminElevationManager, + IServiceProvider serviceProvider + ) + { + _adminElevationManager = adminElevationManager; + _serviceProvider = serviceProvider; + } + + public bool IsAdminModeSupported => _adminElevationManager.IsAdminModeSupported; + + public async Task CreateAdminItemCreatorAsync() + { + await _adminElevationManager.CreateAdminInstanceIfNecessaryAsync(); + var connection = await _adminElevationManager.CreateConnectionAsync(); + + Debug.Assert(connection != null); + + var adminItemCreator = _serviceProvider.GetInitableResolver( + connection, + _adminElevationManager.ProviderName) + .GetRequiredService(); + return adminItemCreator; + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs new file mode 100644 index 0000000..87d9528 --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentProvider.cs @@ -0,0 +1,30 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; +using FileTime.Providers.Remote; + +namespace FileTime.Providers.LocalAdmin; + +//TODO: this should be a RemoteContentProvider if there will be one +public class AdminContentProvider : RemoteContentProvider, IAdminContentProvider +{ + public AdminContentProvider() : base("local", "localAdmin") + { + } + + public override Task GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) + => throw new NotImplementedException(); + + public override NativePath GetNativePath(FullName fullName) + => throw new NotImplementedException(); + + public override FullName GetFullName(NativePath nativePath) + => throw new NotImplementedException(); + + public override Task GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public override bool CanHandlePath(NativePath path) + => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs b/src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs new file mode 100644 index 0000000..89411d3 --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs @@ -0,0 +1,197 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using FileTime.App.Core.Services; +using FileTime.Core.Interactions; +using FileTime.Providers.Local; +using FileTime.Server.Common; +using FileTime.Server.Common.Connections.SignalR; +using InitableService; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace FileTime.Providers.LocalAdmin; + +public class AdminElevationManager : IAdminElevationManager, INotifyPropertyChanged, IExitHandler +{ + private class ConnectionInfo + { + public string? SignalRBaseUrl { get; init; } + } + + private readonly SemaphoreSlim _lock = new(1, 1); + private readonly IUserCommunicationService _dialogService; + private readonly ILogger _logger; + private readonly IOptionsMonitor _configuration; + private readonly IServiceProvider _serviceProvider; + private ConnectionInfo? _connectionInfo; + private bool _isAdminInstanceRunning; + private Process? _adminProcess; + + public bool IsAdminModeSupported => true; + private bool StartProcess => _configuration.CurrentValue.StartProcess ?? true; + + public bool IsAdminInstanceRunning + { + get => _isAdminInstanceRunning; + private set => SetField(ref _isAdminInstanceRunning, value); + } + + public string ProviderName => LocalContentProviderConstants.ContentProviderId; + + public AdminElevationManager( + IUserCommunicationService dialogService, + ILogger logger, + IOptionsMonitor configuration, + IServiceProvider serviceProvider + ) + { + _dialogService = dialogService; + _logger = logger; + _configuration = configuration; + _serviceProvider = serviceProvider; + } + + public async Task CreateAdminInstanceIfNecessaryAsync(string? confirmationMessage = null) + { + ArgumentNullException.ThrowIfNull(_configuration.CurrentValue.ServerExecutablePath, "ServerExecutablePath"); + + await _lock.WaitAsync(); + try + { + if (IsAdminInstanceRunning) return; + + confirmationMessage ??= "This operation requires admin privileges. Please confirm to continue."; + var confirmationResult = await _dialogService.ShowMessageBox(confirmationMessage); + if (confirmationResult == MessageBoxResult.Cancel) return; + + var port = _configuration.CurrentValue.ServerPort; + _logger.LogTrace("Admin server port is {Port}", port is null ? "" : $"{port}"); + if (StartProcess || port is null) + { + var portFileName = Path.GetTempFileName(); + var process = new Process + { + StartInfo = new() + { + FileName = _configuration.CurrentValue.ServerExecutablePath, + ArgumentList = + { + "--PortWriter:FileName", + portFileName + }, + UseShellExecute = true, + Verb = "runas" + }, + EnableRaisingEvents = true + }; + process.Exited += ProcessExitHandler; + process.Start(); + _adminProcess = process; + + IsAdminInstanceRunning = true; + + //TODO: timeout + while (!File.Exists(portFileName) || new FileInfo(portFileName).Length == 0) + await Task.Delay(10); + + var content = await File.ReadAllLinesAsync(portFileName); + if (int.TryParse(content.FirstOrDefault(), out var parsedPort)) + { + port = parsedPort; + } + else + { + _logger.LogError( + "Could not parse port from content {Content}", + string.Join(Environment.NewLine, content) + ); + } + } + + var connectionInfo = new ConnectionInfo + { + SignalRBaseUrl = $"http://localhost:{port}/RemoteHub" + }; + + _connectionInfo = connectionInfo; + } + catch (Exception ex) + { + IsAdminInstanceRunning = false; + _logger.LogError(ex, "Error creating admin instance"); + } + finally + { + _lock.Release(); + } + } + + public async Task CreateConnectionAsync() + { + ArgumentNullException.ThrowIfNull(_connectionInfo); + try + { + //TODO: use other connections too (if there will be any) + ArgumentNullException.ThrowIfNull(_connectionInfo.SignalRBaseUrl); + + var connection = await _serviceProvider + .GetAsyncInitableResolver(_connectionInfo.SignalRBaseUrl) + .GetRequiredServiceAsync(); + + return connection; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating SignalR connection"); + throw; + } + } + + private void ProcessExitHandler(object? sender, EventArgs e) + { + _lock.Wait(); + IsAdminInstanceRunning = false; + _lock.Release(); + } + + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } + + public async Task ExitAsync() + { + if (!StartProcess) + { + _logger.LogTrace("Not stopping admin process as it was not started by this instance"); + return; + } + if (!IsAdminInstanceRunning) + { + _logger.LogTrace("Not stopping admin process as it is not running"); + return; + } + + try + { + _logger.LogInformation("Stopping admin process"); + var connection = await CreateConnectionAsync(); + await connection.Exit(); + _logger.LogInformation("Admin process stopped successfully"); + } + catch(Exception ex) + { + _logger.LogError(ex, "Error stopping admin process, trying to kill it"); + _adminProcess?.Kill(); + } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin/FileTime.Providers.LocalAdmin.csproj b/src/Providers/FileTime.Providers.LocalAdmin/FileTime.Providers.LocalAdmin.csproj new file mode 100644 index 0000000..49fc866 --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin/FileTime.Providers.LocalAdmin.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + + ..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\7.0.9\Microsoft.Extensions.Configuration.Abstractions.dll + + + + diff --git a/src/Providers/FileTime.Providers.LocalAdmin/Startup.cs b/src/Providers/FileTime.Providers.LocalAdmin/Startup.cs new file mode 100644 index 0000000..dfadfb0 --- /dev/null +++ b/src/Providers/FileTime.Providers.LocalAdmin/Startup.cs @@ -0,0 +1,20 @@ +using FileTime.App.Core.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace FileTime.Providers.LocalAdmin; + +public static class Startup +{ + public static IServiceCollection AddLocalAdminProviderServices(this IServiceCollection services, IConfigurationRoot configuration) + { + services.AddOptions().Bind(configuration.GetSection(AdminElevationConfiguration.SectionName)); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); + return services; + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote.Abstractions/FileTime.Providers.Remote.Abstractions.csproj b/src/Providers/FileTime.Providers.Remote.Abstractions/FileTime.Providers.Remote.Abstractions.csproj new file mode 100644 index 0000000..b4d1d9a --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote.Abstractions/FileTime.Providers.Remote.Abstractions.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + FileTime.Providers.Remote + + + + + + + + diff --git a/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteContentProvider.cs b/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteContentProvider.cs new file mode 100644 index 0000000..5a71a49 --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteContentProvider.cs @@ -0,0 +1,8 @@ +using FileTime.Core.ContentAccess; + +namespace FileTime.Providers.Remote; + +public interface IRemoteContentProvider : IContentProvider +{ + +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemCreator.cs b/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemCreator.cs new file mode 100644 index 0000000..6e12712 --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemCreator.cs @@ -0,0 +1,12 @@ +using FileTime.Core.ContentAccess; +using FileTime.Server.Common; +using InitableService; + +namespace FileTime.Providers.Remote; + +public interface IRemoteItemCreator : + IItemCreator, + IInitable +{ + +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote/FileTime.Providers.Remote.csproj b/src/Providers/FileTime.Providers.Remote/FileTime.Providers.Remote.csproj new file mode 100644 index 0000000..58f684b --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote/FileTime.Providers.Remote.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + diff --git a/src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs b/src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs new file mode 100644 index 0000000..d40a89e --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote/RemoteContentProvider.cs @@ -0,0 +1,24 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; + +namespace FileTime.Providers.Remote; + +public class RemoteContentProvider : ContentProviderBase, IRemoteContentProvider +{ + public RemoteContentProvider(string remoteName, string name = "remote") : base(name) + { + } + + //TODO implement + public override Task GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default) => throw new NotImplementedException(); + + public override NativePath GetNativePath(FullName fullName) => throw new NotImplementedException(); + + public override FullName GetFullName(NativePath nativePath) => throw new NotImplementedException(); + + public override Task GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public override bool CanHandlePath(NativePath path) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote/RemoteItemCreator.cs b/src/Providers/FileTime.Providers.Remote/RemoteItemCreator.cs new file mode 100644 index 0000000..9e6c724 --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote/RemoteItemCreator.cs @@ -0,0 +1,24 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Models; +using FileTime.Server.Common; + +namespace FileTime.Providers.Remote; + +public class RemoteItemCreator : + ItemCreatorBase, + IRemoteItemCreator +{ + private IRemoteConnection _remoteConnection = null!; + private string _remoteContentProviderId = null!; + public void Init(IRemoteConnection remoteConnection, string remoteContentProviderId) + { + _remoteConnection = remoteConnection; + _remoteContentProviderId = remoteContentProviderId; + } + + public override async Task CreateContainerAsync(IRemoteContentProvider contentProvider, FullName fullName) + => await _remoteConnection.CreateContainerAsync(_remoteContentProviderId, fullName); + + public override async Task CreateElementAsync(IRemoteContentProvider contentProvider, FullName fullName) + => await _remoteConnection.CreateElementAsync(_remoteContentProviderId, fullName); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote/Startup.cs b/src/Providers/FileTime.Providers.Remote/Startup.cs new file mode 100644 index 0000000..59a975e --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote/Startup.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace FileTime.Providers.Remote; + +public static class Startup +{ + + public static IServiceCollection AddRemoteProviderServices(this IServiceCollection serviceCollection) + { + serviceCollection.TryAddSingleton(); + serviceCollection.TryAddTransient(); + return serviceCollection; + + } +} \ No newline at end of file