Sftp, fullname+nativepath refactor

This commit is contained in:
2022-02-17 23:24:41 +01:00
parent 15dc956064
commit 5072d828e6
30 changed files with 772 additions and 51 deletions

View File

@@ -4,6 +4,7 @@ using FileTime.Core.CommandHandlers;
using FileTime.Core.Providers;
using FileTime.Core.Timeline;
using FileTime.Providers.Local;
using FileTime.Providers.Sftp;
using FileTime.Providers.Smb;
using Microsoft.Extensions.DependencyInjection;
@@ -18,11 +19,11 @@ namespace FileTime.App.Core
return serviceCollection
.AddSingleton<IClipboard, Clipboard.Clipboard>()
.AddSingleton<TopContainer>()
.AddSingleton<LocalContentProvider>()
.AddSingleton<IContentProvider, LocalContentProvider>(sp => sp.GetService<LocalContentProvider>() ?? throw new Exception($"No {nameof(LocalContentProvider)} instance found"))
.AddSingleton<IContentProvider, SmbContentProvider>()
.AddSingleton<CommandExecutor>()
.AddSingleton<TimeRunner>()
.AddLocalServices()
.AddSmbServices()
.AddSftpServices()
.RegisterCommandHandlers();
}

View File

@@ -9,6 +9,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Providers\FileTime.Providers.Sftp\FileTime.Providers.Sftp.csproj" />
<ProjectReference Include="..\..\Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj" />
<ProjectReference Include="..\FileTime.App.Core\FileTime.App.Core.csproj" />
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local\FileTime.Providers.Local.csproj" />

View File

@@ -61,7 +61,6 @@ namespace FileTime.Core.Components
{
if (_currentlySelecting) return false;
if (_currentSelectedItem == value) return false;
IItem? itemToSelect = null;
if (value != null)
{

View File

@@ -12,6 +12,7 @@ namespace FileTime.Core.Models
Task RefreshAsync(CancellationToken token = default);
async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
if (path == null) return this;
var paths = path.Split(Constants.SeparatorChar);
var item = (await GetItems())?.FirstOrDefault(i => i.Name == paths[0]);

View File

@@ -0,0 +1,134 @@
using AsyncEvent;
using FileTime.Core.Models;
namespace FileTime.Core.Providers
{
public abstract class AbstractContainer<TProvider> : IContainer where TProvider : IContentProvider
{
private readonly IContainer _parent;
private readonly List<Exception> _exceptions = new();
private IReadOnlyList<IContainer>? _containers;
private IReadOnlyList<IItem>? _items;
private IReadOnlyList<IElement>? _elements;
public IReadOnlyList<Exception> Exceptions { get; }
public bool IsLoaded { get; protected set; }
public bool SupportsDirectoryLevelSoftDelete { get; protected set; }
public AsyncEventHandler Refreshed { get; protected set; } = new();
public string Name { get; protected set; }
public string? FullName { get; protected set; }
public string? NativePath { get; protected set; }
public virtual bool IsHidden { get; protected set; }
public bool IsDestroyed { get; protected set; }
public virtual SupportsDelete CanDelete { get; protected set; }
public virtual bool CanRename { get; protected set; }
public TProvider Provider { get; }
IContentProvider IItem.Provider => Provider;
protected AbstractContainer(TProvider provider, IContainer parent, string name)
{
_parent = parent;
Provider = provider;
Name = name;
FullName = (parent?.FullName ?? Name) + Constants.SeparatorChar + Name;
Exceptions = _exceptions.AsReadOnly();
}
public abstract Task<bool> CanOpenAsync();
public abstract Task<IContainer> CloneAsync();
public abstract Task<IContainer> CreateContainerAsync(string name);
public abstract Task<IElement> CreateElementAsync(string name);
public abstract Task Delete(bool hardDelete = false);
public virtual void Destroy()
{
_items = null;
_containers = null;
_elements = null;
IsDestroyed = true;
Refreshed = new AsyncEventHandler();
}
public virtual async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
{
if (_containers == null) await RefreshAsync(token);
return _containers;
}
public virtual async Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default)
{
if (_elements == null) await RefreshAsync(token);
return _elements;
}
public virtual async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
{
if (_items == null) await RefreshAsync(token);
return _items;
}
public virtual IContainer? GetParent() => _parent;
public virtual async Task<bool> IsExistsAsync(string name)
{
var items = await GetItems();
return items?.Any(i => i.Name == name) ?? false;
}
public virtual async Task RefreshAsync(CancellationToken token = default)
{
var containers = new List<IContainer>();
var elements = new List<IElement>();
foreach (var item in await RefreshItems(token))
{
if (item is IContainer container)
{
containers.Add(container);
}
else if (item is IElement element)
{
elements.Add(element);
}
}
if (_items != null)
{
foreach (var item in _items)
{
item.Destroy();
}
}
_containers = containers.OrderBy(c => c.Name).ToList().AsReadOnly();
_elements = elements.OrderBy(e => e.Name).ToList().AsReadOnly();
_items = _containers.Cast<IItem>().Concat(_elements).ToList().AsReadOnly();
if (Refreshed != null) await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token);
}
public abstract Task<IEnumerable<IItem>> RefreshItems(CancellationToken token = default);
public abstract Task Rename(string newName);
public virtual void Unload()
{
_items = null;
_containers = null;
_elements = null;
}
}
}

View File

@@ -5,6 +5,7 @@ namespace FileTime.Core.Providers
public interface IContentProvider : IContainer
{
bool SupportsContentStreams { get; }
string Protocol { get; }
Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default);

View File

@@ -32,6 +32,8 @@ namespace FileTime.Core.Timeline
public bool IsDestroyed => false;
public bool SupportsContentStreams => false;
public string Protocol => "time2://";
public TimeProvider(PointInTime pointInTime)
{
_pointInTime = pointInTime;

View File

@@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{0D2B4BAA
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Tools.Compression", "Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj", "{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Providers.Sftp", "Providers\FileTime.Providers.Sftp\FileTime.Providers.Sftp.csproj", "{0E650206-801D-4E8D-95BA-4565B32092E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -293,6 +295,26 @@ Global
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966}.Release|x64.Build.0 = Release|Any CPU
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966}.Release|x86.ActiveCfg = Release|Any CPU
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966}.Release|x86.Build.0 = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|ARM.ActiveCfg = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|ARM.Build.0 = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|ARM64.Build.0 = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|x64.ActiveCfg = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|x64.Build.0 = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|x86.ActiveCfg = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Debug|x86.Build.0 = Debug|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|Any CPU.Build.0 = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|ARM.ActiveCfg = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|ARM.Build.0 = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|ARM64.ActiveCfg = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|ARM64.Build.0 = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|x64.ActiveCfg = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|x64.Build.0 = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|x86.ActiveCfg = Release|Any CPU
{0E650206-801D-4E8D-95BA-4565B32092E1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -310,6 +332,7 @@ Global
{9BDAC126-200F-4056-8D35-36EC059B40F3} = {38B1B927-4201-4B7A-87EE-737B8C6D4090}
{22B33BC6-3987-4BE6-8C54-BFC75C78CCE7} = {890275FF-943A-4D07-83BA-14E5C52D7846}
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966} = {0D2B4BAA-0399-459C-B022-41DB7F408225}
{0E650206-801D-4E8D-95BA-4565B32092E1} = {517D96CE-A956-4638-A93D-465D34DE22B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8D679DCE-AC84-4A91-BFED-8F8D8E1D8183}

View File

@@ -147,6 +147,7 @@
<converters:GetFileExtensionConverter x:Key="GetFileExtensionConverter"/>
<converters:CommandToCommandNameConverter x:Key="CommandToCommandNameConverter"/>
<converters:NamePartShrinkerConverter x:Key="NamePartShrinkerConverter"/>
<converters:StringReplaceConverter x:Key="PathPreformatter" OldValue="://" NewValue="/"/>
</ResourceDictionary>
</Application.Resources>

View File

@@ -21,7 +21,6 @@ namespace FileTime.Avalonia
.AddServices()
.RegisterLogging()
.AddViewModels()
.RegisterCommandHandlers()
.BuildServiceProvider()
.InitSerilog();

View File

@@ -51,16 +51,11 @@ namespace FileTime.Avalonia.Application
{
if (!_updateFromCode && value != null)
{
/*try
{*/
/*var task = SetSelectedItemAsync(value, true);
Task.WaitAll(new Task[] { task }, 100);*/
Task.Run(async () => await SetSelectedItemAsync(value, true)).Wait();
/*}
catch
try
{
//TODO: Debug, linux start after restore 3 tabs
}*/
Task.Run(async () => await SetSelectedItemAsync(value, true)).Wait();
}
catch (AggregateException e) when (e.InnerExceptions.Count == 1 && e.InnerExceptions[0] is IndexOutOfRangeException) { }
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace FileTime.Avalonia.Converters
{
public class StringReplaceConverter : IValueConverter
{
public string? OldValue { get; set; }
public string? NewValue { get; set; }
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is string s && OldValue != null && NewValue != null ? s.Replace(OldValue, NewValue) : value;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -10,7 +10,6 @@ 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;
@@ -39,7 +38,6 @@ namespace FileTime.Avalonia
.AddSingleton(new PersistenceSettings(Program.AppDataRoot))
.AddSingleton<ProgramsService>()
.AddSingleton<ToastMessageSink>()
.AddSmbServices()
.AddSingleton<IIconProvider, MaterialIconProvider>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -53,15 +51,6 @@ namespace FileTime.Avalonia
return serviceCollection;
}
internal static IServiceCollection RegisterCommandHandlers(this IServiceCollection serviceCollection)
{
foreach (var commandHandler in Providers.Local.Startup.GetCommandHandlers())
{
serviceCollection.AddTransient(typeof(ICommandHandler), commandHandler);
}
return serviceCollection;
}
internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection)
{

View File

@@ -35,7 +35,7 @@
<Rectangle Fill="#01000000"/>
<StackPanel Margin="20,10" Orientation="Horizontal">
<local:PathPresenter DataContext="{Binding AppState.SelectedTab.CurrentLocation.Container.FullName}"/>
<local:PathPresenter DataContext="{Binding AppState.SelectedTab.CurrentLocation.Container.FullName,Converter={StaticResource PathPreformatter}}"/>
<TextBlock
Text="{Binding AppState.SelectedTab.SelectedItem.Item.Name}" Foreground="{StaticResource AccentBrush}" />
</StackPanel>

View File

@@ -5,6 +5,7 @@
<ItemGroup>
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>

View File

@@ -18,6 +18,7 @@ namespace FileTime.Providers.Local
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly();
public string Name { get; } = "local";
public string Protocol { get; } = "local://";
public string? FullName { get; }
public string? NativePath => null;
@@ -59,7 +60,7 @@ namespace FileTime.Providers.Local
path = path.Replace(Path.DirectorySeparatorChar, Constants.SeparatorChar).TrimEnd(Constants.SeparatorChar);
var pathParts = (IsCaseInsensitive ? path.ToLower() : path).TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && pathParts.Length == 1 && pathParts[0] == "") return this;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && pathParts.Length == 1 && pathParts[0]?.Length == 0) return this;
var normalizedRootContainerName = NormalizePath(pathParts[0]);
var rootContainer = _rootContainers.FirstOrDefault(c => NormalizePath(c.Name) == normalizedRootContainerName);

View File

@@ -1,10 +1,15 @@
using FileTime.Core.Providers;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Providers.Local
{
public static class Startup
{
public static Type[] GetCommandHandlers()
public static IServiceCollection AddLocalServices(this IServiceCollection serviceCollection)
{
return Array.Empty<Type>();
return serviceCollection
.AddSingleton<LocalContentProvider>()
.AddSingleton<IContentProvider, LocalContentProvider>(sp => sp.GetService<LocalContentProvider>() ?? throw new Exception($"No {nameof(LocalContentProvider)} instance found"));
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SSH.NET" Version="2020.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core\FileTime.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,63 @@
using Renci.SshNet;
namespace FileTime.Providers.Sftp
{
public class SftpClientContext
{
private readonly Func<int, Task<SftpClient>> _getSftpClient;
private readonly Action _disposeClient;
private bool _isRunning;
private readonly object _lock = new();
public SftpClientContext(Func<int, Task<SftpClient>> getSftpClient, Action disposeClient)
{
_getSftpClient = getSftpClient;
_disposeClient = disposeClient;
}
public async Task RunWithSftpClientAsync(Action<SftpClient> action, int maxRetries = SftpServer.MAXRETRIES)
{
await RunWithSftpClientAsync<object?>((client) => { action(client); return null; }, maxRetries);
}
public async Task<T> RunWithSftpClientAsync<T>(Func<SftpClient, T> func, int maxRetries = SftpServer.MAXRETRIES)
{
while (true)
{
lock (_lock)
{
if (!_isRunning)
{
_isRunning = true;
break;
}
}
await Task.Delay(1);
}
try
{
SftpClient client;
while (true)
{
try
{
client = await _getSftpClient(maxRetries);
return func(client);
}
//TODO: dispose client on Sftp exception
catch (Exception e)
{
throw new Exception("Exception was thrown while executing method with SftpClient.", e);
}
}
}
finally
{
lock (_lock)
{
_isRunning = false;
}
}
}
}
}

View File

@@ -0,0 +1,153 @@
using AsyncEvent;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
using FileTime.Core.Providers;
using Microsoft.Extensions.Logging;
namespace FileTime.Providers.Sftp
{
public class SftpContentProvider : IContentProvider
{
private IContainer? _parent;
private readonly IInputInterface _inputInterface;
private readonly List<IContainer> _rootContainers;
private readonly IReadOnlyList<IContainer> _rootContainersReadOnly;
private IReadOnlyList<IItem>? _items;
private readonly IReadOnlyList<IElement> _elements = new List<IElement>().AsReadOnly();
private readonly ILogger<SftpContentProvider> _logger;
public bool SupportsContentStreams => false;
public IReadOnlyList<Exception> Exceptions { get; } = new List<Exception>().AsReadOnly();
public bool IsLoaded => true;
public bool SupportsDirectoryLevelSoftDelete => false;
public AsyncEventHandler Refreshed { get; } = new AsyncEventHandler();
public string Name => "sftp";
public string? FullName => null;
public string? NativePath => null;
public bool IsHidden => false;
public bool IsDestroyed => false;
public SupportsDelete CanDelete => SupportsDelete.False;
public bool CanRename => false;
public IContentProvider Provider => this;
public string Protocol => "sftp://";
public SftpContentProvider(IInputInterface inputInterface, ILogger<SftpContentProvider> logger)
{
_logger = logger;
_rootContainers = new List<IContainer>();
_items = new List<IItem>();
_rootContainersReadOnly = _rootContainers.AsReadOnly();
_inputInterface = inputInterface;
}
public bool CanHandlePath(string path) => path.StartsWith("sftp://");
public Task<bool> CanOpenAsync() => Task.FromResult(true);
public Task<IContainer> CloneAsync() => Task.FromResult((IContainer)this);
public async Task<IContainer> CreateContainerAsync(string name)
{
var container = _rootContainers.Find(c => c.Name == name);
if (container == null)
{
container = new SftpServer(name, this, _inputInterface);
_rootContainers.Add(container);
_items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
}
await RefreshAsync();
//await SaveServers();
return container;
}
public Task<IElement> CreateElementAsync(string name)
{
throw new NotSupportedException();
}
public Task Delete(bool hardDelete = false)
{
throw new NotSupportedException();
}
public void Destroy() { }
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 async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
{
await Init();
return _items;
}
public IContainer? GetParent() => _parent;
public Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default) => Task.FromResult(_rootContainersReadOnly);
public async Task<bool> IsExistsAsync(string name) => (await GetItems())?.Any(i => i.Name == name) ?? false;
public async Task RefreshAsync(CancellationToken token = default) => await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token);
public Task Rename(string newName) => throw new NotSupportedException();
public void SetParent(IContainer container) => _parent = container;
public void Unload() { }
private Task Init()
{
return Task.CompletedTask;
}
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
if (path == null) return this;
var pathParts = path.TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar);
var rootContainer = (await GetContainers())?.FirstOrDefault(c => c.Name == pathParts[0]);
if (rootContainer == null)
{
return null;
}
var remainingPath = string.Join(Constants.SeparatorChar, pathParts.Skip(1));
try
{
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath, acceptDeepestMatch);
}
catch (Exception e)
{
_logger.LogError(e, "Error while getting path {Path}", path);
if (acceptDeepestMatch)
{
return rootContainer ?? this;
}
else
{
throw;
}
}
}
}
}

View File

@@ -0,0 +1,71 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Providers.Sftp
{
public class SftpFile : IElement
{
public bool IsSpecial => throw new NotImplementedException();
public string Name => throw new NotImplementedException();
public string? FullName => throw new NotImplementedException();
public string? NativePath => throw new NotImplementedException();
public bool IsHidden => throw new NotImplementedException();
public bool IsDestroyed => throw new NotImplementedException();
public SupportsDelete CanDelete => throw new NotImplementedException();
public bool CanRename => throw new NotImplementedException();
public IContentProvider Provider => throw new NotImplementedException();
public Task Delete(bool hardDelete = false)
{
throw new NotImplementedException();
}
public void Destroy()
{
throw new NotImplementedException();
}
public Task<string> GetContent(CancellationToken token = default)
{
throw new NotImplementedException();
}
public Task<IContentReader> GetContentReaderAsync()
{
throw new NotImplementedException();
}
public Task<IContentWriter> GetContentWriterAsync()
{
throw new NotImplementedException();
}
public Task<long> GetElementSize(CancellationToken token = default)
{
throw new NotImplementedException();
}
public IContainer? GetParent()
{
throw new NotImplementedException();
}
public string GetPrimaryAttributeText()
{
throw new NotImplementedException();
}
public Task Rename(string newName)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,44 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Providers.Sftp
{
public class SftpFolder : AbstractContainer<SftpContentProvider>
{
private readonly SftpServer _server;
public SftpFolder(SftpContentProvider provider, SftpServer server, IContainer parent, string path) : base(provider, parent, path)
{
_server = server;
NativePath = FullName;
}
public override Task<bool> CanOpenAsync() => Task.FromResult(true);
public override Task<IContainer> CloneAsync()
{
return Task.FromResult((IContainer)new SftpFolder(Provider, _server, GetParent()!, Name));
}
public override Task<IContainer> CreateContainerAsync(string name)
{
throw new NotImplementedException();
}
public override Task<IElement> CreateElementAsync(string name)
{
throw new NotImplementedException();
}
public override Task Delete(bool hardDelete = false)
{
throw new NotImplementedException();
}
public override async Task<IEnumerable<IItem>> RefreshItems(CancellationToken token = default) => await _server.ListDirectory(FullName![(_server.FullName!.Length + 1)..]);
public override Task Rename(string newName)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,175 @@
using FileTime.Core.Interactions;
using FileTime.Core.Models;
using FileTime.Core.Providers;
using Renci.SshNet;
using Renci.SshNet.Common;
namespace FileTime.Providers.Sftp
{
public class SftpServer : AbstractContainer<SftpContentProvider>
{
internal const int MAXRETRIES = 5;
private bool _reenterCredentials;
private SftpClient? _client;
private readonly SftpClientContext _sftpClientContext;
private bool _refreshingClient;
private readonly object _clientGuard = new();
private readonly IInputInterface _inputInterface;
public string? Username { get; private set; }
public string? Password { get; private set; }
public SftpServer(string name, SftpContentProvider sftpContentProvider, IInputInterface inputInterface, string? username = null, string? password = null)
: base(sftpContentProvider, sftpContentProvider, name)
{
_inputInterface = inputInterface;
_sftpClientContext = new SftpClientContext(GetClient, () => { });
Username = username;
Password = password;
Name = name;
NativePath = FullName = sftpContentProvider.Protocol + Constants.SeparatorChar + name;
CanDelete = SupportsDelete.True;
}
public override async Task<IEnumerable<IItem>> RefreshItems(CancellationToken token = default) => await ListDirectory("");
public override Task<bool> CanOpenAsync() => Task.FromResult(true);
public override Task<IContainer> CloneAsync() => Task.FromResult((IContainer)this);
public override Task<IContainer> CreateContainerAsync(string name)
{
throw new NotImplementedException();
}
public override Task<IElement> CreateElementAsync(string name)
{
throw new NotImplementedException();
}
public override Task Delete(bool hardDelete = false)
{
throw new NotImplementedException();
}
public override Task Rename(string newName)
{
throw new NotImplementedException();
}
public override void Unload()
{
base.Unload();
lock (_clientGuard)
{
_client?.Disconnect();
_client = null;
}
}
private async Task<SftpClient> GetClient(int maxRetries = MAXRETRIES)
{
bool isClientNull;
lock (_clientGuard)
{
isClientNull = _client == null;
}
int reTries = 0;
while (isClientNull)
{
if (!await RefreshSftpClient())
{
await Task.Delay(1);
}
lock (_clientGuard)
{
isClientNull = _client == null;
}
if (reTries >= maxRetries)
{
throw new Exception($"Could not connect to server {Name} after {reTries} retry");
}
reTries++;
}
return _client!;
}
private async Task<bool> RefreshSftpClient()
{
lock (_clientGuard)
{
if (_refreshingClient) return false;
_refreshingClient = true;
}
try
{
if (_reenterCredentials || Username == null || Password == null)
{
var inputs = await _inputInterface.ReadInputs(
new InputElement[]
{
InputElement.ForText($"Username for '{Name}'", Username ?? ""),
InputElement.ForPassword($"Password for '{Name}'", Password ?? "")
});
Username = inputs[0];
Password = inputs[1];
}
var client = new SftpClient(Name, Username, Password);
try
{
client.Connect();
}
catch (SshAuthenticationException)
{
_reenterCredentials = true;
}
catch
{
throw;
}
lock (_clientGuard)
{
_client = client;
}
}
finally
{
lock (_clientGuard)
{
_refreshingClient = false;
}
}
return true;
}
public async Task<IEnumerable<IItem>> ListDirectory(string path)
{
return await _sftpClientContext.RunWithSftpClientAsync(client =>
{
var containers = new List<IContainer>();
var elements = new List<IElement>();
foreach (var file in client.ListDirectory(path))
{
if (file.IsDirectory)
{
var container = new SftpFolder(Provider, this, this, file.Name);
containers.Add(container);
}
}
return containers.Cast<IItem>().Concat(elements);
});
}
}
}

View File

@@ -0,0 +1,14 @@
using FileTime.Core.Providers;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Providers.Sftp
{
public static class Startup
{
public static IServiceCollection AddSftpServices(this IServiceCollection serviceCollection)
{
return serviceCollection
.AddSingleton<IContentProvider, SftpContentProvider>();
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using AsyncEvent;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
@@ -22,8 +23,9 @@ namespace FileTime.Providers.Smb
private readonly ILogger<SmbContentProvider> _logger;
public string Name { get; } = "smb";
public string Protocol { get; } = "smb://";
public string? FullName { get; }
public string? FullName => null;
public string? NativePath => null;
public bool IsHidden => false;
@@ -53,12 +55,11 @@ namespace FileTime.Providers.Smb
public async Task<IContainer> CreateContainerAsync(string name)
{
var fullName = "\\\\" + name;
var container = _rootContainers.Find(c => c.Name == name);
if (container == null)
{
container = new SmbServer(fullName, this, _inputInterface);
container = new SmbServer(name, this, _inputInterface);
_rootContainers.Add(container);
_items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
}
@@ -84,7 +85,10 @@ namespace FileTime.Providers.Smb
{
if (path == null) return this;
var pathParts = path.TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar);
path = path.TrimStart(Constants.SeparatorChar);
if (path.StartsWith("\\\\")) path = path[2..];
else if (path.StartsWith("smb://")) path = path[6..];
var pathParts = path.Split(Constants.SeparatorChar);
var rootContainer = (await GetContainers())?.FirstOrDefault(c => c.Name == pathParts[0]);
@@ -195,6 +199,6 @@ namespace FileTime.Providers.Smb
}
}
public static string GetNativePath(string fullName) => fullName.Replace("/", "\\");
public static string GetNativePathSeparator() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "\\" : "/";
}
}

View File

@@ -27,14 +27,14 @@ namespace FileTime.Providers.Smb
public SmbFile(string name, SmbContentProvider provider, SmbShare smbShare, IContainer parent, SmbClientContext smbClientContext)
{
Name = name;
FullName = parent.FullName + Constants.SeparatorChar + Name;
NativePath = SmbContentProvider.GetNativePath(FullName);
Provider = provider;
_parent = parent;
_smbClientContext = smbClientContext;
_smbShare = smbShare;
Name = name;
FullName = parent.FullName + Constants.SeparatorChar + Name;
NativePath = parent.NativePath + SmbContentProvider.GetNativePathSeparator() + name;
}
public async Task Delete(bool hardDelete = false)
@@ -146,6 +146,6 @@ namespace FileTime.Providers.Smb
});
}
private string GetPathFromShare() => SmbContentProvider.GetNativePath(FullName![(_smbShare.FullName!.Length + 1)..]);
private string GetPathFromShare() => FullName![(_smbShare.FullName!.Length + 1)..].Replace("/", "\\");
}
}

View File

@@ -37,19 +37,19 @@ namespace FileTime.Providers.Smb
public SmbFolder(string name, SmbContentProvider contentProvider, SmbShare smbShare, IContainer parent, SmbClientContext smbClientContext)
{
_parent = parent;
_smbClientContext = smbClientContext;
SmbShare = smbShare;
Name = name;
FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
NativePath = SmbContentProvider.GetNativePath(FullName);
FullName = parent.FullName! + Constants.SeparatorChar + Name;
NativePath = parent.NativePath + SmbContentProvider.GetNativePathSeparator() + name;
Provider = contentProvider;
_smbClientContext = smbClientContext;
}
public async Task<IContainer> CreateContainerAsync(string name)
{
var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name;
await SmbShare.CreateContainerWithPathAsync(SmbContentProvider.GetNativePath(path));
await SmbShare.CreateContainerWithPathAsync(path.Replace("/", "\\"));
await RefreshAsync();
return _containers!.FirstOrDefault(e => e.Name == name)!;
@@ -58,7 +58,7 @@ namespace FileTime.Providers.Smb
public async Task<IElement> CreateElementAsync(string name)
{
var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name;
await SmbShare.CreateElementWithPathAsync(SmbContentProvider.GetNativePath(path));
await SmbShare.CreateElementWithPathAsync(path.Replace("/", "\\"));
await RefreshAsync();
return _elements!.FirstOrDefault(e => e.Name == name)!;
@@ -161,6 +161,6 @@ namespace FileTime.Providers.Smb
_elements = null;
}
private string GetPathFromShare() => SmbContentProvider.GetNativePath(FullName![(SmbShare.FullName!.Length + 1)..]);
private string GetPathFromShare() => FullName![(SmbShare.FullName!.Length + 1)..].Replace("/", "\\");
}
}

View File

@@ -1,4 +1,5 @@
using System.Net;
using System.Runtime.InteropServices;
using AsyncEvent;
using FileTime.Core.Interactions;
using FileTime.Core.Models;
@@ -56,7 +57,11 @@ namespace FileTime.Providers.Smb
Password = password;
Provider = contentProvider;
NativePath = FullName = Name = path;
Name = path;
FullName = contentProvider.Protocol + Name;
NativePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "\\\\" + Name
: contentProvider.Protocol + Name;
}
public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
@@ -182,11 +187,11 @@ namespace FileTime.Providers.Smb
}
try
{
var couldParse = IPAddress.TryParse(Name[2..], out var ipAddress);
var couldParse = IPAddress.TryParse(Name, out var ipAddress);
var client = new SMB2Client();
var connected = couldParse
? client.Connect(ipAddress, SMBTransportType.DirectTCPTransport)
: client.Connect(Name[2..], SMBTransportType.DirectTCPTransport);
: client.Connect(Name, SMBTransportType.DirectTCPTransport);
if (connected)
{

View File

@@ -40,8 +40,8 @@ namespace FileTime.Providers.Smb
_smbClientContext = smbClientContext;
Name = name;
FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
NativePath = SmbContentProvider.GetNativePath(FullName);
FullName = parent.FullName! + Constants.SeparatorChar + Name;
NativePath = parent.NativePath + SmbContentProvider.GetNativePathSeparator() + name;
Provider = contentProvider;
}

View File

@@ -1,3 +1,4 @@
using FileTime.Core.Providers;
using FileTime.Providers.Smb.Persistence;
using Microsoft.Extensions.DependencyInjection;
@@ -8,7 +9,8 @@ namespace FileTime.Providers.Smb
public static IServiceCollection AddSmbServices(this IServiceCollection serviceCollection)
{
return serviceCollection
.AddSingleton<PersistenceService>();
.AddSingleton<PersistenceService>()
.AddSingleton<IContentProvider, SmbContentProvider>();
}
}
}