ContentReader for Compressed (+read content for provider)
This commit is contained in:
22
src/Core/FileTime.Core.ContentAccess/Helper.cs
Normal file
22
src/Core/FileTime.Core.ContentAccess/Helper.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using FileTime.Core.Models;
|
||||||
|
|
||||||
|
namespace FileTime.Core.ContentAccess;
|
||||||
|
|
||||||
|
internal static class Helper
|
||||||
|
{
|
||||||
|
internal static async Task<ParentElementReaderContext> GetParentElementReaderAsync(
|
||||||
|
IContentAccessorFactory contentAccessorFactory,
|
||||||
|
IElement element,
|
||||||
|
IContentProvider parentContentProvider)
|
||||||
|
{
|
||||||
|
var elementNativePath = element.NativePath!;
|
||||||
|
var supportedPath = (await parentContentProvider.GetSupportedPathPart(elementNativePath))!;
|
||||||
|
var parentElement = (IElement) await parentContentProvider.GetItemByNativePathAsync(supportedPath, element.PointInTime);
|
||||||
|
|
||||||
|
var contentReaderFactory = contentAccessorFactory.GetContentReaderFactory(parentElement.Provider);
|
||||||
|
var reader = await contentReaderFactory.CreateContentReaderAsync(parentElement);
|
||||||
|
var subPath = new NativePath(elementNativePath.Path.Substring(supportedPath.Path.Length + 2 + Constants.SubContentProviderRootContainer.Length));
|
||||||
|
|
||||||
|
return new ParentElementReaderContext(reader, subPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using FileTime.Core.Models;
|
||||||
|
|
||||||
|
namespace FileTime.Core.ContentAccess;
|
||||||
|
|
||||||
|
public record ParentElementReaderContext(IContentReader ContentReader, NativePath SubNativePath);
|
||||||
@@ -6,13 +6,18 @@ namespace FileTime.Core.ContentAccess;
|
|||||||
|
|
||||||
public abstract class SubContentProviderBase : ContentProviderBase
|
public abstract class SubContentProviderBase : ContentProviderBase
|
||||||
{
|
{
|
||||||
|
private readonly IContentAccessorFactory _contentAccessorFactory;
|
||||||
|
|
||||||
public IContentProvider ParentContentProvider { get; }
|
public IContentProvider ParentContentProvider { get; }
|
||||||
|
|
||||||
protected SubContentProviderBase(
|
protected SubContentProviderBase(
|
||||||
|
ITimelessContentProvider timelessContentProvider,
|
||||||
|
IContentAccessorFactory contentAccessorFactory,
|
||||||
IContentProvider parentContentProvider,
|
IContentProvider parentContentProvider,
|
||||||
string name,
|
string name
|
||||||
ITimelessContentProvider timelessContentProvider) : base(name, timelessContentProvider)
|
) : base(name, timelessContentProvider)
|
||||||
{
|
{
|
||||||
|
_contentAccessorFactory = contentAccessorFactory;
|
||||||
ParentContentProvider = parentContentProvider;
|
ParentContentProvider = parentContentProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,4 +45,7 @@ public abstract class SubContentProviderBase : ContentProviderBase
|
|||||||
|
|
||||||
public override async ValueTask<NativePath?> GetSupportedPathPart(NativePath nativePath)
|
public override async ValueTask<NativePath?> GetSupportedPathPart(NativePath nativePath)
|
||||||
=> await ParentContentProvider.GetSupportedPathPart(nativePath);
|
=> await ParentContentProvider.GetSupportedPathPart(nativePath);
|
||||||
|
|
||||||
|
protected async Task<ParentElementReaderContext> GetParentElementReaderAsync(IElement element)
|
||||||
|
=> await Helper.GetParentElementReaderAsync(_contentAccessorFactory, element, ParentContentProvider);
|
||||||
}
|
}
|
||||||
18
src/Core/FileTime.Core.ContentAccess/SubContentReaderBase.cs
Normal file
18
src/Core/FileTime.Core.ContentAccess/SubContentReaderBase.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using FileTime.Core.Models;
|
||||||
|
|
||||||
|
namespace FileTime.Core.ContentAccess;
|
||||||
|
|
||||||
|
public abstract class SubContentReaderBase<TContentProvider> : IContentReaderFactory<TContentProvider> where TContentProvider : IContentProvider
|
||||||
|
{
|
||||||
|
private readonly IContentAccessorFactory _contentAccessorFactory;
|
||||||
|
|
||||||
|
protected SubContentReaderBase(IContentAccessorFactory contentAccessorFactory)
|
||||||
|
{
|
||||||
|
_contentAccessorFactory = contentAccessorFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task<IContentReader> CreateContentReaderAsync(IElement element);
|
||||||
|
|
||||||
|
protected async Task<ParentElementReaderContext> GetParentElementReaderAsync(IElement element, SubContentProviderBase provider)
|
||||||
|
=> await Helper.GetParentElementReaderAsync(_contentAccessorFactory, element, provider.ParentContentProvider);
|
||||||
|
}
|
||||||
@@ -1,22 +1,42 @@
|
|||||||
using System.Text;
|
using FileTime.Core.ContentAccess;
|
||||||
using FileTime.Core.ContentAccess;
|
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Timeline;
|
using FileTime.Core.Timeline;
|
||||||
|
using SharpCompress.Archives;
|
||||||
|
|
||||||
namespace FileTime.Tools.Compression.ContentProvider;
|
namespace FileTime.Tools.Compression.ContentProvider;
|
||||||
|
|
||||||
public class CompressedContentProvider : SubContentProviderBase, ICompressedContentProvider
|
public sealed class CompressedContentProvider : SubContentProviderBase, ICompressedContentProvider
|
||||||
{
|
{
|
||||||
public CompressedContentProvider(
|
public CompressedContentProvider(
|
||||||
IContentProvider parentContentProvider,
|
ITimelessContentProvider timelessContentProvider,
|
||||||
ITimelessContentProvider timelessContentProvider
|
IContentAccessorFactory contentAccessorFactory,
|
||||||
|
IContentProvider parentContentProvider
|
||||||
)
|
)
|
||||||
: base(parentContentProvider, "compression", timelessContentProvider)
|
: base(
|
||||||
|
timelessContentProvider,
|
||||||
|
contentAccessorFactory,
|
||||||
|
parentContentProvider,
|
||||||
|
"compression")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default)
|
public override async Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default)
|
||||||
=> Task.FromResult((byte[]?)"Not implemented..."u8.ToArray());
|
{
|
||||||
|
var parentElementContext = await GetParentElementReaderAsync(element);
|
||||||
|
var reader = parentElementContext.ContentReader;
|
||||||
|
var subPath = parentElementContext.SubNativePath.Path;
|
||||||
|
|
||||||
|
await using var readerStream = reader.AsStream();
|
||||||
|
using var archive = ArchiveFactory.Open(readerStream);
|
||||||
|
|
||||||
|
var entry = archive.Entries.First(e => e.Key == subPath);
|
||||||
|
await using var contentReader= entry.OpenEntryStream();
|
||||||
|
|
||||||
|
var data = new byte[1024 * 1024];
|
||||||
|
var readAsync = await contentReader.ReadAsync(data, cancellationToken);
|
||||||
|
|
||||||
|
return data[..readAsync].ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
public override VolumeSizeInfo? GetVolumeSizeInfo(FullName path) => throw new NotImplementedException();
|
public override VolumeSizeInfo? GetVolumeSizeInfo(FullName path) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -3,17 +3,19 @@ using FileTime.Core.Timeline;
|
|||||||
|
|
||||||
namespace FileTime.Tools.Compression.ContentProvider;
|
namespace FileTime.Tools.Compression.ContentProvider;
|
||||||
|
|
||||||
public class CompressedContentProviderFactory : ICompressedContentProviderFactory
|
public sealed class CompressedContentProviderFactory : ICompressedContentProviderFactory
|
||||||
{
|
{
|
||||||
private readonly ITimelessContentProvider _timelessContentProvider;
|
private readonly ITimelessContentProvider _timelessContentProvider;
|
||||||
|
private readonly IContentAccessorFactory _contentAccessorFactory;
|
||||||
|
|
||||||
public CompressedContentProviderFactory(ITimelessContentProvider timelessContentProvider)
|
public CompressedContentProviderFactory(
|
||||||
|
ITimelessContentProvider timelessContentProvider,
|
||||||
|
IContentAccessorFactory contentAccessorFactory)
|
||||||
{
|
{
|
||||||
_timelessContentProvider = timelessContentProvider;
|
_timelessContentProvider = timelessContentProvider;
|
||||||
|
_contentAccessorFactory = contentAccessorFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICompressedContentProvider Create(IContentProvider parentContentProvider)
|
public ICompressedContentProvider Create(IContentProvider parentContentProvider)
|
||||||
{
|
=> new CompressedContentProvider(_timelessContentProvider, _contentAccessorFactory, parentContentProvider);
|
||||||
return new CompressedContentProvider(parentContentProvider, _timelessContentProvider);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using FileTime.Core.ContentAccess;
|
||||||
|
using SharpCompress.Archives;
|
||||||
|
|
||||||
|
namespace FileTime.Tools.Compression.ContentProvider;
|
||||||
|
|
||||||
|
public sealed class CompressedContentReader : IContentReader
|
||||||
|
{
|
||||||
|
private readonly IDisposable[] _disposables;
|
||||||
|
private readonly Stream _stream;
|
||||||
|
|
||||||
|
public int PreferredBufferSize => 1024 * 1024;
|
||||||
|
public long? Position => _stream.Position;
|
||||||
|
|
||||||
|
public CompressedContentReader(IArchiveEntry entry, IDisposable[] disposables)
|
||||||
|
{
|
||||||
|
_disposables = disposables;
|
||||||
|
_stream = entry.OpenEntryStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_stream.Dispose();
|
||||||
|
foreach (var disposable in _disposables)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> ReadBytesAsync(int bufferSize, int? offset = null)
|
||||||
|
{
|
||||||
|
var data = new byte[bufferSize];
|
||||||
|
var read = await _stream.ReadAsync(data, offset ?? 0, bufferSize);
|
||||||
|
|
||||||
|
return data[..read].ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPosition(long position) => _stream.Seek(position, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
public Stream AsStream() => _stream;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using FileTime.Core.ContentAccess;
|
||||||
|
using FileTime.Core.Models;
|
||||||
|
using SharpCompress.Archives;
|
||||||
|
|
||||||
|
namespace FileTime.Tools.Compression.ContentProvider;
|
||||||
|
|
||||||
|
public sealed class CompressedContentReaderFactory : SubContentReaderBase<CompressedContentProvider>
|
||||||
|
{
|
||||||
|
|
||||||
|
public CompressedContentReaderFactory(IContentAccessorFactory contentAccessorFactory)
|
||||||
|
: base(contentAccessorFactory)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public override async Task<IContentReader> CreateContentReaderAsync(IElement element)
|
||||||
|
{
|
||||||
|
if (element.Provider is not CompressedContentProvider provider)
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Provider must be {nameof(CompressedContentProvider)}, but it is " + element.Provider.GetType(),
|
||||||
|
nameof(element));
|
||||||
|
|
||||||
|
var parentElementReaderContext = await GetParentElementReaderAsync(element, provider);
|
||||||
|
var reader = parentElementReaderContext.ContentReader;
|
||||||
|
var subPath = parentElementReaderContext.SubNativePath;
|
||||||
|
|
||||||
|
var readerStream = reader.AsStream();
|
||||||
|
var archive = ArchiveFactory.Open(readerStream);
|
||||||
|
|
||||||
|
var entry = archive.Entries.First(e => e.Key == subPath.Path);
|
||||||
|
|
||||||
|
var disposables = new IDisposable[] {archive, readerStream};
|
||||||
|
|
||||||
|
return new CompressedContentReader(entry, disposables);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ public static class Startup
|
|||||||
services.AddSingleton<IUserCommandHandler, CompressionUserCommandHandler>();
|
services.AddSingleton<IUserCommandHandler, CompressionUserCommandHandler>();
|
||||||
services.TryAddSingleton<ICompressedContentProviderFactory, CompressedContentProviderFactory>();
|
services.TryAddSingleton<ICompressedContentProviderFactory, CompressedContentProviderFactory>();
|
||||||
services.AddSingleton<ISubContentProvider, CompressedSubContentProvider>();
|
services.AddSingleton<ISubContentProvider, CompressedSubContentProvider>();
|
||||||
|
services.TryAddSingleton<IContentReaderFactory<CompressedContentProvider>, CompressedContentReaderFactory>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace FileTime.Tools.VirtualDiskSources;
|
namespace FileTime.Tools.VirtualDiskSources;
|
||||||
|
|
||||||
public class DiscUtilsInitializer : IPreStartupHandler
|
public sealed class DiscUtilsInitializer : IPreStartupHandler
|
||||||
{
|
{
|
||||||
public Task InitAsync()
|
public Task InitAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,43 +5,40 @@ using FileTime.Core.Timeline;
|
|||||||
|
|
||||||
namespace FileTime.Tools.VirtualDiskSources;
|
namespace FileTime.Tools.VirtualDiskSources;
|
||||||
|
|
||||||
public class VirtualDiskContentProvider : SubContentProviderBase, IVirtualDiskContentProvider
|
public sealed class VirtualDiskContentProvider : SubContentProviderBase, IVirtualDiskContentProvider
|
||||||
{
|
{
|
||||||
private readonly IContentAccessorFactory _contentAccessorFactory;
|
private readonly IContentAccessorFactory _contentAccessorFactory;
|
||||||
|
|
||||||
public VirtualDiskContentProvider(
|
public VirtualDiskContentProvider(
|
||||||
IContentProvider parentContentProvider,
|
|
||||||
ITimelessContentProvider timelessContentProvider,
|
ITimelessContentProvider timelessContentProvider,
|
||||||
IContentAccessorFactory contentAccessorFactory)
|
IContentAccessorFactory contentAccessorFactory,
|
||||||
: base(parentContentProvider, "virtual-disk", timelessContentProvider)
|
IContentProvider parentContentProvider)
|
||||||
|
: base(
|
||||||
|
timelessContentProvider,
|
||||||
|
contentAccessorFactory,
|
||||||
|
parentContentProvider,
|
||||||
|
"virtual-disk"
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_contentAccessorFactory = contentAccessorFactory;
|
_contentAccessorFactory = contentAccessorFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default)
|
public override async Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var elementNativePath = element.NativePath!;
|
var parentElementContext = await GetParentElementReaderAsync(element);
|
||||||
|
var reader = parentElementContext.ContentReader;
|
||||||
var supportedPath = await ParentContentProvider.GetSupportedPathPart(elementNativePath);
|
var subPath = parentElementContext.SubNativePath.Path;
|
||||||
if (supportedPath is null) return null;
|
|
||||||
|
|
||||||
var parentItem = await ParentContentProvider.GetItemByNativePathAsync(supportedPath, element.PointInTime);
|
|
||||||
if (parentItem is not IElement parentElement) return null;
|
|
||||||
|
|
||||||
var contentReaderFactory = _contentAccessorFactory.GetContentReaderFactory(parentElement.Provider);
|
|
||||||
var reader = await contentReaderFactory.CreateContentReaderAsync(parentElement);
|
|
||||||
|
|
||||||
await using var readerStream = reader.AsStream();
|
await using var readerStream = reader.AsStream();
|
||||||
using var discReader = new UdfReader(readerStream);
|
using var discReader = new UdfReader(readerStream);
|
||||||
|
|
||||||
var subPath = elementNativePath.Path.Substring(supportedPath.Path.Length + 2 + Constants.SubContentProviderRootContainer.Length);
|
|
||||||
var fileInfo = discReader.GetFileInfo(subPath);
|
var fileInfo = discReader.GetFileInfo(subPath);
|
||||||
|
|
||||||
await using var contentReader = fileInfo.Open(FileMode.Open, FileAccess.Read);
|
await using var contentReader = fileInfo.Open(FileMode.Open, FileAccess.Read);
|
||||||
var data = new byte[1024 * 1024];
|
var data = new byte[1024 * 1024];
|
||||||
var readAsync = await contentReader.ReadAsync(data, cancellationToken);
|
var readAsync = await contentReader.ReadAsync(data, cancellationToken);
|
||||||
|
|
||||||
return data[0..readAsync].ToArray();
|
return data[..readAsync].ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override VolumeSizeInfo? GetVolumeSizeInfo(FullName path)
|
public override VolumeSizeInfo? GetVolumeSizeInfo(FullName path)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using FileTime.Core.Timeline;
|
|||||||
|
|
||||||
namespace FileTime.Tools.VirtualDiskSources;
|
namespace FileTime.Tools.VirtualDiskSources;
|
||||||
|
|
||||||
public class VirtualDiskContentProviderFactory : IVirtualDiskContentProviderFactory
|
public sealed class VirtualDiskContentProviderFactory : IVirtualDiskContentProviderFactory
|
||||||
{
|
{
|
||||||
private readonly ITimelessContentProvider _timelessContentProvider;
|
private readonly ITimelessContentProvider _timelessContentProvider;
|
||||||
private readonly IContentAccessorFactory _contentAccessorFactory;
|
private readonly IContentAccessorFactory _contentAccessorFactory;
|
||||||
@@ -17,5 +17,5 @@ public class VirtualDiskContentProviderFactory : IVirtualDiskContentProviderFact
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IVirtualDiskContentProvider Create(IContentProvider parentContentProvider)
|
public IVirtualDiskContentProvider Create(IContentProvider parentContentProvider)
|
||||||
=> new VirtualDiskContentProvider(parentContentProvider, _timelessContentProvider, _contentAccessorFactory);
|
=> new VirtualDiskContentProvider(_timelessContentProvider, _contentAccessorFactory, parentContentProvider);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace FileTime.Tools.VirtualDiskSources;
|
namespace FileTime.Tools.VirtualDiskSources;
|
||||||
|
|
||||||
public class VirtualDiskContentReader : IContentReader
|
public sealed class VirtualDiskContentReader : IContentReader
|
||||||
{
|
{
|
||||||
private readonly Stream _stream;
|
private readonly Stream _stream;
|
||||||
private readonly ICollection<IDisposable> _disposables;
|
private readonly ICollection<IDisposable> _disposables;
|
||||||
|
|||||||
@@ -4,37 +4,28 @@ using FileTime.Core.Models;
|
|||||||
|
|
||||||
namespace FileTime.Tools.VirtualDiskSources;
|
namespace FileTime.Tools.VirtualDiskSources;
|
||||||
|
|
||||||
public class VirtualDiskContentReaderFactory : IContentReaderFactory<VirtualDiskContentProvider>
|
public sealed class VirtualDiskContentReaderFactory : SubContentReaderBase<VirtualDiskContentProvider>
|
||||||
{
|
{
|
||||||
private readonly IContentAccessorFactory _contentAccessorFactory;
|
|
||||||
|
|
||||||
public VirtualDiskContentReaderFactory(IContentAccessorFactory contentAccessorFactory)
|
public VirtualDiskContentReaderFactory(IContentAccessorFactory contentAccessorFactory)
|
||||||
|
: base(contentAccessorFactory)
|
||||||
{
|
{
|
||||||
_contentAccessorFactory = contentAccessorFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IContentReader> CreateContentReaderAsync(IElement element)
|
public override async Task<IContentReader> CreateContentReaderAsync(IElement element)
|
||||||
{
|
{
|
||||||
if (element.Provider is not VirtualDiskContentProvider provider)
|
if (element.Provider is not VirtualDiskContentProvider provider)
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
"Provider must be VirtualDiskContentProvider, but it is " + element.Provider.GetType(),
|
$"Provider must be {nameof(VirtualDiskContentProvider)}, but it is " + element.Provider.GetType(),
|
||||||
nameof(element));
|
nameof(element));
|
||||||
|
|
||||||
var elementNativePath = element.NativePath!;
|
var parentElementReaderContext = await GetParentElementReaderAsync(element, provider);
|
||||||
|
var reader = parentElementReaderContext.ContentReader;
|
||||||
var supportedPath = (await provider.ParentContentProvider.GetSupportedPathPart(elementNativePath))!;
|
var subPath = parentElementReaderContext.SubNativePath;
|
||||||
|
|
||||||
var parentElement = (IElement) await provider.ParentContentProvider.GetItemByNativePathAsync(supportedPath, element.PointInTime);
|
|
||||||
|
|
||||||
|
|
||||||
var contentReaderFactory = _contentAccessorFactory.GetContentReaderFactory(parentElement.Provider);
|
|
||||||
var reader = await contentReaderFactory.CreateContentReaderAsync(parentElement);
|
|
||||||
|
|
||||||
var readerStream = reader.AsStream();
|
var readerStream = reader.AsStream();
|
||||||
var discReader = new UdfReader(readerStream);
|
var discReader = new UdfReader(readerStream);
|
||||||
|
|
||||||
var subPath = elementNativePath.Path.Substring(supportedPath.Path.Length + 2 + Constants.SubContentProviderRootContainer.Length);
|
var fileInfo = discReader.GetFileInfo(subPath.Path);
|
||||||
var fileInfo = discReader.GetFileInfo(subPath);
|
|
||||||
|
|
||||||
var contentReader = fileInfo.Open(FileMode.Open, FileAccess.Read);
|
var contentReader = fileInfo.Open(FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using FileTime.Core.Timeline;
|
|||||||
|
|
||||||
namespace FileTime.Tools.VirtualDiskSources;
|
namespace FileTime.Tools.VirtualDiskSources;
|
||||||
|
|
||||||
public class VirtualDiskSubContentProvider : IVirtualDiskSubContentProvider
|
public sealed class VirtualDiskSubContentProvider : IVirtualDiskSubContentProvider
|
||||||
{
|
{
|
||||||
private readonly IContentAccessorFactory _contentAccessorFactory;
|
private readonly IContentAccessorFactory _contentAccessorFactory;
|
||||||
private readonly ITimelessContentProvider _timelessContentProvider;
|
private readonly ITimelessContentProvider _timelessContentProvider;
|
||||||
|
|||||||
Reference in New Issue
Block a user