Content Reader/Writer, StreamCopyCommandHandler

This commit is contained in:
2022-06-09 06:59:53 +02:00
parent e947282d7b
commit 6d9bf7ab32
27 changed files with 494 additions and 27 deletions

View File

@@ -1,5 +1,3 @@
using FileTime.Core.Timeline;
namespace FileTime.Core.Command;
public interface ICommandHandler

View File

@@ -0,0 +1,80 @@
namespace FileTime.Core.ContentAccess;
public class ContentAccessStream : Stream
{
private readonly IContentReader? _contentReader;
private readonly IContentWriter? _contentWriter;
public override bool CanRead => _contentReader != null;
public override bool CanSeek => _contentReader != null;
public override bool CanWrite => _contentWriter != null;
public override long Length => throw new NotImplementedException();
public override long Position
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public ContentAccessStream(IContentReader contentReader)
{
_contentReader = contentReader;
}
public ContentAccessStream(IContentWriter contentWriter)
{
_contentWriter = contentWriter;
}
public override void Flush()
{
if (_contentWriter == null) throw new NotSupportedException();
Task.Run(async () => await _contentWriter.FlushAsync()).Wait();
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_contentReader == null) throw new IOException("This stream is not readable");
var dataTask = Task.Run(async () => await _contentReader.ReadBytesAsync(count, offset));
dataTask.Wait();
var data = dataTask.Result;
if (data.Length > count) throw new Exception("More bytes has been read than requested");
Array.Copy(data, buffer, data.Length);
return data.Length;
}
public override long Seek(long offset, SeekOrigin origin)
{
if (_contentReader == null) throw new NotSupportedException();
var newPosition = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _contentReader.Position ?? 0 + offset,
_ => throw new NotSupportedException()
};
_contentReader.SetPosition(newPosition);
return newPosition;
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
if (_contentWriter == null) throw new NotSupportedException();
var data = buffer;
if (buffer.Length != count)
{
data = new byte[count];
Array.Copy(buffer, data, count);
}
Task.Run(async () => await _contentWriter.WriteBytesAsync(data, offset)).Wait();
}
}

View File

@@ -4,4 +4,8 @@ public interface IContentAccessorFactory
{
IItemCreator<TContentProvider> GetItemCreator<TContentProvider>() where TContentProvider : IContentProvider;
IItemCreator GetItemCreator(IContentProvider provider);
IContentReaderFactory<TContentProvider> GetContentReaderFactory<TContentProvider>() where TContentProvider : IContentProvider;
IContentReaderFactory GetContentReaderFactory(IContentProvider provider);
IContentWriterFactory<TContentProvider> GetContentWriterFactory<TContentProvider>() where TContentProvider : IContentProvider;
IContentWriterFactory GetContentWriterFactory(IContentProvider provider);
}

View File

@@ -7,6 +7,8 @@ namespace FileTime.Core.ContentAccess;
public interface IContentProvider : IContainer, IOnContainerEnter
{
bool SupportsContentStreams { get; }
Task<IItem> GetItemByFullNameAsync(
FullName fullName,
PointInTime pointInTime,
@@ -25,4 +27,5 @@ public interface IContentProvider : IContainer, IOnContainerEnter
Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default);
bool CanHandlePath(NativePath path);
bool CanHandlePath(FullName path);
}

View File

@@ -0,0 +1,8 @@
namespace FileTime.Core.ContentAccess;
public interface IContentProviderRegistry
{
IEnumerable<IContentProvider> ContentProviders { get; }
void AddContentProvider(IContentProvider contentProvider);
void RemoveContentProvider(IContentProvider contentProvider);
}

View File

@@ -0,0 +1,10 @@
namespace FileTime.Core.ContentAccess;
public interface IContentReader : IDisposable
{
int PreferredBufferSize { get; }
long? Position { get; }
Task<byte[]> ReadBytesAsync(int bufferSize, int? offset = null);
void SetPosition(long position);
}

View File

@@ -0,0 +1,12 @@
using FileTime.Core.Models;
namespace FileTime.Core.ContentAccess;
public interface IContentReaderFactory
{
Task<IContentReader> CreateContentReaderAsync(IElement element);
}
public interface IContentReaderFactory<in TContentProvider> : IContentReaderFactory where TContentProvider : IContentProvider
{
}

View File

@@ -0,0 +1,9 @@
namespace FileTime.Core.ContentAccess;
public interface IContentWriter : IDisposable
{
int PreferredBufferSize { get; }
Task WriteBytesAsync(byte[] data, int? index = null);
Task FlushAsync();
}

View File

@@ -0,0 +1,12 @@
using FileTime.Core.Models;
namespace FileTime.Core.ContentAccess;
public interface IContentWriterFactory
{
Task<IContentWriter> CreateContentWriterAsync(IElement element);
}
public interface IContentWriterFactory<in TContentProvider> : IContentWriterFactory where TContentProvider : IContentProvider
{
}

View File

@@ -53,4 +53,7 @@ public class AbsolutePath
public AbsolutePath GetChild(string childName, AbsolutePathType type)
=> new (TimelessProvider, PointInTime, Path.GetChild(childName), type);
public AbsolutePath? GetParent()
=> Path.GetParent() is { } parentFullName ? new (TimelessProvider, PointInTime, parentFullName, AbsolutePathType.Container) : null;
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.Core.Command\FileTime.Core.Command.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
using FileTime.Core.Command;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Core.CommandHandlers;
public static class Startup
{
public static IServiceCollection AddDefaultCommandHandlers(this IServiceCollection serviceCollection)
{
return serviceCollection
.AddSingleton<ICommandHandler, StreamCopyCommandHandler>();
}
}

View File

@@ -0,0 +1,85 @@
using FileTime.Core.Command;
using FileTime.Core.Command.Copy;
using FileTime.Core.ContentAccess;
using FileTime.Core.Extensions;
using FileTime.Core.Models;
namespace FileTime.Core.CommandHandlers;
public class StreamCopyCommandHandler : ICommandHandler
{
private readonly IContentProviderRegistry _contentProviderRegistry;
private readonly IContentAccessorFactory _contentAccessorFactory;
public StreamCopyCommandHandler(
IContentProviderRegistry contentProviderRegistry,
IContentAccessorFactory contentAccessorFactory)
{
_contentProviderRegistry = contentProviderRegistry;
_contentAccessorFactory = contentAccessorFactory;
}
public bool CanHandle(ICommand command)
{
if (command is not CopyCommand copyCommand) return false;
var targetSupportsContentStream =
_contentProviderRegistry
.ContentProviders
.FirstOrDefault(p => p.CanHandlePath(copyCommand.Target!))
?.SupportsContentStreams ?? false;
var allSourcesSupportsContentStream =
copyCommand
.Sources
.Select(s =>
_contentProviderRegistry
.ContentProviders
.FirstOrDefault(p => p.CanHandlePath(s))
)
.All(p => p?.SupportsContentStreams ?? false);
return targetSupportsContentStream && allSourcesSupportsContentStream;
}
public async Task ExecuteAsync(ICommand command)
{
if (command is not CopyCommand copyCommand) throw new ArgumentException($"Can not execute command of type '{command.GetType()}'.");
await copyCommand.ExecuteAsync(CopyElement);
}
public async Task CopyElement(AbsolutePath sourcePath, AbsolutePath targetPath, CopyCommandContext copyCommandContext)
{
var parent = (IContainer?) (await targetPath.GetParent()!.ResolveAsync())!;
var elementName = targetPath.Path;
var parentChildren = await parent.Items.GetItemsAsync();
if (parentChildren!.All(e => e.Path.GetName() != elementName.GetName()))
{
var itemCreator = _contentAccessorFactory.GetItemCreator(parent.Provider);
await itemCreator.CreateElementAsync(parent.Provider, elementName);
}
var source = (IElement?) (await sourcePath.ResolveAsync())!;
var target = (IElement?) (await targetPath.ResolveAsync())!;
using var reader = await _contentAccessorFactory.GetContentReaderFactory(source.Provider).CreateContentReaderAsync(source);
using var writer = await _contentAccessorFactory.GetContentWriterFactory(target.Provider).CreateContentWriterAsync(target);
byte[] dataRead;
var currentProgress = 0L;
do
{
dataRead = await reader.ReadBytesAsync(writer.PreferredBufferSize);
if (dataRead.Length > 0)
{
await writer.WriteBytesAsync(dataRead);
await writer.FlushAsync();
currentProgress += dataRead.LongLength;
copyCommandContext.CurrentProgress?.SetProgress(currentProgress);
await copyCommandContext.UpdateProgress();
}
} while (dataRead.Length > 0);
}
}

View File

@@ -24,4 +24,32 @@ public class ContentAccessorFactory : IContentAccessorFactory
return (IItemCreator)_serviceProvider.GetRequiredService(genericType);
}
public IContentReaderFactory<TContentProvider> GetContentReaderFactory<TContentProvider>() where TContentProvider : IContentProvider
{
var genericType = typeof(IContentReaderFactory<>).MakeGenericType(typeof(TContentProvider));
return (IContentReaderFactory<TContentProvider>)_serviceProvider.GetRequiredService(genericType);
}
public IContentReaderFactory GetContentReaderFactory(IContentProvider provider)
{
var genericType = typeof(IContentReaderFactory<>).MakeGenericType(provider.GetType());
return (IContentReaderFactory)_serviceProvider.GetRequiredService(genericType);
}
public IContentWriterFactory<TContentProvider> GetContentWriterFactory<TContentProvider>() where TContentProvider : IContentProvider
{
var genericType = typeof(IContentWriterFactory<>).MakeGenericType(typeof(TContentProvider));
return (IContentWriterFactory<TContentProvider>)_serviceProvider.GetRequiredService(genericType);
}
public IContentWriterFactory GetContentWriterFactory(IContentProvider provider)
{
var genericType = typeof(IContentWriterFactory<>).MakeGenericType(provider.GetType());
return (IContentWriterFactory)_serviceProvider.GetRequiredService(genericType);
}
}

View File

@@ -61,6 +61,8 @@ public abstract class ContentProviderBase : IContentProvider
public virtual Task OnEnter() => Task.CompletedTask;
public virtual bool SupportsContentStreams { get; protected set; }
public virtual async Task<IItem> GetItemByFullNameAsync(FullName fullName,
PointInTime pointInTime,
bool forceResolve = false,
@@ -83,4 +85,5 @@ public abstract class ContentProviderBase : IContentProvider
CancellationToken cancellationToken = default);
public abstract bool CanHandlePath(NativePath path);
public bool CanHandlePath(FullName path) => CanHandlePath(GetNativePath(path));
}

View File

@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Core.ContentAccess;
public class ContentProviderRegistry : IContentProviderRegistry
{
private readonly IServiceProvider _serviceProvider;
private readonly Lazy<IList<IContentProvider>> _defaultContentProviders;
private readonly List<IContentProvider> _additionalContentProviders = new();
public ContentProviderRegistry(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_defaultContentProviders = new Lazy<IList<IContentProvider>>(() => serviceProvider.GetServices<IContentProvider>().ToList());
}
public IEnumerable<IContentProvider> ContentProviders => _defaultContentProviders.Value.Concat(_additionalContentProviders);
public void AddContentProvider(IContentProvider contentProvider) => _additionalContentProviders.Add(contentProvider);
public void RemoveContentProvider(IContentProvider contentProvider) => _additionalContentProviders.Remove(contentProvider);
}

View File

@@ -2,20 +2,18 @@ using System.Reactive.Subjects;
using FileTime.Core.ContentAccess;
using FileTime.Core.Enums;
using FileTime.Core.Models;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Core.Timeline;
public class TimelessContentProvider : ITimelessContentProvider
{
private readonly Lazy<List<IContentProvider>> _contentProviders;
private readonly IContentProviderRegistry _contentProviderRegistry;
public BehaviorSubject<PointInTime> CurrentPointInTime { get; } = new(PointInTime.Present);
public TimelessContentProvider(IServiceProvider serviceProvider)
public TimelessContentProvider(IContentProviderRegistry contentProviderRegistry)
{
_contentProviders =
new Lazy<List<IContentProvider>>(() => serviceProvider.GetServices<IContentProvider>().ToList());
_contentProviderRegistry = contentProviderRegistry;
}
public async Task<IItem> GetItemByFullNameAsync(FullName fullName, PointInTime? pointInTime,
@@ -25,7 +23,7 @@ public class TimelessContentProvider : ITimelessContentProvider
{
//TODO time modifications
var contentProviderName = fullName.Path.Split(Constants.SeparatorChar).FirstOrDefault();
var contentProvider = _contentProviders.Value.FirstOrDefault(p => p.Name == contentProviderName);
var contentProvider = _contentProviderRegistry.ContentProviders.FirstOrDefault(p => p.Name == contentProviderName);
if (contentProvider is null)
throw new Exception($"No content provider is found for name '{contentProviderName}'");
@@ -37,7 +35,7 @@ public class TimelessContentProvider : ITimelessContentProvider
public async Task<IItem?> GetItemByNativePathAsync(NativePath nativePath, PointInTime? pointInTime = null)
{
foreach (var contentProvider in _contentProviders.Value)
foreach (var contentProvider in _contentProviderRegistry.ContentProviders)
{
if(!contentProvider.CanHandlePath(nativePath)) continue;