Sftp, fullname+nativepath refactor
This commit is contained in:
@@ -4,6 +4,7 @@ using FileTime.Core.CommandHandlers;
|
|||||||
using FileTime.Core.Providers;
|
using FileTime.Core.Providers;
|
||||||
using FileTime.Core.Timeline;
|
using FileTime.Core.Timeline;
|
||||||
using FileTime.Providers.Local;
|
using FileTime.Providers.Local;
|
||||||
|
using FileTime.Providers.Sftp;
|
||||||
using FileTime.Providers.Smb;
|
using FileTime.Providers.Smb;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -18,11 +19,11 @@ namespace FileTime.App.Core
|
|||||||
return serviceCollection
|
return serviceCollection
|
||||||
.AddSingleton<IClipboard, Clipboard.Clipboard>()
|
.AddSingleton<IClipboard, Clipboard.Clipboard>()
|
||||||
.AddSingleton<TopContainer>()
|
.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<CommandExecutor>()
|
||||||
.AddSingleton<TimeRunner>()
|
.AddSingleton<TimeRunner>()
|
||||||
|
.AddLocalServices()
|
||||||
|
.AddSmbServices()
|
||||||
|
.AddSftpServices()
|
||||||
.RegisterCommandHandlers();
|
.RegisterCommandHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Providers\FileTime.Providers.Sftp\FileTime.Providers.Sftp.csproj" />
|
||||||
<ProjectReference Include="..\..\Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj" />
|
<ProjectReference Include="..\..\Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj" />
|
||||||
<ProjectReference Include="..\FileTime.App.Core\FileTime.App.Core.csproj" />
|
<ProjectReference Include="..\FileTime.App.Core\FileTime.App.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local\FileTime.Providers.Local.csproj" />
|
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local\FileTime.Providers.Local.csproj" />
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ namespace FileTime.Core.Components
|
|||||||
{
|
{
|
||||||
if (_currentlySelecting) return false;
|
if (_currentlySelecting) return false;
|
||||||
|
|
||||||
if (_currentSelectedItem == value) return false;
|
|
||||||
IItem? itemToSelect = null;
|
IItem? itemToSelect = null;
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace FileTime.Core.Models
|
|||||||
Task RefreshAsync(CancellationToken token = default);
|
Task RefreshAsync(CancellationToken token = default);
|
||||||
async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
||||||
{
|
{
|
||||||
|
if (path == null) return this;
|
||||||
var paths = path.Split(Constants.SeparatorChar);
|
var paths = path.Split(Constants.SeparatorChar);
|
||||||
|
|
||||||
var item = (await GetItems())?.FirstOrDefault(i => i.Name == paths[0]);
|
var item = (await GetItems())?.FirstOrDefault(i => i.Name == paths[0]);
|
||||||
|
|||||||
134
src/Core/FileTime.Core/Providers/AbstractContainer.cs
Normal file
134
src/Core/FileTime.Core/Providers/AbstractContainer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace FileTime.Core.Providers
|
|||||||
public interface IContentProvider : IContainer
|
public interface IContentProvider : IContainer
|
||||||
{
|
{
|
||||||
bool SupportsContentStreams { get; }
|
bool SupportsContentStreams { get; }
|
||||||
|
string Protocol { get; }
|
||||||
|
|
||||||
Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default);
|
Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default);
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ namespace FileTime.Core.Timeline
|
|||||||
public bool IsDestroyed => false;
|
public bool IsDestroyed => false;
|
||||||
public bool SupportsContentStreams => false;
|
public bool SupportsContentStreams => false;
|
||||||
|
|
||||||
|
public string Protocol => "time2://";
|
||||||
|
|
||||||
public TimeProvider(PointInTime pointInTime)
|
public TimeProvider(PointInTime pointInTime)
|
||||||
{
|
{
|
||||||
_pointInTime = pointInTime;
|
_pointInTime = pointInTime;
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{0D2B4BAA
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Tools.Compression", "Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj", "{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Tools.Compression", "Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj", "{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -310,6 +332,7 @@ Global
|
|||||||
{9BDAC126-200F-4056-8D35-36EC059B40F3} = {38B1B927-4201-4B7A-87EE-737B8C6D4090}
|
{9BDAC126-200F-4056-8D35-36EC059B40F3} = {38B1B927-4201-4B7A-87EE-737B8C6D4090}
|
||||||
{22B33BC6-3987-4BE6-8C54-BFC75C78CCE7} = {890275FF-943A-4D07-83BA-14E5C52D7846}
|
{22B33BC6-3987-4BE6-8C54-BFC75C78CCE7} = {890275FF-943A-4D07-83BA-14E5C52D7846}
|
||||||
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966} = {0D2B4BAA-0399-459C-B022-41DB7F408225}
|
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966} = {0D2B4BAA-0399-459C-B022-41DB7F408225}
|
||||||
|
{0E650206-801D-4E8D-95BA-4565B32092E1} = {517D96CE-A956-4638-A93D-465D34DE22B1}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {8D679DCE-AC84-4A91-BFED-8F8D8E1D8183}
|
SolutionGuid = {8D679DCE-AC84-4A91-BFED-8F8D8E1D8183}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
<converters:GetFileExtensionConverter x:Key="GetFileExtensionConverter"/>
|
<converters:GetFileExtensionConverter x:Key="GetFileExtensionConverter"/>
|
||||||
<converters:CommandToCommandNameConverter x:Key="CommandToCommandNameConverter"/>
|
<converters:CommandToCommandNameConverter x:Key="CommandToCommandNameConverter"/>
|
||||||
<converters:NamePartShrinkerConverter x:Key="NamePartShrinkerConverter"/>
|
<converters:NamePartShrinkerConverter x:Key="NamePartShrinkerConverter"/>
|
||||||
|
<converters:StringReplaceConverter x:Key="PathPreformatter" OldValue="://" NewValue="/"/>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ namespace FileTime.Avalonia
|
|||||||
.AddServices()
|
.AddServices()
|
||||||
.RegisterLogging()
|
.RegisterLogging()
|
||||||
.AddViewModels()
|
.AddViewModels()
|
||||||
.RegisterCommandHandlers()
|
|
||||||
.BuildServiceProvider()
|
.BuildServiceProvider()
|
||||||
.InitSerilog();
|
.InitSerilog();
|
||||||
|
|
||||||
|
|||||||
@@ -51,16 +51,11 @@ namespace FileTime.Avalonia.Application
|
|||||||
{
|
{
|
||||||
if (!_updateFromCode && value != null)
|
if (!_updateFromCode && value != null)
|
||||||
{
|
{
|
||||||
/*try
|
try
|
||||||
{*/
|
|
||||||
/*var task = SetSelectedItemAsync(value, true);
|
|
||||||
Task.WaitAll(new Task[] { task }, 100);*/
|
|
||||||
Task.Run(async () => await SetSelectedItemAsync(value, true)).Wait();
|
|
||||||
/*}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
//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) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ 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.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;
|
||||||
@@ -39,7 +38,6 @@ namespace FileTime.Avalonia
|
|||||||
.AddSingleton(new PersistenceSettings(Program.AppDataRoot))
|
.AddSingleton(new PersistenceSettings(Program.AppDataRoot))
|
||||||
.AddSingleton<ProgramsService>()
|
.AddSingleton<ProgramsService>()
|
||||||
.AddSingleton<ToastMessageSink>()
|
.AddSingleton<ToastMessageSink>()
|
||||||
.AddSmbServices()
|
|
||||||
.AddSingleton<IIconProvider, MaterialIconProvider>();
|
.AddSingleton<IIconProvider, MaterialIconProvider>();
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
@@ -53,15 +51,6 @@ namespace FileTime.Avalonia
|
|||||||
|
|
||||||
return serviceCollection;
|
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)
|
internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<Rectangle Fill="#01000000"/>
|
<Rectangle Fill="#01000000"/>
|
||||||
|
|
||||||
<StackPanel Margin="20,10" Orientation="Horizontal">
|
<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
|
<TextBlock
|
||||||
Text="{Binding AppState.SelectedTab.SelectedItem.Item.Name}" Foreground="{StaticResource AccentBrush}" />
|
Text="{Binding AppState.SelectedTab.SelectedItem.Item.Name}" Foreground="{StaticResource AccentBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0"/>
|
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace FileTime.Providers.Local
|
|||||||
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly();
|
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().AsReadOnly();
|
||||||
|
|
||||||
public string Name { get; } = "local";
|
public string Name { get; } = "local";
|
||||||
|
public string Protocol { get; } = "local://";
|
||||||
|
|
||||||
public string? FullName { get; }
|
public string? FullName { get; }
|
||||||
public string? NativePath => null;
|
public string? NativePath => null;
|
||||||
@@ -59,7 +60,7 @@ namespace FileTime.Providers.Local
|
|||||||
path = path.Replace(Path.DirectorySeparatorChar, Constants.SeparatorChar).TrimEnd(Constants.SeparatorChar);
|
path = path.Replace(Path.DirectorySeparatorChar, Constants.SeparatorChar).TrimEnd(Constants.SeparatorChar);
|
||||||
var pathParts = (IsCaseInsensitive ? path.ToLower() : path).TrimStart(Constants.SeparatorChar).Split(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 normalizedRootContainerName = NormalizePath(pathParts[0]);
|
||||||
var rootContainer = _rootContainers.FirstOrDefault(c => NormalizePath(c.Name) == normalizedRootContainerName);
|
var rootContainer = _rootContainers.FirstOrDefault(c => NormalizePath(c.Name) == normalizedRootContainerName);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
using FileTime.Core.Providers;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace FileTime.Providers.Local
|
namespace FileTime.Providers.Local
|
||||||
{
|
{
|
||||||
public static class Startup
|
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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
63
src/Providers/FileTime.Providers.Sftp/SftpClientContext.cs
Normal file
63
src/Providers/FileTime.Providers.Sftp/SftpClientContext.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs
Normal file
153
src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Providers/FileTime.Providers.Sftp/SftpFile.cs
Normal file
71
src/Providers/FileTime.Providers.Sftp/SftpFile.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Providers/FileTime.Providers.Sftp/SftpFolder.cs
Normal file
44
src/Providers/FileTime.Providers.Sftp/SftpFolder.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
175
src/Providers/FileTime.Providers.Sftp/SftpServer.cs
Normal file
175
src/Providers/FileTime.Providers.Sftp/SftpServer.cs
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Providers/FileTime.Providers.Sftp/Startup.cs
Normal file
14
src/Providers/FileTime.Providers.Sftp/Startup.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using AsyncEvent;
|
using AsyncEvent;
|
||||||
using FileTime.Core.Interactions;
|
using FileTime.Core.Interactions;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
@@ -22,8 +23,9 @@ namespace FileTime.Providers.Smb
|
|||||||
private readonly ILogger<SmbContentProvider> _logger;
|
private readonly ILogger<SmbContentProvider> _logger;
|
||||||
|
|
||||||
public string Name { get; } = "smb";
|
public string Name { get; } = "smb";
|
||||||
|
public string Protocol { get; } = "smb://";
|
||||||
|
|
||||||
public string? FullName { get; }
|
public string? FullName => null;
|
||||||
public string? NativePath => null;
|
public string? NativePath => null;
|
||||||
|
|
||||||
public bool IsHidden => false;
|
public bool IsHidden => false;
|
||||||
@@ -53,12 +55,11 @@ namespace FileTime.Providers.Smb
|
|||||||
|
|
||||||
public async Task<IContainer> CreateContainerAsync(string name)
|
public async Task<IContainer> CreateContainerAsync(string name)
|
||||||
{
|
{
|
||||||
var fullName = "\\\\" + name;
|
|
||||||
var container = _rootContainers.Find(c => c.Name == name);
|
var container = _rootContainers.Find(c => c.Name == name);
|
||||||
|
|
||||||
if (container == null)
|
if (container == null)
|
||||||
{
|
{
|
||||||
container = new SmbServer(fullName, this, _inputInterface);
|
container = new SmbServer(name, this, _inputInterface);
|
||||||
_rootContainers.Add(container);
|
_rootContainers.Add(container);
|
||||||
_items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
|
_items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
|
||||||
}
|
}
|
||||||
@@ -84,7 +85,10 @@ namespace FileTime.Providers.Smb
|
|||||||
{
|
{
|
||||||
if (path == null) return this;
|
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]);
|
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) ? "\\" : "/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,14 +27,14 @@ namespace FileTime.Providers.Smb
|
|||||||
|
|
||||||
public SmbFile(string name, SmbContentProvider provider, SmbShare smbShare, IContainer parent, SmbClientContext smbClientContext)
|
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;
|
Provider = provider;
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
_smbClientContext = smbClientContext;
|
_smbClientContext = smbClientContext;
|
||||||
_smbShare = smbShare;
|
_smbShare = smbShare;
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
FullName = parent.FullName + Constants.SeparatorChar + Name;
|
||||||
|
NativePath = parent.NativePath + SmbContentProvider.GetNativePathSeparator() + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Delete(bool hardDelete = false)
|
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("/", "\\");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,19 +37,19 @@ namespace FileTime.Providers.Smb
|
|||||||
public SmbFolder(string name, SmbContentProvider contentProvider, SmbShare smbShare, IContainer parent, SmbClientContext smbClientContext)
|
public SmbFolder(string name, SmbContentProvider contentProvider, SmbShare smbShare, IContainer parent, SmbClientContext smbClientContext)
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
|
_smbClientContext = smbClientContext;
|
||||||
SmbShare = smbShare;
|
SmbShare = smbShare;
|
||||||
|
|
||||||
Name = name;
|
Name = name;
|
||||||
FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
|
FullName = parent.FullName! + Constants.SeparatorChar + Name;
|
||||||
NativePath = SmbContentProvider.GetNativePath(FullName);
|
NativePath = parent.NativePath + SmbContentProvider.GetNativePathSeparator() + name;
|
||||||
Provider = contentProvider;
|
Provider = contentProvider;
|
||||||
_smbClientContext = smbClientContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IContainer> CreateContainerAsync(string name)
|
public async Task<IContainer> CreateContainerAsync(string name)
|
||||||
{
|
{
|
||||||
var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name;
|
var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name;
|
||||||
await SmbShare.CreateContainerWithPathAsync(SmbContentProvider.GetNativePath(path));
|
await SmbShare.CreateContainerWithPathAsync(path.Replace("/", "\\"));
|
||||||
await RefreshAsync();
|
await RefreshAsync();
|
||||||
|
|
||||||
return _containers!.FirstOrDefault(e => e.Name == name)!;
|
return _containers!.FirstOrDefault(e => e.Name == name)!;
|
||||||
@@ -58,7 +58,7 @@ namespace FileTime.Providers.Smb
|
|||||||
public async Task<IElement> CreateElementAsync(string name)
|
public async Task<IElement> CreateElementAsync(string name)
|
||||||
{
|
{
|
||||||
var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name;
|
var path = FullName![(SmbShare.FullName!.Length + 1)..] + Constants.SeparatorChar + name;
|
||||||
await SmbShare.CreateElementWithPathAsync(SmbContentProvider.GetNativePath(path));
|
await SmbShare.CreateElementWithPathAsync(path.Replace("/", "\\"));
|
||||||
await RefreshAsync();
|
await RefreshAsync();
|
||||||
|
|
||||||
return _elements!.FirstOrDefault(e => e.Name == name)!;
|
return _elements!.FirstOrDefault(e => e.Name == name)!;
|
||||||
@@ -161,6 +161,6 @@ namespace FileTime.Providers.Smb
|
|||||||
_elements = null;
|
_elements = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetPathFromShare() => SmbContentProvider.GetNativePath(FullName![(SmbShare.FullName!.Length + 1)..]);
|
private string GetPathFromShare() => FullName![(SmbShare.FullName!.Length + 1)..].Replace("/", "\\");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using AsyncEvent;
|
using AsyncEvent;
|
||||||
using FileTime.Core.Interactions;
|
using FileTime.Core.Interactions;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
@@ -56,7 +57,11 @@ namespace FileTime.Providers.Smb
|
|||||||
Password = password;
|
Password = password;
|
||||||
|
|
||||||
Provider = contentProvider;
|
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)
|
public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
|
||||||
@@ -182,11 +187,11 @@ namespace FileTime.Providers.Smb
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var couldParse = IPAddress.TryParse(Name[2..], out var ipAddress);
|
var couldParse = IPAddress.TryParse(Name, out var ipAddress);
|
||||||
var client = new SMB2Client();
|
var client = new SMB2Client();
|
||||||
var connected = couldParse
|
var connected = couldParse
|
||||||
? client.Connect(ipAddress, SMBTransportType.DirectTCPTransport)
|
? client.Connect(ipAddress, SMBTransportType.DirectTCPTransport)
|
||||||
: client.Connect(Name[2..], SMBTransportType.DirectTCPTransport);
|
: client.Connect(Name, SMBTransportType.DirectTCPTransport);
|
||||||
|
|
||||||
if (connected)
|
if (connected)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ namespace FileTime.Providers.Smb
|
|||||||
_smbClientContext = smbClientContext;
|
_smbClientContext = smbClientContext;
|
||||||
|
|
||||||
Name = name;
|
Name = name;
|
||||||
FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
|
FullName = parent.FullName! + Constants.SeparatorChar + Name;
|
||||||
NativePath = SmbContentProvider.GetNativePath(FullName);
|
NativePath = parent.NativePath + SmbContentProvider.GetNativePathSeparator() + name;
|
||||||
Provider = contentProvider;
|
Provider = contentProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using FileTime.Core.Providers;
|
||||||
using FileTime.Providers.Smb.Persistence;
|
using FileTime.Providers.Smb.Persistence;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -8,7 +9,8 @@ namespace FileTime.Providers.Smb
|
|||||||
public static IServiceCollection AddSmbServices(this IServiceCollection serviceCollection)
|
public static IServiceCollection AddSmbServices(this IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
return serviceCollection
|
return serviceCollection
|
||||||
.AddSingleton<PersistenceService>();
|
.AddSingleton<PersistenceService>()
|
||||||
|
.AddSingleton<IContentProvider, SmbContentProvider>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user