Smb save servers, auth

This commit is contained in:
2022-02-10 11:24:04 +01:00
parent 70180b9f12
commit ec03067af1
19 changed files with 463 additions and 132 deletions

View File

@@ -3,5 +3,6 @@ namespace FileTime.Core.Models
public static class Constants public static class Constants
{ {
public const char SeparatorChar = '/'; public const char SeparatorChar = '/';
public const string ContentProviderProtocol = "ctp://";
} }
} }

View File

@@ -10,7 +10,26 @@ namespace FileTime.Core.Models
Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default); Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default);
Task RefreshAsync(CancellationToken token = default); Task RefreshAsync(CancellationToken token = default);
Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false); async Task<IItem?> 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<IContainer> CreateContainer(string name); Task<IContainer> CreateContainer(string name);
Task<IElement> CreateElement(string name); Task<IElement> CreateElement(string name);

View File

@@ -0,0 +1,12 @@
namespace FileTime.Core.Persistence
{
public class PersistenceSettings
{
public PersistenceSettings(string rootAppDataPath)
{
RootAppDataPath = rootAppDataPath;
}
public string RootAppDataPath { get; }
}
}

View File

@@ -1,4 +1,6 @@
using FileTime.Avalonia.Application; using FileTime.Avalonia.Application;
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Avalonia.Models.Persistence namespace FileTime.Avalonia.Models.Persistence
{ {
@@ -11,7 +13,8 @@ namespace FileTime.Avalonia.Models.Persistence
public TabState(TabContainer tab) 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; Number = tab.TabNumber;
} }
} }

View File

@@ -12,6 +12,7 @@ using FileTime.Core.Components;
using FileTime.Core.Providers; using FileTime.Core.Providers;
using FileTime.Providers.Local; using FileTime.Providers.Local;
using FileTime.Core.Models; using FileTime.Core.Models;
using Microsoft.Extensions.Logging;
namespace FileTime.Avalonia.Services namespace FileTime.Avalonia.Services
{ {
@@ -23,22 +24,26 @@ namespace FileTime.Avalonia.Services
private readonly string _settingsPath; private readonly string _settingsPath;
private readonly IEnumerable<IContentProvider> _contentProviders; private readonly IEnumerable<IContentProvider> _contentProviders;
private readonly LocalContentProvider _localContentProvider; private readonly LocalContentProvider _localContentProvider;
private readonly ILogger<StatePersistenceService> _logger;
public StatePersistenceService( public StatePersistenceService(
AppState appState, AppState appState,
ItemNameConverterService itemNameConverterService, ItemNameConverterService itemNameConverterService,
IEnumerable<IContentProvider> contentProviders, IEnumerable<IContentProvider> contentProviders,
LocalContentProvider localContentProvider) LocalContentProvider localContentProvider,
ILogger<StatePersistenceService> logger)
{ {
_appState = appState; _appState = appState;
_itemNameConverterService = itemNameConverterService; _itemNameConverterService = itemNameConverterService;
_contentProviders = contentProviders; _contentProviders = contentProviders;
_localContentProvider = localContentProvider; _localContentProvider = localContentProvider;
_logger = logger;
_settingsPath = Path.Combine(Program.AppDataRoot, "savedState.json"); _settingsPath = Path.Combine(Program.AppDataRoot, "savedState.json");
_jsonOptions = new JsonSerializerOptions() _jsonOptions = new JsonSerializerOptions()
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true,
WriteIndented = true
}; };
} }
@@ -55,7 +60,10 @@ namespace FileTime.Avalonia.Services
await RestoreTabs(state.TabStates); await RestoreTabs(state.TabStates);
} }
} }
catch { } catch (Exception e)
{
_logger.LogError(e, "Unkown exception while restoring app state.");
}
} }
public void SaveStates() public void SaveStates()
@@ -97,33 +105,48 @@ namespace FileTime.Avalonia.Services
foreach (var tab in tabStates.Tabs) foreach (var tab in tabStates.Tabs)
{ {
if (tab.Path == null) continue; try
IItem? pathItem = null;
foreach (var contentProvider in _contentProviders)
{ {
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); var contentProviderName = tab.Path.Substring(Constants.ContentProviderProtocol.Length);
if (pathItem != null) break; 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);
} }
catch (Exception e)
var container = pathItem switch
{ {
IContainer c => c, _logger.LogError(e, "Unkown exception while restoring tab. {0}", JsonSerializer.Serialize(tab, _jsonOptions));
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);
} }
if (_appState.Tabs.FirstOrDefault(t => t.TabNumber == tabStates.ActiveTabNumber) is TabContainer tabContainer) if (_appState.Tabs.FirstOrDefault(t => t.TabNumber == tabStates.ActiveTabNumber) is TabContainer tabContainer)
@@ -133,7 +156,10 @@ namespace FileTime.Avalonia.Services
return true; return true;
} }
catch { } catch (Exception e)
{
_logger.LogError(e, "Unkown exception while restoring tabs.");
}
return false; return false;
} }
} }

View File

@@ -7,6 +7,8 @@ using FileTime.Avalonia.Services;
using FileTime.Avalonia.ViewModels; using FileTime.Avalonia.ViewModels;
using FileTime.Core.Command; using FileTime.Core.Command;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
using FileTime.Core.Persistence;
using FileTime.Providers.Smb;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
@@ -31,6 +33,8 @@ namespace FileTime.Avalonia
.AddSingleton<KeyboardConfigurationService>() .AddSingleton<KeyboardConfigurationService>()
.AddSingleton<KeyInputHandlerService>() .AddSingleton<KeyInputHandlerService>()
.AddSingleton<DialogService>() .AddSingleton<DialogService>()
.AddSingleton(new PersistenceSettings(Program.AppDataRoot))
.AddSmbServices()
.AddSingleton<IIconProvider, MaterialIconProvider>(); .AddSingleton<IIconProvider, MaterialIconProvider>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

View File

@@ -67,12 +67,16 @@ namespace FileTime.Avalonia.ViewModels
public List<ItemNamePart> DisplayName => ItemNameConverterService.GetDisplayName(this); public List<ItemNamePart> 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<ContainerViewModel> Containers public ObservableCollection<ContainerViewModel> Containers
{ {
get get
{ {
if (!_isInitialized) Task.Run(Refresh).Wait(); try
{
if (!_isInitialized) Task.Run(Refresh).Wait();
}
catch(Exception e) { }
return _containers; return _containers;
} }
set set
@@ -90,7 +94,11 @@ namespace FileTime.Avalonia.ViewModels
{ {
get get
{ {
if (!_isInitialized) Task.Run(Refresh).Wait(); try
{
if (!_isInitialized) Task.Run(Refresh).Wait();
}
catch(Exception e) { }
return _elements; return _elements;
} }
set set
@@ -107,7 +115,11 @@ namespace FileTime.Avalonia.ViewModels
{ {
get get
{ {
if (!_isInitialized) Task.Run(Refresh).Wait(); try
{
if (!_isInitialized) Task.Run(Refresh).Wait();
}
catch(Exception e) { }
return _items; return _items;
} }
set set
@@ -118,6 +130,55 @@ namespace FileTime.Avalonia.ViewModels
OnPropertyChanged(nameof(Items)); OnPropertyChanged(nameof(Items));
} }
} }
}*/
public Task Containers => GetContainers();
public Task Elements => GetElements();
public Task Items => GetItems();
public async Task<ObservableCollection<ContainerViewModel>> GetContainers(CancellationToken token = default)
{
if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token);
return _containers;
}
public async Task<ObservableCollection<ElementViewModel>> GetElements(CancellationToken token = default)
{
if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token);
return _elements;
}
public async Task<ObservableCollection<IItemViewModel>> GetItems(CancellationToken token = default)
{
if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token);
return _items;
}
private void SetContainers(ObservableCollection<ContainerViewModel> value)
{
if (value != _containers)
{
_containers = value;
OnPropertyChanged(nameof(Containers));
}
}
private void SetElements(ObservableCollection<ElementViewModel> value)
{
if (value != _elements)
{
_elements = value;
OnPropertyChanged(nameof(Elements));
}
}
private void SetItems(ObservableCollection<IItemViewModel> value)
{
if (value != _items)
{
_items = value;
OnPropertyChanged(nameof(Items));
}
} }
public ContainerViewModel(INewItemProcessor newItemProcessor, ContainerViewModel? parent, IContainer container, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService) public ContainerViewModel(INewItemProcessor newItemProcessor, ContainerViewModel? parent, IContainer container, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService)
@@ -211,14 +272,14 @@ namespace FileTime.Avalonia.ViewModels
} }
else else
{ {
Containers = new ObservableCollection<ContainerViewModel>(newContainers); SetContainers(new ObservableCollection<ContainerViewModel>(newContainers));
Elements = new ObservableCollection<ElementViewModel>(newElements); SetElements(new ObservableCollection<ElementViewModel>(newElements));
Items = new ObservableCollection<IItemViewModel>(newContainers.Cast<IItemViewModel>().Concat(newElements)); SetItems(new ObservableCollection<IItemViewModel>(newContainers.Cast<IItemViewModel>().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) catch (Exception e)
@@ -293,7 +354,7 @@ namespace FileTime.Avalonia.ViewModels
} }
} }
if(unloadEvents) if (unloadEvents)
{ {
Container.Refreshed.Remove(Container_Refreshed); Container.Refreshed.Remove(Container_Refreshed);
} }
@@ -303,24 +364,6 @@ namespace FileTime.Avalonia.ViewModels
_items.Clear(); _items.Clear();
} }
public async Task<ObservableCollection<ContainerViewModel>> GetContainers(CancellationToken token = default)
{
if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token);
return _containers;
}
public async Task<ObservableCollection<ElementViewModel>> GetElements(CancellationToken token = default)
{
if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token);
return _elements;
}
public async Task<ObservableCollection<IItemViewModel>> GetItems(CancellationToken token = default)
{
if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token);
return _items;
}
private void Dispose() private void Dispose()
{ {
if (!_disposed) if (!_disposed)

View File

@@ -34,7 +34,6 @@
<Rectangle Fill="#01000000"/> <Rectangle Fill="#01000000"/>
<StackPanel Margin="20,10" Orientation="Horizontal"> <StackPanel Margin="20,10" Orientation="Horizontal">
<!--TextBlock Text="{Binding AppState.SelectedTab.TabNumber,StringFormat=({0})}" /-->
<local:PathPresenter DataContext="{Binding AppState.SelectedTab.CurrentLocation.Container.FullName}"/> <local:PathPresenter DataContext="{Binding AppState.SelectedTab.CurrentLocation.Container.FullName}"/>
<TextBlock <TextBlock
Text="{Binding AppState.SelectedTab.SelectedItem.Item.Name}" Foreground="{StaticResource AccentForegroundBrush}" /> Text="{Binding AppState.SelectedTab.SelectedItem.Item.Name}" Foreground="{StaticResource AccentForegroundBrush}" />
@@ -222,9 +221,10 @@
<Grid> <Grid>
<ListBox <ListBox
x:CompileBindings="False"
Classes="ContentListView" Classes="ContentListView"
IsEnabled="False" IsEnabled="False"
Items="{Binding AppState.SelectedTab.Parent.Items}"> Items="{Binding AppState.SelectedTab.Parent.Items^}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<local:ItemView ShowAttributes="False"/> <local:ItemView ShowAttributes="False"/>
@@ -244,8 +244,9 @@
<Grid Grid.Column="2"> <Grid Grid.Column="2">
<ListBox <ListBox
x:Name="CurrentItems" x:Name="CurrentItems"
x:CompileBindings="False"
IsTabStop="True" IsTabStop="True"
Items="{Binding AppState.SelectedTab.CurrentLocation.Items}" Items="{Binding AppState.SelectedTab.CurrentLocation.Items^}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Hidden"
SelectedItem="{Binding AppState.SelectedTab.SelectedItem, Mode=TwoWay}" SelectedItem="{Binding AppState.SelectedTab.SelectedItem, Mode=TwoWay}"
@@ -258,12 +259,13 @@
</ListBox> </ListBox>
<TextBlock <TextBlock
x:CompileBindings="False"
x:Name="CurrentEmpty" x:Name="CurrentEmpty"
Margin="10" Margin="10"
HorizontalAlignment="Center" HorizontalAlignment="Center"
FontWeight="Bold" FontWeight="Bold"
Foreground="{DynamicResource ErrorBrush}" Foreground="{DynamicResource ErrorBrush}"
IsVisible="{Binding AppState.SelectedTab.CurrentLocation.Items.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> IsVisible="{Binding AppState.SelectedTab.CurrentLocation.Items^.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}">
Empty Empty
</TextBlock> </TextBlock>
</Grid> </Grid>
@@ -282,8 +284,9 @@
Classes="ContentListView" Classes="ContentListView"
IsEnabled="False" IsEnabled="False"
x:Name="ChildItems" x:Name="ChildItems"
Items="{Binding AppState.SelectedTab.ChildContainer.Items}" x:CompileBindings="False"
IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"> Items="{Binding AppState.SelectedTab.ChildContainer.Items^}"
IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items^.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<local:ItemView/> <local:ItemView/>
@@ -291,7 +294,9 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<Grid IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> <Grid
x:CompileBindings="False"
IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items^.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}">
<TextBlock <TextBlock
x:Name="ChildEmpty" x:Name="ChildEmpty"

View File

@@ -124,7 +124,8 @@ namespace FileTime.Providers.Local
if (item is IContainer container) if (item is IContainer container)
{ {
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch); var result = await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch);
return result == null && acceptDeepestMatch ? this : result;
} }
return null; return null;

View File

@@ -5,9 +5,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core\FileTime.Core.csproj"/> <ProjectReference Include="..\..\Core\FileTime.Core\FileTime.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SMBLibrary" Version="1.4.8"/> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="SMBLibrary" Version="1.4.8" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -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<PersistenceService> _logger;
public PersistenceService(PersistenceSettings persistenceSettings, ILogger<PersistenceService> logger)
{
_persistenceSettings = persistenceSettings;
_logger = logger;
_jsonOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
}
public async Task SaveServers(IEnumerable<Smb.SmbServer> 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<List<SmbServer>> LoadServers()
{
var serverFilePath = Path.Combine(_persistenceSettings.RootAppDataPath, smbFolderName, serverFileName);
var servers = new List<SmbServer>();
if (!new FileInfo(serverFilePath).Exists) return servers;
using var stream = File.OpenRead(serverFilePath);
var serversRoot = await JsonSerializer.DeserializeAsync<ServersPersistenceRoot>(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
};
}
}
}

View File

@@ -0,0 +1,8 @@
namespace FileTime.Providers.Smb.Persistence
{
public class ServersPersistenceRoot
{
public string Key { get; set; }
public List<SmbServer> Servers { get; set; }
}
}

View File

@@ -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; }
}
}

View File

@@ -7,7 +7,7 @@ namespace FileTime.Providers.Smb
private readonly Func<Task<ISMBClient>> _getSmbClient; private readonly Func<Task<ISMBClient>> _getSmbClient;
private readonly Action _disposeClient; private readonly Action _disposeClient;
private bool _isRunning; private bool _isRunning;
private readonly object _lock = new object(); private readonly object _lock = new();
public SmbClientContext(Func<Task<ISMBClient>> getSmbClient, Action disposeClient) public SmbClientContext(Func<Task<ISMBClient>> getSmbClient, Action disposeClient)
{ {

View File

@@ -3,17 +3,23 @@ using AsyncEvent;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Providers; using FileTime.Core.Providers;
using Microsoft.Extensions.Logging;
namespace FileTime.Providers.Smb namespace FileTime.Providers.Smb
{ {
public class SmbContentProvider : IContentProvider public class SmbContentProvider : IContentProvider
{ {
private readonly object _initializationGuard = new object();
private bool _initialized;
private bool _initializing;
private IContainer? _parent; private IContainer? _parent;
private readonly IInputInterface _inputInterface; private readonly IInputInterface _inputInterface;
private readonly List<IContainer> _rootContainers; private readonly List<IContainer> _rootContainers;
private readonly IReadOnlyList<IContainer> _rootContainersReadOnly; private readonly IReadOnlyList<IContainer> _rootContainersReadOnly;
private IReadOnlyList<IItem>? _items; private IReadOnlyList<IItem> _items;
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly(); private readonly IReadOnlyList<IElement> _elements = new List<IElement>().AsReadOnly();
private readonly Persistence.PersistenceService _persistenceService;
private readonly ILogger<SmbContentProvider> _logger;
public string Name { get; } = "smb"; public string Name { get; } = "smb";
@@ -33,12 +39,14 @@ namespace FileTime.Providers.Smb
public bool IsDestroyed => false; public bool IsDestroyed => false;
public SmbContentProvider(IInputInterface inputInterface) public SmbContentProvider(IInputInterface inputInterface, Persistence.PersistenceService persistenceService, ILogger<SmbContentProvider> logger)
{ {
_rootContainers = new List<IContainer>(); _rootContainers = new List<IContainer>();
_items = new List<IItem>(); _items = new List<IItem>();
_rootContainersReadOnly = _rootContainers.AsReadOnly(); _rootContainersReadOnly = _rootContainers.AsReadOnly();
_inputInterface = inputInterface; _inputInterface = inputInterface;
_persistenceService = persistenceService;
_logger = logger;
} }
public async Task<IContainer> CreateContainer(string name) public async Task<IContainer> CreateContainer(string name)
@@ -55,6 +63,8 @@ namespace FileTime.Providers.Smb
await RefreshAsync(); await RefreshAsync();
await SaveServers();
return container; return container;
} }
@@ -74,7 +84,7 @@ namespace FileTime.Providers.Smb
var pathParts = path.TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar); 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) if (rootContainer == null)
{ {
@@ -98,9 +108,19 @@ namespace FileTime.Providers.Smb
public void SetParent(IContainer container) => _parent = container; public void SetParent(IContainer container) => _parent = container;
public Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default) => Task.FromResult(_rootContainersReadOnly); public Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default) => Task.FromResult(_rootContainersReadOnly);
public Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default) => Task.FromResult(_items); public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
public Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default) => Task.FromResult((IReadOnlyList<IContainer>?)_rootContainersReadOnly); {
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) => Task.FromResult(_elements); await Init();
return _items;
}
public async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
{
await Init();
return _rootContainersReadOnly;
}
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) => Task.FromResult((IReadOnlyList<IElement>?)_elements);
public Task Rename(string newName) => throw new NotSupportedException(); public Task Rename(string newName) => throw new NotSupportedException();
public Task<bool> CanOpen() => Task.FromResult(true); public Task<bool> CanOpen() => Task.FromResult(true);
@@ -108,5 +128,54 @@ namespace FileTime.Providers.Smb
public void Destroy() { } public void Destroy() { }
public void Unload() { } public void Unload() { }
public async Task SaveServers()
{
try
{
await _persistenceService.SaveServers(_rootContainers.OfType<SmbServer>());
}
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;
}
}
}
} }
} }

View File

@@ -55,25 +55,6 @@ namespace FileTime.Providers.Smb
public Task<IContainer> Clone() => Task.FromResult((IContainer)this); public Task<IContainer> Clone() => Task.FromResult((IContainer)this);
public async Task<IItem?> 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 IContainer? GetParent() => _parent;
public Task<bool> IsExists(string name) public Task<bool> IsExists(string name)

View File

@@ -10,17 +10,18 @@ namespace FileTime.Providers.Smb
{ {
public class SmbServer : IContainer public class SmbServer : IContainer
{ {
private string? _username; private bool _reenterCredentials;
private string? _password;
private IReadOnlyList<IContainer>? _shares; private IReadOnlyList<IContainer>? _shares;
private IReadOnlyList<IItem>? _items; private IReadOnlyList<IItem>? _items;
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly(); private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly();
private ISMBClient? _client; private ISMBClient? _client;
private readonly object _clientGuard = new object(); private readonly object _clientGuard = new();
private bool _refreshingClient; private bool _refreshingClient;
private readonly IInputInterface _inputInterface; private readonly IInputInterface _inputInterface;
private readonly SmbClientContext _smbClientContext; private readonly SmbClientContext _smbClientContext;
public string? Username { get; private set; }
public string? Password { get; private set; }
public string Name { get; } public string Name { get; }
@@ -42,10 +43,12 @@ namespace FileTime.Providers.Smb
public bool IsDestroyed => false; 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; _inputInterface = inputInterface;
_smbClientContext = new SmbClientContext(GetSmbClient, DisposeSmbClient); _smbClientContext = new SmbClientContext(GetSmbClient, DisposeSmbClient);
Username = username;
Password = password;
Provider = contentProvider; Provider = contentProvider;
FullName = Name = path; FullName = Name = path;
@@ -53,12 +56,12 @@ namespace FileTime.Providers.Smb
public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default) public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
{ {
if (_shares == null) await RefreshAsync(); if (_shares == null) await RefreshAsync(token);
return _shares; return _shares;
} }
public async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default) public async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
{ {
if (_shares == null) await RefreshAsync(); if (_shares == null) await RefreshAsync(token);
return _shares; return _shares;
} }
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default)
@@ -81,9 +84,24 @@ namespace FileTime.Providers.Smb
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false) public async Task<IItem?> 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; public IContainer? GetParent() => Provider;
@@ -152,30 +170,32 @@ namespace FileTime.Providers.Smb
if (connected) if (connected)
{ {
if (_username == null && _password == null) if (_reenterCredentials || Username == null || Password == null)
{ {
var inputs = await _inputInterface.ReadInputs( var inputs = await _inputInterface.ReadInputs(
new InputElement[] new InputElement[]
{ {
new InputElement($"Username for '{Name}'", InputType.Text), new InputElement($"Username for '{Name}'", InputType.Text, Username ?? ""),
new InputElement($"Password for '{Name}'", InputType.Password) new InputElement($"Password for '{Name}'", InputType.Password, Password ?? "")
}); });
_username = inputs[0]; Username = inputs[0];
_password = inputs[1]; 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; _reenterCredentials = true;
_password = null;
} }
else else
{ {
_reenterCredentials = false;
lock (_clientGuard) lock (_clientGuard)
{ {
_client = client; _client = client;
} }
await Provider.SaveServers();
} }
} }
} }

View File

@@ -11,7 +11,7 @@ namespace FileTime.Providers.Smb
private IReadOnlyList<IItem>? _items; private IReadOnlyList<IItem>? _items;
private IReadOnlyList<IContainer>? _containers; private IReadOnlyList<IContainer>? _containers;
private IReadOnlyList<IElement>? _elements; private IReadOnlyList<IElement>? _elements;
private SmbClientContext _smbClientContext; private readonly SmbClientContext _smbClientContext;
private readonly IContainer? _parent; private readonly IContainer? _parent;
public string Name { get; } public string Name { get; }
@@ -45,17 +45,17 @@ namespace FileTime.Providers.Smb
public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default) public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
{ {
if (_items == null) await RefreshAsync(); if (_items == null) await RefreshAsync(token);
return _items; return _items;
} }
public async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default) public async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
{ {
if (_containers == null) await RefreshAsync(); if (_containers == null) await RefreshAsync(token);
return _containers; return _containers;
} }
public async Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) public async Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default)
{ {
if (_elements == null) await RefreshAsync(); if (_elements == null) await RefreshAsync(token);
return _elements; return _elements;
} }
@@ -74,25 +74,6 @@ namespace FileTime.Providers.Smb
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async Task<IItem?> 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 IContainer? GetParent() => _parent;
public Task<IContainer> Clone() => Task.FromResult((IContainer)this); public Task<IContainer> Clone() => Task.FromResult((IContainer)this);

View File

@@ -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<PersistenceService>();
}
}
}