diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs index d12cbbf..ccb6a5d 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs @@ -1,6 +1,6 @@ -using FileTime.App.Core.Models; using FileTime.App.Core.Models.Enums; using FileTime.Core.Models; +using FileTime.Core.Models.Extensions; using InitableService; namespace FileTime.App.Core.ViewModels; diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 9c214e8..0ba7f6b 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -5,6 +5,7 @@ using FileTime.App.Core.Services.Persistence; using FileTime.Core.Command; using FileTime.Core.Command.CreateContainer; using FileTime.Core.Command.CreateElement; +using FileTime.Core.CommandHandlers; using FileTime.Core.ContentAccess; using FileTime.Core.Services; using FileTime.Core.Timeline; @@ -34,10 +35,11 @@ public static class DependencyInjection return serviceCollection .AddCoreAppServices() .AddLocalServices() - .RegisterCommands(); + .RegisterCommands() + .AddDefaultCommandHandlers(); } - public static IServiceCollection RegisterCommands(this IServiceCollection serviceCollection) + private static IServiceCollection RegisterCommands(this IServiceCollection serviceCollection) { return serviceCollection .AddTransient() diff --git a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj index ce4dcb5..ec7582a 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj +++ b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Core/FileTime.Core.Abstraction/Command/ICommandHandler.cs b/src/Core/FileTime.Core.Abstraction/Command/ICommandHandler.cs index 94b262b..bf370d3 100644 --- a/src/Core/FileTime.Core.Abstraction/Command/ICommandHandler.cs +++ b/src/Core/FileTime.Core.Abstraction/Command/ICommandHandler.cs @@ -1,5 +1,3 @@ -using FileTime.Core.Timeline; - namespace FileTime.Core.Command; public interface ICommandHandler diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/ContentAccessStream.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/ContentAccessStream.cs new file mode 100644 index 0000000..a9efb55 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/ContentAccessStream.cs @@ -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(); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs index 917c080..be6ba41 100644 --- a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentAccessorFactory.cs @@ -4,4 +4,8 @@ public interface IContentAccessorFactory { IItemCreator GetItemCreator() where TContentProvider : IContentProvider; IItemCreator GetItemCreator(IContentProvider provider); + IContentReaderFactory GetContentReaderFactory() where TContentProvider : IContentProvider; + IContentReaderFactory GetContentReaderFactory(IContentProvider provider); + IContentWriterFactory GetContentWriterFactory() where TContentProvider : IContentProvider; + IContentWriterFactory GetContentWriterFactory(IContentProvider provider); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs index 8afe518..5d57fc2 100644 --- a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs @@ -7,6 +7,8 @@ namespace FileTime.Core.ContentAccess; public interface IContentProvider : IContainer, IOnContainerEnter { + bool SupportsContentStreams { get; } + Task GetItemByFullNameAsync( FullName fullName, PointInTime pointInTime, @@ -25,4 +27,5 @@ public interface IContentProvider : IContainer, IOnContainerEnter Task GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default); bool CanHandlePath(NativePath path); + bool CanHandlePath(FullName path); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs new file mode 100644 index 0000000..a38f409 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs @@ -0,0 +1,8 @@ +namespace FileTime.Core.ContentAccess; + +public interface IContentProviderRegistry +{ + IEnumerable ContentProviders { get; } + void AddContentProvider(IContentProvider contentProvider); + void RemoveContentProvider(IContentProvider contentProvider); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentReader.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentReader.cs new file mode 100644 index 0000000..d70d99c --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentReader.cs @@ -0,0 +1,10 @@ +namespace FileTime.Core.ContentAccess; + +public interface IContentReader : IDisposable +{ + int PreferredBufferSize { get; } + long? Position { get; } + + Task ReadBytesAsync(int bufferSize, int? offset = null); + void SetPosition(long position); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentReaderFactory.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentReaderFactory.cs new file mode 100644 index 0000000..8da268a --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentReaderFactory.cs @@ -0,0 +1,12 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.ContentAccess; + +public interface IContentReaderFactory +{ + Task CreateContentReaderAsync(IElement element); +} + +public interface IContentReaderFactory : IContentReaderFactory where TContentProvider : IContentProvider +{ +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentWriter.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentWriter.cs new file mode 100644 index 0000000..83d4e39 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentWriter.cs @@ -0,0 +1,9 @@ +namespace FileTime.Core.ContentAccess; + +public interface IContentWriter : IDisposable +{ + int PreferredBufferSize { get; } + + Task WriteBytesAsync(byte[] data, int? index = null); + Task FlushAsync(); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentWriterFactory.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentWriterFactory.cs new file mode 100644 index 0000000..f571e50 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentWriterFactory.cs @@ -0,0 +1,12 @@ +using FileTime.Core.Models; + +namespace FileTime.Core.ContentAccess; + +public interface IContentWriterFactory +{ + Task CreateContentWriterAsync(IElement element); +} + +public interface IContentWriterFactory : IContentWriterFactory where TContentProvider : IContentProvider +{ +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs b/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs index 1448bfc..cbd2ec7 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs @@ -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; } \ No newline at end of file diff --git a/src/Core/FileTime.Core.CommandHandlers/FileTime.Core.CommandHandlers.csproj b/src/Core/FileTime.Core.CommandHandlers/FileTime.Core.CommandHandlers.csproj new file mode 100644 index 0000000..d88cbe8 --- /dev/null +++ b/src/Core/FileTime.Core.CommandHandlers/FileTime.Core.CommandHandlers.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/src/Core/FileTime.Core.CommandHandlers/Startup.cs b/src/Core/FileTime.Core.CommandHandlers/Startup.cs new file mode 100644 index 0000000..a73e9a5 --- /dev/null +++ b/src/Core/FileTime.Core.CommandHandlers/Startup.cs @@ -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(); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.CommandHandlers/StreamCopyCommandHandler.cs b/src/Core/FileTime.Core.CommandHandlers/StreamCopyCommandHandler.cs new file mode 100644 index 0000000..10c2105 --- /dev/null +++ b/src/Core/FileTime.Core.CommandHandlers/StreamCopyCommandHandler.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs b/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs index 66d1c6c..b3c4aca 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentAccessorFactory.cs @@ -24,4 +24,32 @@ public class ContentAccessorFactory : IContentAccessorFactory return (IItemCreator)_serviceProvider.GetRequiredService(genericType); } + + public IContentReaderFactory GetContentReaderFactory() where TContentProvider : IContentProvider + { + var genericType = typeof(IContentReaderFactory<>).MakeGenericType(typeof(TContentProvider)); + + return (IContentReaderFactory)_serviceProvider.GetRequiredService(genericType); + } + + public IContentReaderFactory GetContentReaderFactory(IContentProvider provider) + { + var genericType = typeof(IContentReaderFactory<>).MakeGenericType(provider.GetType()); + + return (IContentReaderFactory)_serviceProvider.GetRequiredService(genericType); + } + + public IContentWriterFactory GetContentWriterFactory() where TContentProvider : IContentProvider + { + var genericType = typeof(IContentWriterFactory<>).MakeGenericType(typeof(TContentProvider)); + + return (IContentWriterFactory)_serviceProvider.GetRequiredService(genericType); + } + + public IContentWriterFactory GetContentWriterFactory(IContentProvider provider) + { + var genericType = typeof(IContentWriterFactory<>).MakeGenericType(provider.GetType()); + + return (IContentWriterFactory)_serviceProvider.GetRequiredService(genericType); + } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs index c77eaa9..68a67b4 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs @@ -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 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)); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs b/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs new file mode 100644 index 0000000..b9602db --- /dev/null +++ b/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace FileTime.Core.ContentAccess; + +public class ContentProviderRegistry : IContentProviderRegistry +{ + private readonly IServiceProvider _serviceProvider; + private readonly Lazy> _defaultContentProviders; + private readonly List _additionalContentProviders = new(); + + public ContentProviderRegistry(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _defaultContentProviders = new Lazy>(() => serviceProvider.GetServices().ToList()); + } + + public IEnumerable ContentProviders => _defaultContentProviders.Value.Concat(_additionalContentProviders); + + public void AddContentProvider(IContentProvider contentProvider) => _additionalContentProviders.Add(contentProvider); + public void RemoveContentProvider(IContentProvider contentProvider) => _additionalContentProviders.Remove(contentProvider); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs index 7eb1bf3..8823884 100644 --- a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs +++ b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs @@ -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> _contentProviders; + private readonly IContentProviderRegistry _contentProviderRegistry; public BehaviorSubject CurrentPointInTime { get; } = new(PointInTime.Present); - public TimelessContentProvider(IServiceProvider serviceProvider) + public TimelessContentProvider(IContentProviderRegistry contentProviderRegistry) { - _contentProviders = - new Lazy>(() => serviceProvider.GetServices().ToList()); + _contentProviderRegistry = contentProviderRegistry; } public async Task 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 GetItemByNativePathAsync(NativePath nativePath, PointInTime? pointInTime = null) { - foreach (var contentProvider in _contentProviders.Value) + foreach (var contentProvider in _contentProviderRegistry.ContentProviders) { if(!contentProvider.CanHandlePath(nativePath)) continue; diff --git a/src/FileTime.sln b/src/FileTime.sln index 5a05343..bdded2f 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Core.Timeline", "C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Core.ContentAccess", "Core\FileTime.Core.ContentAccess\FileTime.Core.ContentAccess.csproj", "{88BBB541-7306-44AE-95B1-0F8765AF1D4E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Core.CommandHandlers", "Core\FileTime.Core.CommandHandlers\FileTime.Core.CommandHandlers.csproj", "{9B161766-A672-4D59-B591-C68907905158}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -139,6 +141,10 @@ Global {88BBB541-7306-44AE-95B1-0F8765AF1D4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {88BBB541-7306-44AE-95B1-0F8765AF1D4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {88BBB541-7306-44AE-95B1-0F8765AF1D4E}.Release|Any CPU.Build.0 = Release|Any CPU + {9B161766-A672-4D59-B591-C68907905158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B161766-A672-4D59-B591-C68907905158}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B161766-A672-4D59-B591-C68907905158}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B161766-A672-4D59-B591-C68907905158}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -164,6 +170,7 @@ Global {1846BE76-8F68-4FC3-8954-871F14743F0B} = {3324D046-1E05-46B5-B1BA-82910D56B332} {2AC5CAFF-EBDA-4C6E-BCFF-304B651F2906} = {3324D046-1E05-46B5-B1BA-82910D56B332} {88BBB541-7306-44AE-95B1-0F8765AF1D4E} = {3324D046-1E05-46B5-B1BA-82910D56B332} + {9B161766-A672-4D59-B591-C68907905158} = {3324D046-1E05-46B5-B1BA-82910D56B332} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index 5a8aad7..bc2051c 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -2,14 +2,13 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Runtime.InteropServices; using DynamicData; -using FileTime.App.Core.Models; using FileTime.Core.ContentAccess; using FileTime.Core.Enums; using FileTime.Core.Models; +using FileTime.Core.Models.Extensions; using FileTime.Core.Timeline; namespace FileTime.Providers.Local; - public sealed partial class LocalContentProvider : ContentProviderBase, ILocalContentProvider { private readonly ITimelessContentProvider _timelessContentProvider; @@ -21,6 +20,8 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo _timelessContentProvider = timelessContentProvider; _isCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + SupportsContentStreams = true; + RefreshRootDirectories(); Items.OnNext(_rootDirectories.Connect()); @@ -50,11 +51,11 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo { var rootDrive = _rootDirectories .Items - .FirstOrDefault(r => + .FirstOrDefault(r => path.Path.StartsWith( - GetNativePath(r.Path).Path, - _isCaseInsensitive - ? StringComparison.InvariantCultureIgnoreCase + GetNativePath(r.Path).Path, + _isCaseInsensitive + ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture ) ); @@ -74,11 +75,11 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo { if ((path?.Length ?? 0) == 0) { - return Task.FromResult((IItem) this); + return Task.FromResult((IItem)this); } else if (Directory.Exists(path)) { - return Task.FromResult((IItem) DirectoryToContainer( + return Task.FromResult((IItem)DirectoryToContainer( new DirectoryInfo(path!.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar), pointInTime, !itemInitializationSettings.SkipChildInitialization) @@ -86,7 +87,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo } else if (File.Exists(path)) { - return Task.FromResult((IItem) FileToElement(new FileInfo(path), pointInTime)); + return Task.FromResult((IItem)FileToElement(new FileInfo(path), pointInTime)); } var type = forceResolvePathType switch @@ -118,10 +119,10 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo return forceResolvePathType switch { AbsolutePathType.Container => Task.FromResult( - (IItem) CreateEmptyContainer( + (IItem)CreateEmptyContainer( nativePath, pointInTime, - Observable.Return(new List() {innerException}) + Observable.Return(new List() { innerException }) ) ), AbsolutePathType.Element => Task.FromResult(CreateEmptyElement(nativePath)), @@ -236,7 +237,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo SourceCache? result = null; try { - var items = initializeChildren ? (List?) GetItemsByContainer(directoryInfo, pointInTime) : null; + var items = initializeChildren ? (List?)GetItemsByContainer(directoryInfo, pointInTime) : null; if (items != null) { result = new SourceCache(i => i.Path.Path); @@ -245,7 +246,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo } catch (Exception e) { - exceptions.OnNext(new List() {e}); + exceptions.OnNext(new List() { e }); } return Task.FromResult(result?.Connect()); @@ -319,10 +320,10 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo var size = maxLength ?? realFileSize switch { > int.MaxValue => int.MaxValue, - _ => (int) realFileSize + _ => (int)realFileSize }; var buffer = new byte[size]; - await reader.ReadAsync(buffer, 0, size); + await reader.ReadAsync(buffer.AsMemory(0, size), cancellationToken); return buffer; } diff --git a/src/Providers/FileTime.Providers.Local/LocalContentReader.cs b/src/Providers/FileTime.Providers.Local/LocalContentReader.cs new file mode 100644 index 0000000..a69bb3d --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalContentReader.cs @@ -0,0 +1,71 @@ +using FileTime.Core.ContentAccess; + +namespace FileTime.Providers.Local; + +public class LocalContentReader : IContentReader +{ + private readonly FileStream _readerStream; + private readonly BinaryReader _binaryReader; + private bool _disposed; + + public int PreferredBufferSize => 1024 * 1024; + public long? Position { get; private set; } + + public LocalContentReader(FileStream readerStream) + { + _readerStream = readerStream; + _binaryReader = new BinaryReader(_readerStream); + } + + public Task ReadBytesAsync(int bufferSize, int? offset = null) + { + var max = bufferSize > 0 && bufferSize < PreferredBufferSize ? bufferSize : PreferredBufferSize; + + if (offset != null) + { + if (Position == null) Position = 0; + var buffer = new byte[max]; + var bytesRead = _binaryReader.Read(buffer, offset.Value, max); + Position += bytesRead; + + if (buffer.Length != bytesRead) + { + Array.Resize(ref buffer, bytesRead); + } + return Task.FromResult(buffer); + } + else + { + return Task.FromResult(_binaryReader.ReadBytes(max)); + } + } + + public void SetPosition(long position) + { + Position = position; + } + + ~LocalContentReader() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _readerStream.Dispose(); + _binaryReader.Dispose(); + } + } + _disposed = true; + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalContentReaderFactory.cs b/src/Providers/FileTime.Providers.Local/LocalContentReaderFactory.cs new file mode 100644 index 0000000..f15d38e --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalContentReaderFactory.cs @@ -0,0 +1,10 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Models; + +namespace FileTime.Providers.Local; + +public class LocalContentReaderFactory : IContentReaderFactory +{ + public Task CreateContentReaderAsync(IElement element) + => Task.FromResult((IContentReader)new LocalContentReader(File.OpenRead(element.NativePath!.Path))); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalContentWriter.cs b/src/Providers/FileTime.Providers.Local/LocalContentWriter.cs new file mode 100644 index 0000000..a8d72eb --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalContentWriter.cs @@ -0,0 +1,60 @@ +using FileTime.Core.ContentAccess; + +namespace FileTime.Providers.Local; + +public class LocalContentWriter : IContentWriter +{ + private readonly FileStream _writerStream; + private readonly BinaryWriter _binaryWriter; + private bool disposed; + public int PreferredBufferSize => 1024 * 1024; + + public LocalContentWriter(FileStream writerStream) + { + _writerStream = writerStream; + _binaryWriter = new BinaryWriter(_writerStream); + } + + public Task WriteBytesAsync(byte[] data, int? index = null) + { + if (index != null) + { + _binaryWriter.Write(data, index.Value, data.Length); + } + else + { + _binaryWriter.Write(data); + } + return Task.CompletedTask; + } + + public Task FlushAsync() + { + _binaryWriter.Flush(); + return Task.CompletedTask; + } + + ~LocalContentWriter() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + _writerStream.Dispose(); + _binaryWriter.Dispose(); + } + } + disposed = true; + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalContentWriterFactory.cs b/src/Providers/FileTime.Providers.Local/LocalContentWriterFactory.cs new file mode 100644 index 0000000..71d190b --- /dev/null +++ b/src/Providers/FileTime.Providers.Local/LocalContentWriterFactory.cs @@ -0,0 +1,10 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Models; + +namespace FileTime.Providers.Local; + +public class LocalContentWriterFactory : IContentWriterFactory +{ + public Task CreateContentWriterAsync(IElement element) + => Task.FromResult((IContentWriter)new LocalContentWriter(File.OpenRead(element.NativePath!.Path))); +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/Startup.cs b/src/Providers/FileTime.Providers.Local/Startup.cs index c804093..8270427 100644 --- a/src/Providers/FileTime.Providers.Local/Startup.cs +++ b/src/Providers/FileTime.Providers.Local/Startup.cs @@ -12,6 +12,10 @@ public static class Startup serviceCollection.TryAddSingleton(sp => sp.GetRequiredService()); serviceCollection.TryAddSingleton, LocalItemCreator>(); serviceCollection.TryAddSingleton>(sp => sp.GetRequiredService>()); + serviceCollection.TryAddSingleton, LocalContentReaderFactory>(); + serviceCollection.TryAddSingleton>(sp => sp.GetRequiredService>()); + serviceCollection.TryAddSingleton, LocalContentWriterFactory>(); + serviceCollection.TryAddSingleton>(sp => sp.GetRequiredService>()); return serviceCollection; } } \ No newline at end of file