From ec03067af1b7ec8e9f82548294e374a5cfc92a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Thu, 10 Feb 2022 11:24:04 +0100 Subject: [PATCH] Smb save servers, auth --- src/Core/FileTime.Core/Models/Constants.cs | 1 + src/Core/FileTime.Core/Models/IContainer.cs | 21 ++- .../Persistence/PersistenceSettings.cs | 12 ++ .../Models/Persistence/TabState.cs | 5 +- .../Services/StatePersistenceService.cs | 78 ++++++---- src/GuiApp/FileTime.Avalonia/Startup.cs | 4 + .../ViewModels/ContainerViewModel.cs | 99 +++++++++---- .../FileTime.Avalonia/Views/MainWindow.axaml | 19 ++- .../FileTime.Providers.Local/LocalFolder.cs | 3 +- .../FileTime.Providers.Smb.csproj | 5 +- .../Persistence/PersistenceService.cs | 133 ++++++++++++++++++ .../Persistence/ServersPersistenceRoot.cs | 8 ++ .../Persistence/SmbServer.cs | 10 ++ .../SmbClientContext.cs | 2 +- .../SmbContentProvider.cs | 83 ++++++++++- .../FileTime.Providers.Smb/SmbFolder.cs | 19 --- .../FileTime.Providers.Smb/SmbServer.cs | 52 ++++--- .../FileTime.Providers.Smb/SmbShare.cs | 27 +--- .../FileTime.Providers.Smb/Startup.cs | 14 ++ 19 files changed, 463 insertions(+), 132 deletions(-) create mode 100644 src/Core/FileTime.Core/Persistence/PersistenceSettings.cs create mode 100644 src/Providers/FileTime.Providers.Smb/Persistence/PersistenceService.cs create mode 100644 src/Providers/FileTime.Providers.Smb/Persistence/ServersPersistenceRoot.cs create mode 100644 src/Providers/FileTime.Providers.Smb/Persistence/SmbServer.cs create mode 100644 src/Providers/FileTime.Providers.Smb/Startup.cs diff --git a/src/Core/FileTime.Core/Models/Constants.cs b/src/Core/FileTime.Core/Models/Constants.cs index 7244ea6..f8d8aaf 100644 --- a/src/Core/FileTime.Core/Models/Constants.cs +++ b/src/Core/FileTime.Core/Models/Constants.cs @@ -3,5 +3,6 @@ namespace FileTime.Core.Models public static class Constants { public const char SeparatorChar = '/'; + public const string ContentProviderProtocol = "ctp://"; } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IContainer.cs b/src/Core/FileTime.Core/Models/IContainer.cs index 95ce589..f1231ea 100644 --- a/src/Core/FileTime.Core/Models/IContainer.cs +++ b/src/Core/FileTime.Core/Models/IContainer.cs @@ -10,7 +10,26 @@ namespace FileTime.Core.Models Task?> GetElements(CancellationToken token = default); Task RefreshAsync(CancellationToken token = default); - Task GetByPath(string path, bool acceptDeepestMatch = false); + async Task GetByPath(string path, bool acceptDeepestMatch = false) + { + var paths = path.Split(Constants.SeparatorChar); + + var item = (await GetItems())?.FirstOrDefault(i => i.Name == paths[0]); + + if (paths.Length == 1) + { + return item; + } + + if (item is IContainer container) + { + var result = await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch); + return result == null && acceptDeepestMatch ? this : result; + } + + return null; + } + Task CreateContainer(string name); Task CreateElement(string name); diff --git a/src/Core/FileTime.Core/Persistence/PersistenceSettings.cs b/src/Core/FileTime.Core/Persistence/PersistenceSettings.cs new file mode 100644 index 0000000..5513386 --- /dev/null +++ b/src/Core/FileTime.Core/Persistence/PersistenceSettings.cs @@ -0,0 +1,12 @@ +namespace FileTime.Core.Persistence +{ + public class PersistenceSettings + { + public PersistenceSettings(string rootAppDataPath) + { + RootAppDataPath = rootAppDataPath; + } + + public string RootAppDataPath { get; } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Models/Persistence/TabState.cs b/src/GuiApp/FileTime.Avalonia/Models/Persistence/TabState.cs index f132da7..1cf9ea5 100644 --- a/src/GuiApp/FileTime.Avalonia/Models/Persistence/TabState.cs +++ b/src/GuiApp/FileTime.Avalonia/Models/Persistence/TabState.cs @@ -1,4 +1,6 @@ using FileTime.Avalonia.Application; +using FileTime.Core.Models; +using FileTime.Core.Providers; namespace FileTime.Avalonia.Models.Persistence { @@ -11,7 +13,8 @@ namespace FileTime.Avalonia.Models.Persistence public TabState(TabContainer tab) { - Path = tab.CurrentLocation.Item.FullName; + var item = tab.CurrentLocation.Item; + Path = item is IContentProvider contentProvider ? Constants.ContentProviderProtocol + contentProvider.Name : item.FullName; Number = tab.TabNumber; } } diff --git a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs index ae8b892..46b20bf 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs @@ -12,6 +12,7 @@ using FileTime.Core.Components; using FileTime.Core.Providers; using FileTime.Providers.Local; using FileTime.Core.Models; +using Microsoft.Extensions.Logging; namespace FileTime.Avalonia.Services { @@ -23,22 +24,26 @@ namespace FileTime.Avalonia.Services private readonly string _settingsPath; private readonly IEnumerable _contentProviders; private readonly LocalContentProvider _localContentProvider; + private readonly ILogger _logger; public StatePersistenceService( AppState appState, ItemNameConverterService itemNameConverterService, IEnumerable contentProviders, - LocalContentProvider localContentProvider) + LocalContentProvider localContentProvider, + ILogger logger) { _appState = appState; _itemNameConverterService = itemNameConverterService; _contentProviders = contentProviders; _localContentProvider = localContentProvider; + _logger = logger; _settingsPath = Path.Combine(Program.AppDataRoot, "savedState.json"); _jsonOptions = new JsonSerializerOptions() { - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, + WriteIndented = true }; } @@ -55,7 +60,10 @@ namespace FileTime.Avalonia.Services await RestoreTabs(state.TabStates); } } - catch { } + catch (Exception e) + { + _logger.LogError(e, "Unkown exception while restoring app state."); + } } public void SaveStates() @@ -97,33 +105,48 @@ namespace FileTime.Avalonia.Services foreach (var tab in tabStates.Tabs) { - if (tab.Path == null) continue; - - IItem? pathItem = null; - foreach (var contentProvider in _contentProviders) + try { - if (contentProvider.CanHandlePath(tab.Path)) + if (tab.Path == null) continue; + + IItem? pathItem = null; + if (tab.Path.StartsWith(Constants.ContentProviderProtocol)) { - pathItem = await contentProvider.GetByPath(tab.Path, true); - if (pathItem != null) break; + var contentProviderName = tab.Path.Substring(Constants.ContentProviderProtocol.Length); + pathItem = _contentProviders.FirstOrDefault(c => c.Name == contentProviderName); } + else + { + foreach (var contentProvider in _contentProviders) + { + if (contentProvider.CanHandlePath(tab.Path)) + { + pathItem = await contentProvider.GetByPath(tab.Path, true); + if (pathItem != null) break; + } + } + } + + var container = pathItem switch + { + IContainer c => c, + IElement e => e.GetParent(), + _ => null + }; + + if (container == null) continue; + + var newTab = new Tab(); + await newTab.Init(container); + + var newTabContainer = new TabContainer(newTab, _localContentProvider, _itemNameConverterService); + await newTabContainer.Init(tab.Number); + _appState.Tabs.Add(newTabContainer); } - - var container = pathItem switch + catch (Exception e) { - IContainer c => c, - IElement e => e.GetParent(), - _ => null - }; - - if (container == null) continue; - - var newTab = new Tab(); - await newTab.Init(container); - - var newTabContainer = new TabContainer(newTab, _localContentProvider, _itemNameConverterService); - await newTabContainer.Init(tab.Number); - _appState.Tabs.Add(newTabContainer); + _logger.LogError(e, "Unkown exception while restoring tab. {0}", JsonSerializer.Serialize(tab, _jsonOptions)); + } } if (_appState.Tabs.FirstOrDefault(t => t.TabNumber == tabStates.ActiveTabNumber) is TabContainer tabContainer) @@ -133,7 +156,10 @@ namespace FileTime.Avalonia.Services return true; } - catch { } + catch (Exception e) + { + _logger.LogError(e, "Unkown exception while restoring tabs."); + } return false; } } diff --git a/src/GuiApp/FileTime.Avalonia/Startup.cs b/src/GuiApp/FileTime.Avalonia/Startup.cs index 3ff725a..43ef024 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -7,6 +7,8 @@ using FileTime.Avalonia.Services; using FileTime.Avalonia.ViewModels; using FileTime.Core.Command; using FileTime.Core.Interactions; +using FileTime.Core.Persistence; +using FileTime.Providers.Smb; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Serilog; @@ -31,6 +33,8 @@ namespace FileTime.Avalonia .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton(new PersistenceSettings(Program.AppDataRoot)) + .AddSmbServices() .AddSingleton(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs index 9d5c5b7..55b839a 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs @@ -67,12 +67,16 @@ namespace FileTime.Avalonia.ViewModels public List DisplayName => ItemNameConverterService.GetDisplayName(this); - [Obsolete($"This property is for databinding only, use {nameof(GetContainers)} method instead.")] + /*[Obsolete($"This property is for databinding only, use {nameof(GetContainers)} method instead.")] public ObservableCollection Containers { get { - if (!_isInitialized) Task.Run(Refresh).Wait(); + try + { + if (!_isInitialized) Task.Run(Refresh).Wait(); + } + catch(Exception e) { } return _containers; } set @@ -90,7 +94,11 @@ namespace FileTime.Avalonia.ViewModels { get { - if (!_isInitialized) Task.Run(Refresh).Wait(); + try + { + if (!_isInitialized) Task.Run(Refresh).Wait(); + } + catch(Exception e) { } return _elements; } set @@ -107,7 +115,11 @@ namespace FileTime.Avalonia.ViewModels { get { - if (!_isInitialized) Task.Run(Refresh).Wait(); + try + { + if (!_isInitialized) Task.Run(Refresh).Wait(); + } + catch(Exception e) { } return _items; } set @@ -118,6 +130,55 @@ namespace FileTime.Avalonia.ViewModels OnPropertyChanged(nameof(Items)); } } + }*/ + + public Task Containers => GetContainers(); + public Task Elements => GetElements(); + public Task Items => GetItems(); + + public async Task> GetContainers(CancellationToken token = default) + { + if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); + return _containers; + } + + public async Task> GetElements(CancellationToken token = default) + { + if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); + return _elements; + } + + public async Task> GetItems(CancellationToken token = default) + { + if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); + return _items; + } + + private void SetContainers(ObservableCollection value) + { + if (value != _containers) + { + _containers = value; + OnPropertyChanged(nameof(Containers)); + } + } + + private void SetElements(ObservableCollection value) + { + if (value != _elements) + { + _elements = value; + OnPropertyChanged(nameof(Elements)); + } + } + + private void SetItems(ObservableCollection value) + { + if (value != _items) + { + _items = value; + OnPropertyChanged(nameof(Items)); + } } public ContainerViewModel(INewItemProcessor newItemProcessor, ContainerViewModel? parent, IContainer container, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService) @@ -211,14 +272,14 @@ namespace FileTime.Avalonia.ViewModels } else { - Containers = new ObservableCollection(newContainers); - Elements = new ObservableCollection(newElements); - Items = new ObservableCollection(newContainers.Cast().Concat(newElements)); + SetContainers(new ObservableCollection(newContainers)); + SetElements(new ObservableCollection(newElements)); + SetItems(new ObservableCollection(newContainers.Cast().Concat(newElements))); } - for (var i = 0; i < Items.Count; i++) + for (var i = 0; i < _items.Count; i++) { - Items[i].IsAlternative = i % 2 == 1; + _items[i].IsAlternative = i % 2 == 1; } } catch (Exception e) @@ -293,7 +354,7 @@ namespace FileTime.Avalonia.ViewModels } } - if(unloadEvents) + if (unloadEvents) { Container.Refreshed.Remove(Container_Refreshed); } @@ -303,24 +364,6 @@ namespace FileTime.Avalonia.ViewModels _items.Clear(); } - public async Task> GetContainers(CancellationToken token = default) - { - if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); - return _containers; - } - - public async Task> GetElements(CancellationToken token = default) - { - if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); - return _elements; - } - - public async Task> GetItems(CancellationToken token = default) - { - if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); - return _items; - } - private void Dispose() { if (!_disposed) diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index 8d30dda..ba88146 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -34,7 +34,6 @@ - @@ -222,9 +221,10 @@ + Items="{Binding AppState.SelectedTab.Parent.Items^}"> @@ -244,8 +244,9 @@ + IsVisible="{Binding AppState.SelectedTab.CurrentLocation.Items^.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> Empty @@ -282,8 +284,9 @@ Classes="ContentListView" IsEnabled="False" x:Name="ChildItems" - Items="{Binding AppState.SelectedTab.ChildContainer.Items}" - IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"> + x:CompileBindings="False" + Items="{Binding AppState.SelectedTab.ChildContainer.Items^}" + IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items^.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"> @@ -291,7 +294,9 @@ - + enable - + - + + \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/Persistence/PersistenceService.cs b/src/Providers/FileTime.Providers.Smb/Persistence/PersistenceService.cs new file mode 100644 index 0000000..9745d5e --- /dev/null +++ b/src/Providers/FileTime.Providers.Smb/Persistence/PersistenceService.cs @@ -0,0 +1,133 @@ +using System.Text.Json; +using FileTime.Core.Persistence; +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; + +namespace FileTime.Providers.Smb.Persistence +{ + public class PersistenceService + { + private const string smbFolderName = "smb"; + private const string serverFileName = "servers.json"; + private readonly PersistenceSettings _persistenceSettings; + private readonly JsonSerializerOptions _jsonOptions; + private static readonly byte[] _encryptionKey = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 + }; + private readonly ILogger _logger; + + public PersistenceService(PersistenceSettings persistenceSettings, ILogger logger) + { + _persistenceSettings = persistenceSettings; + _logger = logger; + + _jsonOptions = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + } + public async Task SaveServers(IEnumerable servers) + { + ServersPersistenceRoot root; + string? encodedIV = null; + + using (Aes aes = Aes.Create()) + { + aes.Key = _encryptionKey; + + encodedIV = Convert.ToBase64String(aes.IV); + + root = new ServersPersistenceRoot() + { + Key = encodedIV, + Servers = servers.Select(s => SaveServer(s, aes)).ToList() + }; + } + + var smbDirectory = new DirectoryInfo(Path.Combine(_persistenceSettings.RootAppDataPath, smbFolderName)); + if (!smbDirectory.Exists) smbDirectory.Create(); + + var serversPath = Path.Combine(_persistenceSettings.RootAppDataPath, smbFolderName, serverFileName); + + using var stream = File.Create(serversPath); + await JsonSerializer.SerializeAsync(stream, root, _jsonOptions); + } + + public async Task> LoadServers() + { + var serverFilePath = Path.Combine(_persistenceSettings.RootAppDataPath, smbFolderName, serverFileName); + var servers = new List(); + + if (!new FileInfo(serverFilePath).Exists) return servers; + + using var stream = File.OpenRead(serverFilePath); + var serversRoot = await JsonSerializer.DeserializeAsync(stream); + + if (serversRoot == null) return servers; + + if (!string.IsNullOrEmpty(serversRoot.Key)) + { + var iv = Convert.FromBase64String(serversRoot.Key); + + using Aes aes = Aes.Create(); + foreach (var server in serversRoot.Servers) + { + try + { + if (string.IsNullOrEmpty(server.Password)) continue; + + using var memoryStream = new MemoryStream(); + memoryStream.Write(Convert.FromBase64String(server.Password)); + memoryStream.Position = 0; + + using CryptoStream cryptoStream = new( + memoryStream, + aes.CreateDecryptor(_encryptionKey, iv), + CryptoStreamMode.Read); + using StreamReader decryptReader = new(cryptoStream); + server.Password = await decryptReader.ReadToEndAsync(); + } + catch(Exception e) + { + _logger.LogError(e, "Unkown error while decrypting password for {0}", server.Name); + } + } + } + + servers.AddRange(serversRoot.Servers); + + return servers; + } + + private static SmbServer SaveServer(Smb.SmbServer server, Aes aes) + { + var encryptedPassword = ""; + using (var memoryStream = new MemoryStream()) + { + using CryptoStream cryptoStream = new( + memoryStream, + aes.CreateEncryptor(), + CryptoStreamMode.Write); + using StreamWriter encryptWriter = new(cryptoStream); + { + encryptWriter.Write(server.Password); + encryptWriter.Flush(); + cryptoStream.FlushFinalBlock(); + } + + var a = memoryStream.ToArray(); + encryptedPassword = Convert.ToBase64String(a); + } + + return new SmbServer() + { + Path = server.Name, + Name = server.Name, + UserName = server.Username, + Password = encryptedPassword + }; + } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/Persistence/ServersPersistenceRoot.cs b/src/Providers/FileTime.Providers.Smb/Persistence/ServersPersistenceRoot.cs new file mode 100644 index 0000000..fdc1bc0 --- /dev/null +++ b/src/Providers/FileTime.Providers.Smb/Persistence/ServersPersistenceRoot.cs @@ -0,0 +1,8 @@ +namespace FileTime.Providers.Smb.Persistence +{ + public class ServersPersistenceRoot + { + public string Key { get; set; } + public List Servers { get; set; } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/Persistence/SmbServer.cs b/src/Providers/FileTime.Providers.Smb/Persistence/SmbServer.cs new file mode 100644 index 0000000..18b1405 --- /dev/null +++ b/src/Providers/FileTime.Providers.Smb/Persistence/SmbServer.cs @@ -0,0 +1,10 @@ +namespace FileTime.Providers.Smb.Persistence +{ + public class SmbServer + { + public string Path { get; set; } + public string Name { get; set; } + public string? UserName { get; set; } + public string? Password { get; set; } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs b/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs index d808f85..26f2fa5 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs @@ -7,7 +7,7 @@ namespace FileTime.Providers.Smb private readonly Func> _getSmbClient; private readonly Action _disposeClient; private bool _isRunning; - private readonly object _lock = new object(); + private readonly object _lock = new(); public SmbClientContext(Func> getSmbClient, Action disposeClient) { diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs index a23e619..f99da8a 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs @@ -3,17 +3,23 @@ using AsyncEvent; using FileTime.Core.Interactions; using FileTime.Core.Models; using FileTime.Core.Providers; +using Microsoft.Extensions.Logging; namespace FileTime.Providers.Smb { public class SmbContentProvider : IContentProvider { + private readonly object _initializationGuard = new object(); + private bool _initialized; + private bool _initializing; private IContainer? _parent; private readonly IInputInterface _inputInterface; private readonly List _rootContainers; private readonly IReadOnlyList _rootContainersReadOnly; - private IReadOnlyList? _items; - private readonly IReadOnlyList? _elements = new List().AsReadOnly(); + private IReadOnlyList _items; + private readonly IReadOnlyList _elements = new List().AsReadOnly(); + private readonly Persistence.PersistenceService _persistenceService; + private readonly ILogger _logger; public string Name { get; } = "smb"; @@ -33,12 +39,14 @@ namespace FileTime.Providers.Smb public bool IsDestroyed => false; - public SmbContentProvider(IInputInterface inputInterface) + public SmbContentProvider(IInputInterface inputInterface, Persistence.PersistenceService persistenceService, ILogger logger) { _rootContainers = new List(); _items = new List(); _rootContainersReadOnly = _rootContainers.AsReadOnly(); _inputInterface = inputInterface; + _persistenceService = persistenceService; + _logger = logger; } public async Task CreateContainer(string name) @@ -55,6 +63,8 @@ namespace FileTime.Providers.Smb await RefreshAsync(); + await SaveServers(); + return container; } @@ -74,7 +84,7 @@ namespace FileTime.Providers.Smb var pathParts = path.TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar); - var rootContainer = _rootContainers.Find(c => c.Name == pathParts[0]); + var rootContainer = (await GetContainers())?.FirstOrDefault(c => c.Name == pathParts[0]); if (rootContainer == null) { @@ -98,9 +108,19 @@ namespace FileTime.Providers.Smb public void SetParent(IContainer container) => _parent = container; public Task> GetRootContainers(CancellationToken token = default) => Task.FromResult(_rootContainersReadOnly); - public Task?> GetItems(CancellationToken token = default) => Task.FromResult(_items); - public Task?> GetContainers(CancellationToken token = default) => Task.FromResult((IReadOnlyList?)_rootContainersReadOnly); - public Task?> GetElements(CancellationToken token = default) => Task.FromResult(_elements); + public async Task?> GetItems(CancellationToken token = default) + { + await Init(); + return _items; + } + + public async Task?> GetContainers(CancellationToken token = default) + { + await Init(); + return _rootContainersReadOnly; + } + + public Task?> GetElements(CancellationToken token = default) => Task.FromResult((IReadOnlyList?)_elements); public Task Rename(string newName) => throw new NotSupportedException(); public Task CanOpen() => Task.FromResult(true); @@ -108,5 +128,54 @@ namespace FileTime.Providers.Smb public void Destroy() { } public void Unload() { } + + public async Task SaveServers() + { + try + { + await _persistenceService.SaveServers(_rootContainers.OfType()); + } + catch (Exception e) + { + _logger.LogError(e, "Unkown error while saving smb server states."); + } + } + + private async Task Init() + { + while (true) + { + lock (_initializationGuard) + { + if (!_initializing) + { + _initializing = true; + break; + } + } + await Task.Delay(1); + } + try + { + if (_initialized) return; + if (_items.Count > 0) return; + _initialized = true; + + var servers = await _persistenceService.LoadServers(); + foreach (var server in servers) + { + var smbServer = new SmbServer(server.Path, this, _inputInterface, server.UserName, server.Password); + _rootContainers.Add(smbServer); + } + _items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly(); + } + finally + { + lock (_initializationGuard) + { + _initializing = false; + } + } + } } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbFolder.cs b/src/Providers/FileTime.Providers.Smb/SmbFolder.cs index c820f00..f61ce72 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbFolder.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbFolder.cs @@ -55,25 +55,6 @@ namespace FileTime.Providers.Smb public Task Clone() => Task.FromResult((IContainer)this); - public async Task GetByPath(string path, bool acceptDeepestMatch = false) - { - var paths = path.Split(Constants.SeparatorChar); - - var item = (await GetItems())?.FirstOrDefault(i => i.Name == paths[0]); - - if (paths.Length == 1) - { - return item; - } - - if (item is IContainer container) - { - return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch); - } - - return null; - } - public IContainer? GetParent() => _parent; public Task IsExists(string name) diff --git a/src/Providers/FileTime.Providers.Smb/SmbServer.cs b/src/Providers/FileTime.Providers.Smb/SmbServer.cs index 87b45b3..692f323 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbServer.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbServer.cs @@ -10,17 +10,18 @@ namespace FileTime.Providers.Smb { public class SmbServer : IContainer { - private string? _username; - private string? _password; + private bool _reenterCredentials; private IReadOnlyList? _shares; private IReadOnlyList? _items; private readonly IReadOnlyList? _elements = new List().AsReadOnly(); private ISMBClient? _client; - private readonly object _clientGuard = new object(); + private readonly object _clientGuard = new(); private bool _refreshingClient; private readonly IInputInterface _inputInterface; private readonly SmbClientContext _smbClientContext; + public string? Username { get; private set; } + public string? Password { get; private set; } public string Name { get; } @@ -42,10 +43,12 @@ namespace FileTime.Providers.Smb public bool IsDestroyed => false; - public SmbServer(string path, SmbContentProvider contentProvider, IInputInterface inputInterface) + public SmbServer(string path, SmbContentProvider contentProvider, IInputInterface inputInterface, string? username = null, string? password = null) { _inputInterface = inputInterface; _smbClientContext = new SmbClientContext(GetSmbClient, DisposeSmbClient); + Username = username; + Password = password; Provider = contentProvider; FullName = Name = path; @@ -53,12 +56,12 @@ namespace FileTime.Providers.Smb public async Task?> GetItems(CancellationToken token = default) { - if (_shares == null) await RefreshAsync(); + if (_shares == null) await RefreshAsync(token); return _shares; } public async Task?> GetContainers(CancellationToken token = default) { - if (_shares == null) await RefreshAsync(); + if (_shares == null) await RefreshAsync(token); return _shares; } public Task?> GetElements(CancellationToken token = default) @@ -81,9 +84,24 @@ namespace FileTime.Providers.Smb return Task.CompletedTask; } - public Task GetByPath(string path, bool acceptDeepestMatch = false) + public async Task GetByPath(string path, bool acceptDeepestMatch = false) { - throw new NotImplementedException(); + var paths = path.Split(Constants.SeparatorChar); + + var item = (await GetItems())!.FirstOrDefault(i => i.Name == paths[0]); + + if (paths.Length == 1) + { + return item; + } + + if (item is IContainer container) + { + var result = await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch); + return result == null && acceptDeepestMatch ? this : result; + } + + return null; } public IContainer? GetParent() => Provider; @@ -152,30 +170,32 @@ namespace FileTime.Providers.Smb if (connected) { - if (_username == null && _password == null) + if (_reenterCredentials || Username == null || Password == null) { var inputs = await _inputInterface.ReadInputs( new InputElement[] { - new InputElement($"Username for '{Name}'", InputType.Text), - new InputElement($"Password for '{Name}'", InputType.Password) + new InputElement($"Username for '{Name}'", InputType.Text, Username ?? ""), + new InputElement($"Password for '{Name}'", InputType.Password, Password ?? "") }); - _username = inputs[0]; - _password = inputs[1]; + Username = inputs[0]; + Password = inputs[1]; } - if (client.Login(string.Empty, _username, _password) != NTStatus.STATUS_SUCCESS) + if (client.Login(string.Empty, Username, Password) != NTStatus.STATUS_SUCCESS) { - _username = null; - _password = null; + _reenterCredentials = true; } else { + _reenterCredentials = false; lock (_clientGuard) { _client = client; } + + await Provider.SaveServers(); } } } diff --git a/src/Providers/FileTime.Providers.Smb/SmbShare.cs b/src/Providers/FileTime.Providers.Smb/SmbShare.cs index 0cd65b3..a1e9fdb 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbShare.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbShare.cs @@ -11,7 +11,7 @@ namespace FileTime.Providers.Smb private IReadOnlyList? _items; private IReadOnlyList? _containers; private IReadOnlyList? _elements; - private SmbClientContext _smbClientContext; + private readonly SmbClientContext _smbClientContext; private readonly IContainer? _parent; public string Name { get; } @@ -45,17 +45,17 @@ namespace FileTime.Providers.Smb public async Task?> GetItems(CancellationToken token = default) { - if (_items == null) await RefreshAsync(); + if (_items == null) await RefreshAsync(token); return _items; } public async Task?> GetContainers(CancellationToken token = default) { - if (_containers == null) await RefreshAsync(); + if (_containers == null) await RefreshAsync(token); return _containers; } public async Task?> GetElements(CancellationToken token = default) { - if (_elements == null) await RefreshAsync(); + if (_elements == null) await RefreshAsync(token); return _elements; } @@ -74,25 +74,6 @@ namespace FileTime.Providers.Smb throw new NotImplementedException(); } - public async Task GetByPath(string path, bool acceptDeepestMatch = false) - { - var paths = path.Split(Constants.SeparatorChar); - - var item = (await GetItems())?.FirstOrDefault(i => i.Name == paths[0]); - - if (paths.Length == 1) - { - return item; - } - - if (item is IContainer container) - { - return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch); - } - - return null; - } - public IContainer? GetParent() => _parent; public Task Clone() => Task.FromResult((IContainer)this); diff --git a/src/Providers/FileTime.Providers.Smb/Startup.cs b/src/Providers/FileTime.Providers.Smb/Startup.cs new file mode 100644 index 0000000..04a9629 --- /dev/null +++ b/src/Providers/FileTime.Providers.Smb/Startup.cs @@ -0,0 +1,14 @@ +using FileTime.Providers.Smb.Persistence; +using Microsoft.Extensions.DependencyInjection; + +namespace FileTime.Providers.Smb +{ + public static class Startup + { + public static IServiceCollection AddSmbServices(this IServiceCollection serviceCollection) + { + return serviceCollection + .AddSingleton(); + } + } +} \ No newline at end of file