ContentProvider refactor
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Reflection.Metadata;
|
||||||
using AsyncEvent;
|
using AsyncEvent;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Timeline;
|
using FileTime.Core.Timeline;
|
||||||
@@ -65,7 +66,7 @@ namespace FileTime.Core.Command.Delete
|
|||||||
await TraverseTree((await item.ResolveAsync())!);
|
await TraverseTree((await item.ResolveAsync())!);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach(var updatedParent in ItemsToDelete.Select(i => i.GetParent()).Distinct())
|
foreach (var updatedParent in ItemsToDelete.Select(i => i.GetParent()).Distinct())
|
||||||
{
|
{
|
||||||
await timeRunner.RefreshContainer.InvokeAsync(this, updatedParent);
|
await timeRunner.RefreshContainer.InvokeAsync(this, updatedParent);
|
||||||
}
|
}
|
||||||
@@ -80,11 +81,14 @@ namespace FileTime.Core.Command.Delete
|
|||||||
if (_deleteContainer != null) await _deleteContainer.Invoke(container);
|
if (_deleteContainer != null) await _deleteContainer.Invoke(container);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
if (container.AllowRecursiveDeletion)
|
||||||
{
|
{
|
||||||
foreach (var child in (await container.GetItems())!)
|
foreach (var child in (await container.GetItems())!)
|
||||||
{
|
{
|
||||||
await TraverseTree(child);
|
await TraverseTree(child);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_deleteContainer != null) await _deleteContainer.Invoke(container);
|
if (_deleteContainer != null) await _deleteContainer.Invoke(container);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace FileTime.Core.Models
|
|||||||
Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default);
|
Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default);
|
||||||
Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default);
|
Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default);
|
||||||
Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default);
|
Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default);
|
||||||
|
bool AllowRecursiveDeletion { get; }
|
||||||
|
|
||||||
Task RefreshAsync(CancellationToken token = default);
|
Task RefreshAsync(CancellationToken token = default);
|
||||||
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ namespace FileTime.Core.Models
|
|||||||
|
|
||||||
public bool IsDestroyed => BaseContainer.IsDestroyed;
|
public bool IsDestroyed => BaseContainer.IsDestroyed;
|
||||||
public bool IsExists => BaseContainer.IsExists;
|
public bool IsExists => BaseContainer.IsExists;
|
||||||
|
public bool AllowRecursiveDeletion => BaseContainer.AllowRecursiveDeletion;
|
||||||
|
|
||||||
private void RefreshAddBase(Func<object?, AsyncEventArgs, CancellationToken, Task> handler)
|
private void RefreshAddBase(Func<object?, AsyncEventArgs, CancellationToken, Task> handler)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ using FileTime.Core.Models;
|
|||||||
|
|
||||||
namespace FileTime.Core.Providers
|
namespace FileTime.Core.Providers
|
||||||
{
|
{
|
||||||
public abstract class AbstractContainer<TProvider> : IContainer where TProvider : IContentProvider
|
public abstract class AbstractContainer<TProvider> : IContainer where TProvider : class, IContentProvider
|
||||||
{
|
{
|
||||||
private readonly IContainer _parent;
|
private readonly IContainer? _parent;
|
||||||
private readonly List<Exception> _exceptions = new();
|
private readonly List<Exception> _exceptions = new();
|
||||||
private IReadOnlyList<IContainer>? _containers;
|
private IReadOnlyList<IContainer>? _containers;
|
||||||
private IReadOnlyList<IItem>? _items;
|
private IReadOnlyList<IItem>? _items;
|
||||||
@@ -39,13 +39,26 @@ namespace FileTime.Core.Providers
|
|||||||
|
|
||||||
public abstract bool IsExists { get; }
|
public abstract bool IsExists { get; }
|
||||||
|
|
||||||
protected AbstractContainer(TProvider provider, IContainer parent, string name)
|
public virtual bool AllowRecursiveDeletion { get; protected set; }
|
||||||
|
|
||||||
|
protected AbstractContainer(TProvider provider, IContainer parent, string name) : this(name)
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
Provider = provider;
|
Provider = provider;
|
||||||
|
FullName = parent.FullName == null ? name : parent.FullName + Constants.SeparatorChar + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AbstractContainer(string name, string? fullName = null) : this(name)
|
||||||
|
{
|
||||||
|
Provider = this is TProvider provider ? provider : throw new ArgumentException($"This constructor is for {nameof(IContentProvider)}s only");
|
||||||
|
FullName = fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AbstractContainer(string name)
|
||||||
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
FullName = parent.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name;
|
|
||||||
Exceptions = _exceptions.AsReadOnly();
|
Exceptions = _exceptions.AsReadOnly();
|
||||||
|
Provider = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual Task<bool> CanOpenAsync() => Task.FromResult(_exceptions.Count == 0);
|
public virtual Task<bool> CanOpenAsync() => Task.FromResult(_exceptions.Count == 0);
|
||||||
|
|||||||
122
src/Core/FileTime.Core/Providers/ContentProviderBase.cs
Normal file
122
src/Core/FileTime.Core/Providers/ContentProviderBase.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
using AsyncEvent;
|
||||||
|
using FileTime.Core.Models;
|
||||||
|
|
||||||
|
namespace FileTime.Core.Providers
|
||||||
|
{
|
||||||
|
public abstract class ContentProviderBase<T> : AbstractContainer<T>, IContentProvider
|
||||||
|
where T : class, IContentProvider
|
||||||
|
{
|
||||||
|
private readonly object _initializationGuard = new();
|
||||||
|
private bool _initialized;
|
||||||
|
private bool _initializing;
|
||||||
|
private IContainer? _parent;
|
||||||
|
protected IReadOnlyList<IContainer>? RootContainers { get; private set; }
|
||||||
|
public override bool IsExists => true;
|
||||||
|
|
||||||
|
protected ContentProviderBase(
|
||||||
|
string name,
|
||||||
|
string? fullName,
|
||||||
|
string protocol,
|
||||||
|
bool supportsContentStreams)
|
||||||
|
: base(name, fullName)
|
||||||
|
{
|
||||||
|
Protocol = protocol;
|
||||||
|
SupportsContentStreams = supportsContentStreams;
|
||||||
|
|
||||||
|
CanRename = false;
|
||||||
|
CanDelete = SupportsDelete.False;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool SupportsContentStreams { get; }
|
||||||
|
|
||||||
|
public virtual string Protocol { get; }
|
||||||
|
|
||||||
|
public abstract Task<bool> CanHandlePath(string path);
|
||||||
|
public override IContainer? GetParent() => _parent;
|
||||||
|
|
||||||
|
public void SetParent(IContainer parent) => _parent = parent;
|
||||||
|
|
||||||
|
protected async Task AddRootContainer(IContainer newRootContainer)
|
||||||
|
{
|
||||||
|
RootContainers =
|
||||||
|
(await GetContainers())?.Append(newRootContainer).OrderBy(c => c.Name).ToList().AsReadOnly()
|
||||||
|
?? new List<IContainer>() { newRootContainer }.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task AddRootContainers(IEnumerable<IContainer> newRootContainers)
|
||||||
|
{
|
||||||
|
RootContainers =
|
||||||
|
(await GetContainers())?.Concat(newRootContainers).OrderBy(c => c.Name).ToList().AsReadOnly()
|
||||||
|
?? new List<IContainer>(newRootContainers).AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetRootContainers(IEnumerable<IContainer> newRootContainers)
|
||||||
|
=> RootContainers = newRootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
|
||||||
|
|
||||||
|
public override async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
await InitIfNeeded();
|
||||||
|
return RootContainers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
await InitIfNeeded();
|
||||||
|
return new List<IElement>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
await InitIfNeeded();
|
||||||
|
return RootContainers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitIfNeeded()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
lock (_initializationGuard)
|
||||||
|
{
|
||||||
|
if (!_initializing)
|
||||||
|
{
|
||||||
|
_initializing = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Task.Delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_initialized = true;
|
||||||
|
await Init();
|
||||||
|
IsLoaded = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_initializationGuard)
|
||||||
|
{
|
||||||
|
_initializing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual Task Init() { return Task.CompletedTask; }
|
||||||
|
|
||||||
|
public override Task<IEnumerable<IItem>> RefreshItems(CancellationToken token = default) { throw new NotImplementedException($"{nameof(RefreshItems)} should not be called in {nameof(ContentProviderBase<T>)}."); }
|
||||||
|
|
||||||
|
public override Task<bool> IsExistsAsync(string name) => Task.FromResult(RootContainers?.Any(i => i.Name == name) ?? false);
|
||||||
|
|
||||||
|
public override async Task RefreshAsync(CancellationToken token = default) => await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token);
|
||||||
|
public override Task<IContainer> CloneAsync() => Task.FromResult((IContainer)this);
|
||||||
|
public override Task Delete(bool hardDelete = false) => throw new NotSupportedException();
|
||||||
|
public override Task Rename(string newName) => throw new NotSupportedException();
|
||||||
|
public override Task<bool> CanOpenAsync() => Task.FromResult(true);
|
||||||
|
|
||||||
|
|
||||||
|
public override void Unload() { }
|
||||||
|
|
||||||
|
public override void Destroy() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,7 @@ namespace FileTime.Core.Providers
|
|||||||
bool SupportsContentStreams { get; }
|
bool SupportsContentStreams { get; }
|
||||||
string Protocol { get; }
|
string Protocol { get; }
|
||||||
|
|
||||||
Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default);
|
Task<bool> CanHandlePath(string path);
|
||||||
|
|
||||||
bool CanHandlePath(string path);
|
|
||||||
|
|
||||||
void SetParent(IContainer container);
|
void SetParent(IContainer container);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace FileTime.Core.Providers
|
|||||||
|
|
||||||
public bool IsDestroyed => false;
|
public bool IsDestroyed => false;
|
||||||
public bool IsExists => true;
|
public bool IsExists => true;
|
||||||
|
public bool AllowRecursiveDeletion => false;
|
||||||
|
|
||||||
public TopContainer(IEnumerable<IContentProvider> contentProviders)
|
public TopContainer(IEnumerable<IContentProvider> contentProviders)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace FileTime.Core.Timeline
|
|||||||
//FIXME: currently this can be different of the real items NativePath, should be fixed
|
//FIXME: currently this can be different of the real items NativePath, should be fixed
|
||||||
public string? NativePath => FullName;
|
public string? NativePath => FullName;
|
||||||
public bool IsExists => true;
|
public bool IsExists => true;
|
||||||
|
public bool AllowRecursiveDeletion => true;
|
||||||
|
|
||||||
public TimeContainer(string name, IContainer parent, IContentProvider contentProvider, IContentProvider virtualContentProvider, PointInTime pointInTime)
|
public TimeContainer(string name, IContainer parent, IContentProvider contentProvider, IContentProvider virtualContentProvider, PointInTime pointInTime)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,115 +1,30 @@
|
|||||||
using AsyncEvent;
|
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Providers;
|
using FileTime.Core.Providers;
|
||||||
|
|
||||||
namespace FileTime.Core.Timeline
|
namespace FileTime.Core.Timeline
|
||||||
{
|
{
|
||||||
public class TimeProvider : IContentProvider
|
public class TimeProvider : ContentProviderBase<TimeProvider>
|
||||||
{
|
{
|
||||||
private readonly PointInTime _pointInTime;
|
private readonly PointInTime _pointInTime;
|
||||||
|
|
||||||
public bool IsLoaded => true;
|
public TimeProvider(PointInTime pointInTime) : base("time", null, "time2://", false)
|
||||||
|
|
||||||
public AsyncEventHandler Refreshed { get; } = new();
|
|
||||||
|
|
||||||
public string Name => "time";
|
|
||||||
|
|
||||||
public string? FullName => null;
|
|
||||||
public string? NativePath => null;
|
|
||||||
|
|
||||||
public bool IsHidden => false;
|
|
||||||
|
|
||||||
public SupportsDelete CanDelete => SupportsDelete.False;
|
|
||||||
|
|
||||||
public bool CanRename => false;
|
|
||||||
|
|
||||||
public IContentProvider Provider => this;
|
|
||||||
|
|
||||||
public IReadOnlyList<Exception> Exceptions { get; } = new List<Exception>().AsReadOnly();
|
|
||||||
|
|
||||||
public bool SupportsDirectoryLevelSoftDelete => false;
|
|
||||||
|
|
||||||
public bool IsDestroyed => false;
|
|
||||||
public bool SupportsContentStreams => false;
|
|
||||||
|
|
||||||
public string Protocol => "time2://";
|
|
||||||
public bool IsExists => true;
|
|
||||||
|
|
||||||
public TimeProvider(PointInTime pointInTime)
|
|
||||||
{
|
{
|
||||||
_pointInTime = pointInTime;
|
_pointInTime = pointInTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanHandlePath(string path)
|
public override Task<bool> CanHandlePath(string path)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IContainer> CloneAsync() => Task.FromResult((IContainer)this);
|
public override Task<IContainer> CreateContainerAsync(string name)
|
||||||
|
|
||||||
public Task<IContainer> CreateContainerAsync(string name)
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IElement> CreateElementAsync(string name)
|
public override Task<IElement> CreateElementAsync(string name)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Delete(bool hardDelete = false) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IContainer? GetParent() => null;
|
|
||||||
|
|
||||||
public Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> IsExistsAsync(string name)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task RefreshAsync(CancellationToken token = default) => Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task Rename(string newName) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public void SetParent(IContainer container) { }
|
|
||||||
public Task<bool> CanOpenAsync() => Task.FromResult(true);
|
|
||||||
|
|
||||||
public void Destroy() { }
|
|
||||||
|
|
||||||
public void Unload() { }
|
|
||||||
|
|
||||||
public Task<IContentReader> GetContentReaderAsync(IElement element)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IContentWriter> GetContentWriterAsync(IElement element)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +564,7 @@ namespace FileTime.Avalonia.Services
|
|||||||
var path = inputs[0].Value;
|
var path = inputs[0].Value;
|
||||||
foreach (var contentProvider in _contentProviders)
|
foreach (var contentProvider in _contentProviders)
|
||||||
{
|
{
|
||||||
if (contentProvider.CanHandlePath(path))
|
if (await contentProvider.CanHandlePath(path))
|
||||||
{
|
{
|
||||||
var possibleContainer = await contentProvider.GetByPath(path);
|
var possibleContainer = await contentProvider.GetByPath(path);
|
||||||
if (possibleContainer is IContainer container)
|
if (possibleContainer is IContainer container)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ namespace FileTime.Avalonia.Services
|
|||||||
{
|
{
|
||||||
foreach (var contentProvider in _contentProviders)
|
foreach (var contentProvider in _contentProviders)
|
||||||
{
|
{
|
||||||
if (contentProvider.CanHandlePath(tab.Path))
|
if (await contentProvider.CanHandlePath(tab.Path))
|
||||||
{
|
{
|
||||||
pathItem = await contentProvider.GetByPath(tab.Path, true);
|
pathItem = await contentProvider.GetByPath(tab.Path, true);
|
||||||
if (pathItem != null) break;
|
if (pathItem != null) break;
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ namespace FileTime.Avalonia.ViewModels
|
|||||||
|
|
||||||
private async Task<IContainer?> GetContainerForWindowsDrive(DriveInfo drive)
|
private async Task<IContainer?> GetContainerForWindowsDrive(DriveInfo drive)
|
||||||
{
|
{
|
||||||
return (await LocalContentProvider.GetRootContainers()).FirstOrDefault(d => d.Name == drive.Name.TrimEnd(Path.DirectorySeparatorChar));
|
return (await LocalContentProvider.GetContainers())!.FirstOrDefault(d => d.Name == drive.Name.TrimEnd(Path.DirectorySeparatorChar));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IContainer?> GetContainerForLinuxDrive(DriveInfo drive)
|
private async Task<IContainer?> GetContainerForLinuxDrive(DriveInfo drive)
|
||||||
|
|||||||
@@ -1,46 +1,17 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AsyncEvent;
|
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Providers;
|
using FileTime.Core.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace FileTime.Providers.Local
|
namespace FileTime.Providers.Local
|
||||||
{
|
{
|
||||||
public class LocalContentProvider : IContentProvider
|
public class LocalContentProvider : ContentProviderBase<LocalContentProvider>
|
||||||
{
|
{
|
||||||
private readonly ILogger<LocalContentProvider> _logger;
|
private readonly ILogger<LocalContentProvider> _logger;
|
||||||
private IContainer? _parent;
|
|
||||||
|
|
||||||
private readonly IReadOnlyList<IContainer> _rootContainers;
|
|
||||||
private readonly IReadOnlyList<IItem>? _items;
|
|
||||||
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;
|
|
||||||
public bool IsHidden => false;
|
|
||||||
public bool IsLoaded => true;
|
|
||||||
|
|
||||||
public IContentProvider Provider => this;
|
|
||||||
|
|
||||||
public AsyncEventHandler Refreshed { get; } = new();
|
|
||||||
|
|
||||||
public bool IsCaseInsensitive { get; }
|
public bool IsCaseInsensitive { get; }
|
||||||
public SupportsDelete CanDelete => SupportsDelete.False;
|
|
||||||
public bool CanRename => false;
|
|
||||||
public IReadOnlyList<Exception> Exceptions { get; } = new List<Exception>().AsReadOnly();
|
|
||||||
|
|
||||||
public bool SupportsDirectoryLevelSoftDelete => false;
|
|
||||||
|
|
||||||
public bool IsDestroyed => false;
|
|
||||||
public bool SupportsContentStreams => true;
|
|
||||||
public bool IsExists => true;
|
|
||||||
|
|
||||||
public LocalContentProvider(ILogger<LocalContentProvider> logger)
|
public LocalContentProvider(ILogger<LocalContentProvider> logger)
|
||||||
|
: base("local", null, "local://", true)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
@@ -50,8 +21,7 @@ namespace FileTime.Providers.Local
|
|||||||
? new DirectoryInfo("/").GetDirectories()
|
? new DirectoryInfo("/").GetDirectories()
|
||||||
: Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d));
|
: Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d));
|
||||||
|
|
||||||
_rootContainers = rootDirectories.Select(d => new LocalFolder(d, this, this)).OrderBy(d => d.Name).ToList().AsReadOnly();
|
SetRootContainers(rootDirectories.Select(d => new LocalFolder(d, this, this)).OrderBy(d => d.Name));
|
||||||
_items = _rootContainers.Cast<IItem>().ToList().AsReadOnly();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
||||||
@@ -62,11 +32,11 @@ namespace FileTime.Providers.Local
|
|||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && pathParts.Length == 1 && pathParts[0]?.Length == 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);
|
||||||
|
|
||||||
if (rootContainer == null)
|
if (rootContainer == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No root container found with name '{RootContainerName}'", path[0]);
|
_logger.LogWarning("No root container found with name '{RootContainerName}'.", path[0]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,52 +44,18 @@ namespace FileTime.Providers.Local
|
|||||||
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath, acceptDeepestMatch);
|
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath, acceptDeepestMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshAsync(CancellationToken token = default) => await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token);
|
public override Task<IContainer> CreateContainerAsync(string name) => throw new NotSupportedException();
|
||||||
|
public override Task<IElement> CreateElementAsync(string name) => throw new NotSupportedException();
|
||||||
public Task<IContainer> CloneAsync() => Task.FromResult((IContainer)this);
|
|
||||||
|
|
||||||
public IContainer? GetParent() => _parent;
|
|
||||||
public Task<IContainer> CreateContainerAsync(string name) => throw new NotSupportedException();
|
|
||||||
public Task<IElement> CreateElementAsync(string name) => throw new NotSupportedException();
|
|
||||||
public Task<bool> IsExistsAsync(string name) => Task.FromResult(_rootContainers.Any(i => i.Name == name));
|
|
||||||
|
|
||||||
public Task Delete(bool hardDelete = false) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
internal string NormalizePath(string path) => IsCaseInsensitive ? path.ToLower() : path;
|
internal string NormalizePath(string path) => IsCaseInsensitive ? path.ToLower() : path;
|
||||||
|
|
||||||
public bool CanHandlePath(string path)
|
public override async Task<bool> CanHandlePath(string path)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePath(path);
|
var normalizedPath = NormalizePath(path);
|
||||||
Func<IContainer, bool> match = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
Func<IContainer, bool> match = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
? c => normalizedPath.StartsWith(NormalizePath(c.Name))
|
? c => normalizedPath.StartsWith(NormalizePath(c.Name))
|
||||||
: c => normalizedPath.StartsWith(NormalizePath("/" + c.Name));
|
: c => normalizedPath.StartsWith(NormalizePath("/" + c.Name));
|
||||||
return _rootContainers.Any(match);
|
return (await GetContainers())?.Any(match) ?? false;
|
||||||
}
|
|
||||||
|
|
||||||
public void SetParent(IContainer container) => _parent = container;
|
|
||||||
public Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default) => Task.FromResult(_rootContainers);
|
|
||||||
|
|
||||||
public Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default) => Task.FromResult(_items);
|
|
||||||
public Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default) => Task.FromResult((IReadOnlyList<IContainer>?)_rootContainers);
|
|
||||||
public Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default) => Task.FromResult(_elements);
|
|
||||||
|
|
||||||
public Task Rename(string newName) => throw new NotSupportedException();
|
|
||||||
public Task<bool> CanOpenAsync() => Task.FromResult(true);
|
|
||||||
|
|
||||||
public void Destroy()
|
|
||||||
{
|
|
||||||
foreach (var c in _rootContainers)
|
|
||||||
{
|
|
||||||
c.Unload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Unload()
|
|
||||||
{
|
|
||||||
foreach (var c in _rootContainers)
|
|
||||||
{
|
|
||||||
c.Unload();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ namespace FileTime.Providers.Local
|
|||||||
NativePath = Directory.FullName;
|
NativePath = Directory.FullName;
|
||||||
IsHidden = (Directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
IsHidden = (Directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||||
CanRename = true;
|
CanRename = true;
|
||||||
|
AllowRecursiveDeletion = true;
|
||||||
|
|
||||||
//TODO: Linux soft delete
|
//TODO: Linux soft delete
|
||||||
SupportsDirectoryLevelSoftDelete = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
SupportsDirectoryLevelSoftDelete = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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;
|
||||||
@@ -6,67 +5,28 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace FileTime.Providers.Sftp
|
namespace FileTime.Providers.Sftp
|
||||||
{
|
{
|
||||||
public class SftpContentProvider : IContentProvider
|
public class SftpContentProvider : ContentProviderBase<SftpContentProvider>, IContainer
|
||||||
{
|
{
|
||||||
private IContainer? _parent;
|
|
||||||
private readonly IInputInterface _inputInterface;
|
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;
|
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 bool IsExists => true;
|
|
||||||
|
|
||||||
public SftpContentProvider(IInputInterface inputInterface, ILogger<SftpContentProvider> logger)
|
public SftpContentProvider(IInputInterface inputInterface, ILogger<SftpContentProvider> logger)
|
||||||
|
: base("sftp", null, "sftp://", false)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_rootContainers = new List<IContainer>();
|
|
||||||
_items = new List<IItem>();
|
|
||||||
_rootContainersReadOnly = _rootContainers.AsReadOnly();
|
|
||||||
_inputInterface = inputInterface;
|
_inputInterface = inputInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanHandlePath(string path) => path.StartsWith("sftp://");
|
public override Task<bool> CanHandlePath(string path) => Task.FromResult(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)
|
public override async Task<IContainer> CreateContainerAsync(string name)
|
||||||
{
|
{
|
||||||
var container = _rootContainers.Find(c => c.Name == name);
|
var container = RootContainers?.FirstOrDefault(c => c.Name == name);
|
||||||
|
|
||||||
if (container == null)
|
if (container == null)
|
||||||
{
|
{
|
||||||
container = new SftpServer(name, this, _inputInterface);
|
container = new SftpServer(name, this, _inputInterface);
|
||||||
_rootContainers.Add(container);
|
await AddRootContainer(container);
|
||||||
_items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await RefreshAsync();
|
await RefreshAsync();
|
||||||
@@ -76,50 +36,9 @@ namespace FileTime.Providers.Sftp
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IElement> CreateElementAsync(string name)
|
public override Task<IElement> CreateElementAsync(string name) => throw new NotSupportedException();
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Delete(bool hardDelete = false)
|
async Task<IItem?> IContainer.GetByPath(string path, bool acceptDeepestMatch)
|
||||||
{
|
|
||||||
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;
|
if (path == null) return this;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
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;
|
||||||
@@ -8,61 +6,28 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace FileTime.Providers.Smb
|
namespace FileTime.Providers.Smb
|
||||||
{
|
{
|
||||||
public class SmbContentProvider : IContentProvider
|
public class SmbContentProvider : ContentProviderBase<SmbContentProvider>, IContainer
|
||||||
{
|
{
|
||||||
private readonly object _initializationGuard = new();
|
|
||||||
private bool _initialized;
|
|
||||||
private bool _initializing;
|
|
||||||
private IContainer? _parent;
|
|
||||||
private readonly IInputInterface _inputInterface;
|
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 Persistence.PersistenceService _persistenceService;
|
private readonly Persistence.PersistenceService _persistenceService;
|
||||||
private readonly ILogger<SmbContentProvider> _logger;
|
private readonly ILogger<SmbContentProvider> _logger;
|
||||||
|
|
||||||
public string Name { get; } = "smb";
|
|
||||||
public string Protocol { get; } = "smb://";
|
|
||||||
|
|
||||||
public string? FullName => null;
|
|
||||||
public string? NativePath => null;
|
|
||||||
|
|
||||||
public bool IsHidden => false;
|
|
||||||
public bool IsLoaded => true;
|
|
||||||
|
|
||||||
public IContentProvider Provider => this;
|
|
||||||
public SupportsDelete CanDelete => SupportsDelete.False;
|
|
||||||
public bool CanRename => false;
|
|
||||||
public IReadOnlyList<Exception> Exceptions { get; } = new List<Exception>().AsReadOnly();
|
|
||||||
|
|
||||||
public AsyncEventHandler Refreshed { get; } = new();
|
|
||||||
|
|
||||||
public bool SupportsDirectoryLevelSoftDelete => false;
|
|
||||||
|
|
||||||
public bool IsDestroyed => false;
|
|
||||||
public bool SupportsContentStreams => true;
|
|
||||||
public bool IsExists => true;
|
|
||||||
|
|
||||||
public SmbContentProvider(IInputInterface inputInterface, Persistence.PersistenceService persistenceService, ILogger<SmbContentProvider> logger)
|
public SmbContentProvider(IInputInterface inputInterface, Persistence.PersistenceService persistenceService, ILogger<SmbContentProvider> logger)
|
||||||
|
: base("smb", null, "smb://", true)
|
||||||
{
|
{
|
||||||
_rootContainers = new List<IContainer>();
|
|
||||||
_items = new List<IItem>();
|
|
||||||
_rootContainersReadOnly = _rootContainers.AsReadOnly();
|
|
||||||
_inputInterface = inputInterface;
|
_inputInterface = inputInterface;
|
||||||
_persistenceService = persistenceService;
|
_persistenceService = persistenceService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IContainer> CreateContainerAsync(string name)
|
public override async Task<IContainer> CreateContainerAsync(string name)
|
||||||
{
|
{
|
||||||
var container = _rootContainers.Find(c => c.Name == name);
|
var container = RootContainers?.FirstOrDefault(c => c.Name == name);
|
||||||
|
|
||||||
if (container == null)
|
if (container == null)
|
||||||
{
|
{
|
||||||
container = new SmbServer(name, this, _inputInterface);
|
container = new SmbServer(name, this, _inputInterface);
|
||||||
_rootContainers.Add(container);
|
await AddRootContainer(container);
|
||||||
_items = _rootContainers.OrderBy(c => c.Name).ToList().AsReadOnly();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await RefreshAsync();
|
await RefreshAsync();
|
||||||
@@ -72,17 +37,9 @@ namespace FileTime.Providers.Smb
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IElement> CreateElementAsync(string name)
|
public override Task<IElement> CreateElementAsync(string name) => throw new NotSupportedException();
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Delete(bool hardDelete = false)
|
async Task<IItem?> IContainer.GetByPath(string path, bool acceptDeepestMatch)
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
|
|
||||||
{
|
{
|
||||||
if (path == null) return this;
|
if (path == null) return this;
|
||||||
|
|
||||||
@@ -117,45 +74,13 @@ namespace FileTime.Providers.Smb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IContainer? GetParent() => _parent;
|
public override Task<bool> CanHandlePath(string path) => Task.FromResult(path.StartsWith("smb://") || path.StartsWith(@"\\"));
|
||||||
|
|
||||||
public Task<IContainer> CloneAsync() => Task.FromResult((IContainer)this);
|
|
||||||
|
|
||||||
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 bool CanHandlePath(string path) => path.StartsWith("smb://") || path.StartsWith(@"\\");
|
|
||||||
|
|
||||||
public void SetParent(IContainer container) => _parent = container;
|
|
||||||
public Task<IReadOnlyList<IContainer>> GetRootContainers(CancellationToken token = default) => Task.FromResult(_rootContainersReadOnly);
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
|
|
||||||
{
|
|
||||||
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<bool> CanOpenAsync() => Task.FromResult(true);
|
|
||||||
|
|
||||||
public void Destroy() { }
|
|
||||||
|
|
||||||
public void Unload() { }
|
|
||||||
|
|
||||||
public async Task SaveServers()
|
public async Task SaveServers()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _persistenceService.SaveServers(_rootContainers.OfType<SmbServer>());
|
await _persistenceService.SaveServers(RootContainers?.OfType<SmbServer>() ?? Enumerable.Empty<SmbServer>());
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -163,41 +88,10 @@ namespace FileTime.Providers.Smb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Init()
|
protected override 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();
|
var servers = await _persistenceService.LoadServers();
|
||||||
foreach (var server in servers)
|
SetRootContainers(servers.Select(s => new SmbServer(s.Path, this, _inputInterface, s.UserName, s.Password)));
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetNativePathSeparator() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "\\" : "/";
|
public static string GetNativePathSeparator() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "\\" : "/";
|
||||||
|
|||||||
Reference in New Issue
Block a user