From 0b36fb939ce3ef4b2cda662d3ebe72860611453b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Wed, 26 Jul 2023 13:51:17 +0200 Subject: [PATCH] RemoteItemMover, Startup/Exit handler refactor --- .../Services/IExitHandler.cs | 2 +- .../Persistence/ITabPersistenceService.cs | 2 +- .../Services/ContainerRefreshHandler.cs | 8 +-- .../Persistence/TabPersistenceService.cs | 18 +++-- src/AppCommon/FileTime.App.Core/Startup.cs | 2 +- .../Services/FrequencyNavigationService.cs | 6 +- .../Extensions/TaskExtensions.cs | 13 ++-- .../Avalonia/FileTime.GuiApp.App/Startup.cs | 2 +- .../Services/LifecycleService.cs | 30 +++++---- .../Services/RootDriveInfoService.cs | 25 +++---- .../Services/WindowsPlacesService.cs | 2 +- .../LocalItemDeleter.cs | 6 +- .../LocalItemMover.cs | 66 +++++++++++++++---- .../IAdminContentAccessorFactory.cs | 1 + .../AdminContentAccessorFactory.cs | 27 ++++---- .../AdminElevationManager.cs | 25 ++++--- .../IRemoteItemMover.cs | 10 +++ .../RemoteItemMover.cs | 19 ++++++ .../FileTime.Providers.Remote/Startup.cs | 3 +- .../IRemoteConnection.cs | 1 + .../Connections/SignalR/ISignalRHub.cs | 1 + .../Connections/SignalR/SignalRConnection.cs | 44 ++++++++----- .../FileTime.Server.Web/ConnectionHub.cs | 7 ++ 23 files changed, 210 insertions(+), 110 deletions(-) create mode 100644 src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemMover.cs create mode 100644 src/Providers/FileTime.Providers.Remote/RemoteItemMover.cs diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IExitHandler.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IExitHandler.cs index 73c1d78..e7d77c4 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IExitHandler.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IExitHandler.cs @@ -2,5 +2,5 @@ namespace FileTime.App.Core.Services; public interface IExitHandler { - Task ExitAsync(); + Task ExitAsync(CancellationToken token = default); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/Persistence/ITabPersistenceService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/Persistence/ITabPersistenceService.cs index 34ea51f..4ff7e42 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Services/Persistence/ITabPersistenceService.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/Persistence/ITabPersistenceService.cs @@ -2,5 +2,5 @@ namespace FileTime.App.Core.Services.Persistence; public interface ITabPersistenceService : IStartupHandler, IExitHandler { - void SaveStates(); + void SaveStates(CancellationToken token = default); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/ContainerRefreshHandler.cs b/src/AppCommon/FileTime.App.Core/Services/ContainerRefreshHandler.cs index a93d44e..3ec7b20 100644 --- a/src/AppCommon/FileTime.App.Core/Services/ContainerRefreshHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/ContainerRefreshHandler.cs @@ -6,7 +6,7 @@ using FileTime.Core.Timeline; namespace FileTime.App.Core.Services; -public class ContainerRefreshHandler : IStartupHandler, IDisposable +public class ContainerRefreshHandler : IExitHandler { private readonly List _refreshSubscriptions = new(); private List _folders = new(); @@ -38,13 +38,13 @@ public class ContainerRefreshHandler : IStartupHandler, IDisposable ); } - public void Dispose() + public Task ExitAsync(CancellationToken token = default) { foreach (var refreshSubscription in _refreshSubscriptions) { refreshSubscription.Dispose(); } - } - public Task InitAsync() => Task.CompletedTask; + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs b/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs index 63ef489..03d8bb6 100644 --- a/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/Persistence/TabPersistenceService.cs @@ -70,14 +70,14 @@ public class TabPersistenceService : ITabPersistenceService }; } - public Task ExitAsync() + public Task ExitAsync(CancellationToken token = default) { - SaveStates(); + SaveStates(token); return Task.CompletedTask; } - private async Task LoadStatesAsync() + private async Task LoadStatesAsync(CancellationToken token = default) { if (!File.Exists(_settingsPath)) { @@ -88,7 +88,7 @@ public class TabPersistenceService : ITabPersistenceService try { await using var stateReader = File.OpenRead(_settingsPath); - var state = await JsonSerializer.DeserializeAsync(stateReader); + var state = await JsonSerializer.DeserializeAsync(stateReader, cancellationToken: token); if (state != null) { if (await RestoreTabs(state.TabStates)) return; @@ -187,7 +187,7 @@ public class TabPersistenceService : ITabPersistenceService return true; } - public void SaveStates() + public void SaveStates(CancellationToken token = default) { var state = new PersistenceRoot { @@ -210,15 +210,13 @@ public class TabPersistenceService : ITabPersistenceService tabStates.Add(new TabState(currentLocation.FullName!, tab.TabNumber)); } - return new TabStates() + return new TabStates { Tabs = tabStates, ActiveTabNumber = _appState.CurrentSelectedTab?.TabNumber }; } - public async Task InitAsync() - { - await LoadStatesAsync(); - } + public async Task InitAsync() + => await LoadStatesAsync(); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Startup.cs b/src/AppCommon/FileTime.App.Core/Startup.cs index 0d22be8..095d4c5 100644 --- a/src/AppCommon/FileTime.App.Core/Startup.cs +++ b/src/AppCommon/FileTime.App.Core/Startup.cs @@ -31,7 +31,7 @@ public static class Startup return serviceCollection .AddCommandHandlers() .AddSingleton() - .AddSingleton(); + .AddSingleton(); } private static IServiceCollection AddCommandHandlers(this IServiceCollection serviceCollection) diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs b/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs index 7fad564..0536558 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs @@ -190,11 +190,11 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I } } - public async Task ExitAsync() => await SaveStateAsync(); + public async Task ExitAsync(CancellationToken token = default) => await SaveStateAsync(token); - private async Task SaveStateAsync() + private async Task SaveStateAsync(CancellationToken token = default) { - await _saveLock.WaitAsync(); + await _saveLock.WaitAsync(token); try { _lastSave = DateTime.Now; diff --git a/src/Core/FileTime.Core.Abstraction/Extensions/TaskExtensions.cs b/src/Core/FileTime.Core.Abstraction/Extensions/TaskExtensions.cs index 759b8c0..cc0fdbe 100644 --- a/src/Core/FileTime.Core.Abstraction/Extensions/TaskExtensions.cs +++ b/src/Core/FileTime.Core.Abstraction/Extensions/TaskExtensions.cs @@ -2,15 +2,20 @@ namespace FileTime.Core.Extensions; public static class TaskExtensions { - public static async Task AwaitWithTimeout(this Task task, int timeout, T? defaultValue = default) + public static async Task TimeoutAfter(this Task task, int timeoutInMilliseconds) { - if (await Task.WhenAny(task, Task.Delay(timeout)) == task) + if (await Task.WhenAny(task, Task.Delay(timeoutInMilliseconds)) != task) + throw new TimeoutException(); + } + public static async Task TimeoutAfter(this Task task, int timeoutInMilliseconds, T? defaultValue = default) + { + if (await Task.WhenAny(task, Task.Delay(timeoutInMilliseconds)) == task) { - return task.Result; + return await task; } else { - return defaultValue; + throw new TimeoutException(); } } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs index b1e2366..de0bbd2 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs @@ -77,7 +77,7 @@ public static class Startup } return serviceCollection - .AddSingleton() + .AddSingleton() .AddSingleton(sp => sp.GetRequiredService()); } diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/LifecycleService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/LifecycleService.cs index 5997611..73480fc 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/LifecycleService.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/LifecycleService.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Services; +using FileTime.Core.Extensions; using Microsoft.Extensions.Logging; namespace FileTime.GuiApp.Services; @@ -36,27 +37,30 @@ public class LifecycleService public async Task ExitAsync() { - foreach (var exitHandler in _exitHandlers) + var exitCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var exitHandlerTasks = _exitHandlers.Select(e => { try { - await exitHandler.ExitAsync(); + return e.ExitAsync(exitCancellation.Token); } catch (Exception ex) { - _logger.LogError(ex, "Error while running exit handler {Handler}", exitHandler?.GetType().FullName); + _logger.LogError(ex, "Error while running exit handler {Handler}", e.GetType().FullName); } + + return Task.CompletedTask; + }); + + try + { + await Task.WhenAll(exitHandlerTasks).TimeoutAfter(10000); + } + catch + { + } - foreach (var disposable in - _startupHandlers - .OfType() - .Concat( - _exitHandlers.OfType() - ) - ) - { - disposable.Dispose(); - } + exitCancellation.Cancel(); } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/RootDriveInfoService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/RootDriveInfoService.cs index 1e8e2e4..8233913 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/RootDriveInfoService.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/RootDriveInfoService.cs @@ -7,7 +7,7 @@ using ObservableComputations; namespace FileTime.GuiApp.Services; -public class RootDriveInfoService : IStartupHandler, IDisposable +public class RootDriveInfoService : IExitHandler { private readonly ILocalContentProvider _localContentProvider; private readonly List _rootDrives = new(); @@ -21,13 +21,13 @@ public class RootDriveInfoService : IStartupHandler, IDisposable InitRootDrives(); var rootDriveInfos = localContentProvider.Items.Selecting( - i => MatchRootDrive(i) - ) - .Filtering(t => IsNotNull(t.Drive)) - .Selecting(t => Resolve(t)) - .Filtering(t => t.Item is IContainer) - .Selecting(t => new RootDriveInfo(t.Drive, (IContainer)t.Item!)) - .Ordering(d => d.Name); + i => MatchRootDrive(i) + ) + .Filtering(t => IsNotNull(t.Drive)) + .Selecting(t => Resolve(t)) + .Filtering(t => t.Item is IContainer) + .Selecting(t => new RootDriveInfo(t.Drive, (IContainer) t.Item!)) + .Ordering(d => d.Name); rootDriveInfos.For(_rootDriveInfosConsumer); @@ -51,7 +51,7 @@ public class RootDriveInfoService : IStartupHandler, IDisposable } private static bool IsNotNull(object? obj) => obj is not null; - + private static (IItem? Item, DriveInfo Drive) Resolve((AbsolutePath Path, DriveInfo? Drive) tuple) { var t = Task.Run(async () => await tuple.Path.ResolveAsyncSafe()); @@ -73,6 +73,9 @@ public class RootDriveInfoService : IStartupHandler, IDisposable return (Path: sourceItem, Drive: rootDrive); } - public Task InitAsync() => Task.CompletedTask; - public void Dispose() => _rootDriveInfosConsumer.Dispose(); + public Task ExitAsync(CancellationToken token = default) + { + _rootDriveInfosConsumer.Dispose(); + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/WindowsPlacesService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/WindowsPlacesService.cs index 0f779e0..6880fff 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/WindowsPlacesService.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/WindowsPlacesService.cs @@ -51,7 +51,7 @@ public class WindowsPlacesService : IPlacesService } public Dictionary GetSpecialPaths() - => new Dictionary + => new() { {KnownFolders.Desktop.Path, SpecialPathType.Desktop}, {KnownFolders.Documents.Path, SpecialPathType.Documents}, diff --git a/src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs b/src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs index 9bbd39a..8a88d2e 100644 --- a/src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs +++ b/src/Providers/FileTime.Providers.Local/LocalItemDeleter.cs @@ -44,12 +44,12 @@ public class LocalItemDeleter : IItemDeleter throw new FileNotFoundException(nativePath); } } - catch (Exception e) + catch (Exception ex) { - _logger.LogDebug(e, "Failed to delete item with path {Path}", nativePath); + _logger.LogDebug(ex, "Failed to delete item with path {Path}", nativePath); if (!_adminContentAccessorFactory.IsAdminModeSupported - || e is not UnauthorizedAccessException and not IOException) + || ex is not UnauthorizedAccessException and not IOException) { _logger.LogTrace( "Admin mode is disabled or exception is not an access denied one, not trying to create {Path} as admin", diff --git a/src/Providers/FileTime.Providers.Local/LocalItemMover.cs b/src/Providers/FileTime.Providers.Local/LocalItemMover.cs index af969a4..457083e 100644 --- a/src/Providers/FileTime.Providers.Local/LocalItemMover.cs +++ b/src/Providers/FileTime.Providers.Local/LocalItemMover.cs @@ -1,28 +1,66 @@ using FileTime.Core.ContentAccess; using FileTime.Core.Models; +using FileTime.Providers.LocalAdmin; +using Microsoft.Extensions.Logging; namespace FileTime.Providers.Local; public class LocalItemMover : IItemMover { - public Task RenameAsync(ILocalContentProvider contentProvider, FullName fullName, FullName newPath) + private readonly IAdminContentAccessorFactory _adminContentAccessorFactory; + private readonly IAdminContentProvider _adminContentProvider; + private readonly ILogger _logger; + + public LocalItemMover( + IAdminContentAccessorFactory adminContentAccessorFactory, + IAdminContentProvider adminContentProvider, + ILogger logger) { - var source = contentProvider.GetNativePath(fullName); - var destination = contentProvider.GetNativePath(newPath); - - if (File.Exists(source.Path)) + _adminContentAccessorFactory = adminContentAccessorFactory; + _adminContentProvider = adminContentProvider; + _logger = logger; + } + + public async Task RenameAsync(ILocalContentProvider contentProvider, FullName fullName, FullName newPath) + { + _logger.LogTrace("Start renaming item {FullName}", fullName); + try { - File.Move(source.Path, destination.Path); + var source = contentProvider.GetNativePath(fullName); + var destination = contentProvider.GetNativePath(newPath); + + if (File.Exists(source.Path)) + { + _logger.LogTrace("File exists with path {Path}", fullName); + File.Move(source.Path, destination.Path); + } + else if (Directory.Exists(source.Path)) + { + _logger.LogTrace("Directory exists with path {Path}", fullName); + Directory.Move(source.Path, destination.Path); + } + else + { + _logger.LogTrace("No file or directory exists with path {Path}", fullName); + throw new FileNotFoundException(source.Path); + } } - else if (Directory.Exists(source.Path)) + catch (Exception ex) { - Directory.Move(source.Path, destination.Path); + _logger.LogDebug(ex, "Failed to rename item {From} to {To}", fullName, newPath); + + if (!_adminContentAccessorFactory.IsAdminModeSupported + || ex is not UnauthorizedAccessException and not IOException) + { + _logger.LogTrace( + "Admin mode is disabled or exception is not an access denied one, not trying to rename {Path} as admin", + fullName + ); + throw; + } + + var adminItemMover = await _adminContentAccessorFactory.CreateAdminItemMoverAsync(); + await adminItemMover.RenameAsync(_adminContentProvider, fullName, newPath); } - else - { - throw new FileNotFoundException(source.Path); - } - - return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs index ec975a9..8e6e70c 100644 --- a/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs +++ b/src/Providers/FileTime.Providers.LocalAdmin.Abstractions/IAdminContentAccessorFactory.cs @@ -7,4 +7,5 @@ public interface IAdminContentAccessorFactory bool IsAdminModeSupported { get; } Task CreateAdminItemCreatorAsync(); Task CreateAdminItemDeleterAsync(); + Task CreateAdminItemMoverAsync(); } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs index 52ad992..e3e2bc3 100644 --- a/src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs +++ b/src/Providers/FileTime.Providers.LocalAdmin/AdminContentAccessorFactory.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using FileTime.Providers.Remote; +using FileTime.Server.Common; using InitableService; namespace FileTime.Providers.LocalAdmin; @@ -21,30 +22,26 @@ public class AdminContentAccessorFactory : IAdminContentAccessorFactory 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; - } + => await CreateHelperAsync(); public async Task CreateAdminItemDeleterAsync() + => await CreateHelperAsync(); + + public async Task CreateAdminItemMoverAsync() + => await CreateHelperAsync(); + + private async Task CreateHelperAsync() + where T : class, IInitable { await _adminElevationManager.CreateAdminInstanceIfNecessaryAsync(); var connection = await _adminElevationManager.CreateConnectionAsync(); Debug.Assert(connection != null); - var adminItemDeleter = _serviceProvider.GetInitableResolver( + var helper = _serviceProvider.GetInitableResolver( connection, _adminElevationManager.ProviderName) - .GetRequiredService(); - return adminItemDeleter; + .GetRequiredService(); + return helper; } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs b/src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs index 89411d3..98beae6 100644 --- a/src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs +++ b/src/Providers/FileTime.Providers.LocalAdmin/AdminElevationManager.cs @@ -135,10 +135,7 @@ public class AdminElevationManager : IAdminElevationManager, INotifyPropertyChan //TODO: use other connections too (if there will be any) ArgumentNullException.ThrowIfNull(_connectionInfo.SignalRBaseUrl); - var connection = await _serviceProvider - .GetAsyncInitableResolver(_connectionInfo.SignalRBaseUrl) - .GetRequiredServiceAsync(); - + var connection = await SignalRConnection.GetOrCreateForAsync(_connectionInfo.SignalRBaseUrl); return connection; } catch (Exception ex) @@ -168,13 +165,14 @@ public class AdminElevationManager : IAdminElevationManager, INotifyPropertyChan return true; } - public async Task ExitAsync() + public async Task ExitAsync(CancellationToken token = default) { 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"); @@ -188,10 +186,21 @@ public class AdminElevationManager : IAdminElevationManager, INotifyPropertyChan await connection.Exit(); _logger.LogInformation("Admin process stopped successfully"); } - catch(Exception ex) + catch (Exception ex) { - _logger.LogError(ex, "Error stopping admin process, trying to kill it"); - _adminProcess?.Kill(); + _logger.LogError(ex, "Error stopping admin process"); + if (_adminProcess is null) return; + + for (var i = 0; i < 150 && !_adminProcess.HasExited; i++) + { + await Task.Delay(10); + } + + /*if (!_adminProcess.HasExited) + { + _logger.LogInformation("Admin process dit not stopped, killing it"); + _adminProcess.Kill(); + }*/ } } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemMover.cs b/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemMover.cs new file mode 100644 index 0000000..f3c1913 --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote.Abstractions/IRemoteItemMover.cs @@ -0,0 +1,10 @@ +using FileTime.Core.ContentAccess; +using FileTime.Server.Common; +using InitableService; + +namespace FileTime.Providers.Remote; + +public interface IRemoteItemMover : IItemMover, IInitable +{ + +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote/RemoteItemMover.cs b/src/Providers/FileTime.Providers.Remote/RemoteItemMover.cs new file mode 100644 index 0000000..768e2c2 --- /dev/null +++ b/src/Providers/FileTime.Providers.Remote/RemoteItemMover.cs @@ -0,0 +1,19 @@ +using FileTime.Core.Models; +using FileTime.Server.Common; + +namespace FileTime.Providers.Remote; + +public class RemoteItemMover : IRemoteItemMover +{ + + private IRemoteConnection _remoteConnection = null!; + private string _remoteContentProviderId = null!; + public void Init(IRemoteConnection remoteConnection, string remoteContentProviderId) + { + _remoteConnection = remoteConnection; + _remoteContentProviderId = remoteContentProviderId; + } + + public async Task RenameAsync(IRemoteContentProvider contentProvider, FullName fullName, FullName newPath) + => await _remoteConnection.MoveItemAsync(_remoteContentProviderId, fullName, newPath); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Remote/Startup.cs b/src/Providers/FileTime.Providers.Remote/Startup.cs index fe2cf4d..6be7b19 100644 --- a/src/Providers/FileTime.Providers.Remote/Startup.cs +++ b/src/Providers/FileTime.Providers.Remote/Startup.cs @@ -5,13 +5,12 @@ namespace FileTime.Providers.Remote; public static class Startup { - public static IServiceCollection AddRemoteProviderServices(this IServiceCollection serviceCollection) { serviceCollection.TryAddSingleton(); serviceCollection.TryAddTransient(); serviceCollection.TryAddTransient(); + serviceCollection.TryAddTransient(); return serviceCollection; - } } \ No newline at end of file diff --git a/src/Server/FileTime.Server.Common.Abstractions/IRemoteConnection.cs b/src/Server/FileTime.Server.Common.Abstractions/IRemoteConnection.cs index 52388cc..c99c791 100644 --- a/src/Server/FileTime.Server.Common.Abstractions/IRemoteConnection.cs +++ b/src/Server/FileTime.Server.Common.Abstractions/IRemoteConnection.cs @@ -8,4 +8,5 @@ public interface IRemoteConnection Task CreateContainerAsync(string contentProviderId, FullName fullName); Task CreateElementAsync(string contentProviderId, FullName fullName); Task DeleteItemAsync(string contentProviderId, FullName fullName); + Task MoveItemAsync(string contentProviderId, FullName fullName, FullName newPath); } \ No newline at end of file diff --git a/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRHub.cs b/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRHub.cs index 94f90ee..91d1fdd 100644 --- a/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRHub.cs +++ b/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRHub.cs @@ -6,4 +6,5 @@ public interface ISignalRHub Task CreateContainerAsync(string contentProviderId, string fullName); Task CreateElementAsync(string contentProviderId, string fullName); Task DeleteItemAsync(string contentProviderId, string fullName); + Task MoveItemAsync(string contentProviderId, string fullName, string newPath); } \ No newline at end of file diff --git a/src/Server/FileTime.Server.Common/Connections/SignalR/SignalRConnection.cs b/src/Server/FileTime.Server.Common/Connections/SignalR/SignalRConnection.cs index b47f447..098c0c9 100644 --- a/src/Server/FileTime.Server.Common/Connections/SignalR/SignalRConnection.cs +++ b/src/Server/FileTime.Server.Common/Connections/SignalR/SignalRConnection.cs @@ -7,10 +7,10 @@ namespace FileTime.Server.Common.Connections.SignalR; public class SignalRConnection : IRemoteConnection, IAsyncInitable { + private static readonly Dictionary Connections = new(); private string _baseUrl = null!; private HubConnection _connection = null!; - - private ISignalRHub CreateClient() => _connection.CreateHubProxy(); + private ISignalRHub _client = null!; public async Task InitAsync(string baseUrl) { @@ -20,29 +20,37 @@ public class SignalRConnection : IRemoteConnection, IAsyncInitable .WithUrl(_baseUrl) .Build(); await _connection.StartAsync(); + _client = _connection.CreateHubProxy(); + } + + public static async Task GetOrCreateForAsync(string baseUrl) + { + if (Connections.TryGetValue(baseUrl, out var connection)) + { + if (connection._connection.State != HubConnectionState.Disconnected) + { + return connection; + } + Connections.Remove(baseUrl); + } + connection = new SignalRConnection(); + Connections.Add(baseUrl, connection); + await connection.InitAsync(baseUrl); + return connection; } public async Task Exit() - { - var client = CreateClient(); - await client.Exit(); - } + => await _client.Exit(); public async Task CreateContainerAsync(string contentProviderId, FullName fullName) - { - var client = CreateClient(); - await client.CreateContainerAsync(contentProviderId, fullName.Path); - } + => await _client.CreateContainerAsync(contentProviderId, fullName.Path); public async Task CreateElementAsync(string contentProviderId, FullName fullName) - { - var client = CreateClient(); - await client.CreateElementAsync(contentProviderId, fullName.Path); - } + => await _client.CreateElementAsync(contentProviderId, fullName.Path); public async Task DeleteItemAsync(string contentProviderId, FullName fullName) - { - var client = CreateClient(); - await client.DeleteItemAsync(contentProviderId, fullName.Path); - } + => await _client.DeleteItemAsync(contentProviderId, fullName.Path); + + public async Task MoveItemAsync(string contentProviderId, FullName fullName, FullName newPath) + => await _client.MoveItemAsync(contentProviderId, fullName.Path, newPath.Path); } \ No newline at end of file diff --git a/src/Server/FileTime.Server.Web/ConnectionHub.cs b/src/Server/FileTime.Server.Web/ConnectionHub.cs index 2ad2613..16cf313 100644 --- a/src/Server/FileTime.Server.Web/ConnectionHub.cs +++ b/src/Server/FileTime.Server.Web/ConnectionHub.cs @@ -49,4 +49,11 @@ public class ConnectionHub : Hub, ISignalRHub var itemDeleter = _contentAccessorFactory.GetItemDeleter(contentProvider); await itemDeleter.DeleteAsync(contentProvider, new FullName(fullName)); } + + public async Task MoveItemAsync(string contentProviderId, string fullName, string newPath) + { + var contentProvider = _contentProviderRegistry.ContentProviders.First(p => p.Name == contentProviderId); + var itemDeleter = _contentAccessorFactory.GetItemMover(contentProvider); + await itemDeleter.RenameAsync(contentProvider, new FullName(fullName), new FullName(newPath)); + } } \ No newline at end of file