mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Split DI services into focused workflow components
This commit is contained in:
@@ -149,8 +149,20 @@ Roots and constructor dependencies use internal interfaces generated by Pure.DI'
|
||||
for composition binding and supplements the sync signatures produced by Zomp, since source
|
||||
generators cannot inspect one another's generated members.
|
||||
Public extension classes remain beside their public abstractions. Simple overloads supply defaults
|
||||
locally; extraction and filesystem workflows delegate to `ArchiveExtractor` and `ArchiveFileWriter`,
|
||||
including reader extraction, writable-archive entry creation, and saving archives to files.
|
||||
locally; workflows delegate directly to the appropriate typed root:
|
||||
|
||||
| Services | Responsibility |
|
||||
| --- | --- |
|
||||
| `ArchiveService`, `ArchiveDetector`, `ArchiveInspector` | Open/create archives, recognize formats/select factories, and collect metadata respectively |
|
||||
| `ArchiveExtractor` | Whole-archive extraction, including sequential solid-archive decoding |
|
||||
| `ReaderExtractor`, `ArchiveEntryExtractor` | Forward-only reader extraction and individual archive-entry extraction |
|
||||
| `WriterFileOperations`, `WritableArchiveFileOperations` | Writer filesystem operations and writable-archive entry ownership/saving respectively |
|
||||
| `ReaderService`, `WriterService` | Open forward-only readers and writers |
|
||||
|
||||
The inspector depends on detection and opening, not the other way around. Archive extraction
|
||||
depends on the reader and entry extractors. Shared stream decoration lives in the dependency-free
|
||||
`ExtractionStreamHelper`; inspection accumulators remain fresh per-operation objects, never
|
||||
singleton services. There is no aggregate service routing layer.
|
||||
|
||||
An injected `FactoryCatalog` supplies factories and TAR wrappers. The default catalog reads
|
||||
through to `Factory.Factories` and the existing `TarWrapper.Wrappers` array rather than freezing
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace SharpCompress.Archives;
|
||||
public static partial class ArchiveFactory
|
||||
{
|
||||
private static IArchiveService Service => LibraryDefaults.Composition.Archives;
|
||||
private static IArchiveDetector Detector => LibraryDefaults.Composition.Detector;
|
||||
private static IArchiveInspector Inspector => LibraryDefaults.Composition.Inspector;
|
||||
|
||||
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) =>
|
||||
Service.OpenArchive(stream, readerOptions);
|
||||
@@ -71,81 +73,86 @@ public static partial class ArchiveFactory
|
||||
string sourceArchive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
) => Service.WriteToDirectory(sourceArchive, destinationDirectory, options);
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteToDirectory(
|
||||
sourceArchive,
|
||||
destinationDirectory,
|
||||
options
|
||||
);
|
||||
|
||||
public static T FindFactory<T>(string filePath)
|
||||
where T : IFactory => Service.FindFactory<T>(filePath);
|
||||
where T : IFactory => Detector.FindFactory<T>(filePath);
|
||||
|
||||
public static T FindFactory<T>(FileInfo finfo)
|
||||
where T : IFactory => Service.FindFactory<T>(finfo);
|
||||
where T : IFactory => Detector.FindFactory<T>(finfo);
|
||||
|
||||
public static T FindFactory<T>(Stream stream)
|
||||
where T : IFactory => Service.FindFactory<T>(stream);
|
||||
where T : IFactory => Detector.FindFactory<T>(stream);
|
||||
|
||||
internal static ValueTask<T> FindFactoryAsync<T>(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where T : IFactory => Service.FindFactoryAsync<T>(filePath, cancellationToken);
|
||||
where T : IFactory => Detector.FindFactoryAsync<T>(filePath, cancellationToken);
|
||||
|
||||
internal static ValueTask<T> FindFactoryAsync<T>(
|
||||
FileInfo fileInfo,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where T : IFactory => Service.FindFactoryAsync<T>(fileInfo, cancellationToken);
|
||||
where T : IFactory => Detector.FindFactoryAsync<T>(fileInfo, cancellationToken);
|
||||
|
||||
internal static ValueTask<T> FindFactoryAsync<T>(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where T : IFactory => Service.FindFactoryAsync<T>(stream, cancellationToken);
|
||||
where T : IFactory => Detector.FindFactoryAsync<T>(stream, cancellationToken);
|
||||
|
||||
public static bool IsArchive(string filePath, out ArchiveType? type) =>
|
||||
Service.IsArchive(filePath, out type);
|
||||
Detector.IsArchive(filePath, out type);
|
||||
|
||||
public static bool IsArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
out ArchiveType? type
|
||||
) => Service.IsArchive(filePath, readerOptions, out type);
|
||||
) => Detector.IsArchive(filePath, readerOptions, out type);
|
||||
|
||||
public static bool IsArchive(Stream stream, out ArchiveType? type) =>
|
||||
Service.IsArchive(stream, out type);
|
||||
Detector.IsArchive(stream, out type);
|
||||
|
||||
public static bool IsArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
out ArchiveType? type
|
||||
) => Service.IsArchive(stream, readerOptions, out type);
|
||||
) => Detector.IsArchive(stream, readerOptions, out type);
|
||||
|
||||
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.IsArchiveAsync(filePath, cancellationToken);
|
||||
) => Detector.IsArchiveAsync(filePath, cancellationToken);
|
||||
|
||||
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.IsArchiveAsync(filePath, readerOptions, cancellationToken);
|
||||
) => Detector.IsArchiveAsync(filePath, readerOptions, cancellationToken);
|
||||
|
||||
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.IsArchiveAsync(stream, cancellationToken);
|
||||
) => Detector.IsArchiveAsync(stream, cancellationToken);
|
||||
|
||||
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.IsArchiveAsync(stream, readerOptions, cancellationToken);
|
||||
) => Detector.IsArchiveAsync(stream, readerOptions, cancellationToken);
|
||||
|
||||
/// <summary>Identify the archive without enumerating its entries.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
public static ValueTask<ArchiveDetection?> DetectArchiveAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.DetectArchiveAsync(filePath, cancellationToken);
|
||||
) => Detector.DetectArchiveAsync(filePath, cancellationToken);
|
||||
|
||||
/// <summary>Identify the archive without enumerating its entries.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
@@ -153,14 +160,14 @@ public static partial class ArchiveFactory
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.DetectArchiveAsync(filePath, readerOptions, cancellationToken);
|
||||
) => Detector.DetectArchiveAsync(filePath, readerOptions, cancellationToken);
|
||||
|
||||
/// <summary>Identify the archive without enumerating its entries. Restore the stream position.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
public static ValueTask<ArchiveDetection?> DetectArchiveAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.DetectArchiveAsync(stream, cancellationToken);
|
||||
) => Detector.DetectArchiveAsync(stream, cancellationToken);
|
||||
|
||||
/// <summary>Identify the archive without enumerating its entries. Restore the stream position.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
@@ -168,14 +175,14 @@ public static partial class ArchiveFactory
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.DetectArchiveAsync(stream, readerOptions, cancellationToken);
|
||||
) => Detector.DetectArchiveAsync(stream, readerOptions, cancellationToken);
|
||||
|
||||
/// <summary>Inspect archive metadata.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
public static ValueTask<ArchiveInformation?> InspectArchiveAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.InspectArchiveAsync(filePath, cancellationToken);
|
||||
) => Inspector.InspectArchiveAsync(filePath, cancellationToken);
|
||||
|
||||
/// <summary>Inspect archive metadata.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
@@ -183,14 +190,14 @@ public static partial class ArchiveFactory
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.InspectArchiveAsync(filePath, readerOptions, cancellationToken);
|
||||
) => Inspector.InspectArchiveAsync(filePath, readerOptions, cancellationToken);
|
||||
|
||||
/// <summary>Inspect archive metadata, leaving the stream open at its original position.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
public static ValueTask<ArchiveInformation?> InspectArchiveAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.InspectArchiveAsync(stream, cancellationToken);
|
||||
) => Inspector.InspectArchiveAsync(stream, cancellationToken);
|
||||
|
||||
/// <summary>Inspect archive metadata, leaving the stream open at its original position.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
@@ -198,7 +205,7 @@ public static partial class ArchiveFactory
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.InspectArchiveAsync(stream, readerOptions, cancellationToken);
|
||||
) => Inspector.InspectArchiveAsync(stream, readerOptions, cancellationToken);
|
||||
|
||||
/// <summary>Inspect multipart archive metadata.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
@@ -206,7 +213,7 @@ public static partial class ArchiveFactory
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.InspectArchiveAsync(fileInfos, readerOptions, cancellationToken);
|
||||
) => Inspector.InspectArchiveAsync(fileInfos, readerOptions, cancellationToken);
|
||||
|
||||
/// <summary>Inspect multipart archive metadata, leaving the streams open at their original positions.</summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
@@ -214,7 +221,7 @@ public static partial class ArchiveFactory
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Service.InspectArchiveAsync(streams, readerOptions, cancellationToken);
|
||||
) => Inspector.InspectArchiveAsync(streams, readerOptions, cancellationToken);
|
||||
|
||||
public static IEnumerable<string> GetFileParts(string part1) => Service.GetFileParts(part1);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace SharpCompress.Archives;
|
||||
public static partial class IArchiveEntryExtensions
|
||||
{
|
||||
// Give the sync generator a known receiver type before Pure.DI generates the root property.
|
||||
private static IArchiveExtractor Extractor => LibraryDefaults.Composition.Extractor;
|
||||
private static IArchiveEntryExtractor Extractor => LibraryDefaults.Composition.EntryExtractor;
|
||||
|
||||
/// <param name="archiveEntry">The archive entry to extract.</param>
|
||||
extension(IArchiveEntry archiveEntry)
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class IWritableArchiveExtensions
|
||||
string searchPattern = "*.*",
|
||||
SearchOption searchOption = SearchOption.AllDirectories
|
||||
) =>
|
||||
LibraryDefaults.Composition.FileWriter.AddAllFromDirectory(
|
||||
LibraryDefaults.Composition.ArchiveFiles.AddAllFromDirectory(
|
||||
writableArchive,
|
||||
directoryPath,
|
||||
searchPattern,
|
||||
@@ -32,7 +32,7 @@ public static class IWritableArchiveExtensions
|
||||
) => writableArchive.AddEntry(key, source, false, size, modified);
|
||||
|
||||
public IArchiveEntry AddEntry(string key, FileInfo fileInfo) =>
|
||||
LibraryDefaults.Composition.FileWriter.AddEntry(writableArchive, key, fileInfo);
|
||||
LibraryDefaults.Composition.ArchiveFiles.AddEntry(writableArchive, key, fileInfo);
|
||||
}
|
||||
|
||||
public static void SaveTo<TOptions>(
|
||||
@@ -48,5 +48,5 @@ public static class IWritableArchiveExtensions
|
||||
TOptions options
|
||||
)
|
||||
where TOptions : IWriterOptions =>
|
||||
LibraryDefaults.Composition.FileWriter.SaveTo(writableArchive, fileInfo, options);
|
||||
LibraryDefaults.Composition.ArchiveFiles.SaveTo(writableArchive, fileInfo, options);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public static class IWritableAsyncArchiveExtensions
|
||||
string searchPattern = "*.*",
|
||||
SearchOption searchOption = SearchOption.AllDirectories
|
||||
) =>
|
||||
LibraryDefaults.Composition.FileWriter.AddAllFromDirectoryAsync(
|
||||
LibraryDefaults.Composition.ArchiveFiles.AddAllFromDirectoryAsync(
|
||||
writableArchive,
|
||||
directoryPath,
|
||||
searchPattern,
|
||||
@@ -34,7 +34,7 @@ public static class IWritableAsyncArchiveExtensions
|
||||
) => writableArchive.AddEntryAsync(key, source, false, size, modified);
|
||||
|
||||
public ValueTask<IArchiveEntry> AddEntryAsync(string key, FileInfo fileInfo) =>
|
||||
LibraryDefaults.Composition.FileWriter.AddEntryAsync(writableArchive, key, fileInfo);
|
||||
LibraryDefaults.Composition.ArchiveFiles.AddEntryAsync(writableArchive, key, fileInfo);
|
||||
}
|
||||
|
||||
public static ValueTask SaveToAsync<TOptions>(
|
||||
@@ -54,7 +54,7 @@ public static class IWritableAsyncArchiveExtensions
|
||||
)
|
||||
where TOptions : IWriterOptions =>
|
||||
await LibraryDefaults
|
||||
.Composition.FileWriter.SaveToAsync(
|
||||
.Composition.ArchiveFiles.SaveToAsync(
|
||||
writableArchive,
|
||||
fileInfo,
|
||||
options,
|
||||
|
||||
@@ -16,7 +16,7 @@ public static class IAsyncReaderExtensions
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteEntryToDirectoryAsync(
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToDirectoryAsync(
|
||||
reader,
|
||||
destinationDirectory,
|
||||
options,
|
||||
@@ -29,7 +29,7 @@ public static class IAsyncReaderExtensions
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteEntryToFileAsync(
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToFileAsync(
|
||||
reader,
|
||||
destinationFileName,
|
||||
options,
|
||||
@@ -42,7 +42,7 @@ public static class IAsyncReaderExtensions
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteAllToDirectoryAsync(
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteAllToDirectoryAsync(
|
||||
reader,
|
||||
destinationDirectory,
|
||||
options,
|
||||
@@ -54,7 +54,7 @@ public static class IAsyncReaderExtensions
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteEntryToFileAsync(
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToFileAsync(
|
||||
reader,
|
||||
destinationFileName,
|
||||
options,
|
||||
|
||||
@@ -9,17 +9,17 @@ public static class IReaderExtensions
|
||||
extension(IReader reader)
|
||||
{
|
||||
public void WriteEntryTo(string filePath) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteEntryTo(reader, filePath);
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteEntryTo(reader, filePath);
|
||||
|
||||
public void WriteEntryTo(FileInfo filePath) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteEntryTo(reader, filePath);
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteEntryTo(reader, filePath);
|
||||
|
||||
/// <summary>Extract all remaining unread entries to specific directory, retaining filename.</summary>
|
||||
public void WriteAllToDirectory(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteAllToDirectory(
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteAllToDirectory(
|
||||
reader,
|
||||
destinationDirectory,
|
||||
options
|
||||
@@ -30,7 +30,7 @@ public static class IReaderExtensions
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteEntryToDirectory(
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToDirectory(
|
||||
reader,
|
||||
destinationDirectory,
|
||||
options
|
||||
@@ -41,7 +41,7 @@ public static class IReaderExtensions
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
LibraryDefaults.Composition.Extractor.WriteEntryToFile(
|
||||
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToFile(
|
||||
reader,
|
||||
destinationFileName,
|
||||
options
|
||||
|
||||
@@ -13,7 +13,7 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveService
|
||||
internal sealed partial class ArchiveDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the archive at the given file path without enumerating its entries.
|
||||
@@ -110,7 +110,7 @@ internal sealed partial class ArchiveService
|
||||
}
|
||||
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
private async ValueTask<T> FindFactoryAsync<T>(
|
||||
public async ValueTask<T> FindFactoryAsync<T>(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions readerOptions,
|
||||
CancellationToken cancellationToken
|
||||
@@ -141,7 +141,7 @@ internal sealed partial class ArchiveService
|
||||
}
|
||||
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
private async ValueTask<T> FindFactoryAsync<T>(
|
||||
public async ValueTask<T> FindFactoryAsync<T>(
|
||||
Stream stream,
|
||||
ReaderOptions readerOptions,
|
||||
CancellationToken cancellationToken
|
||||
@@ -171,7 +171,7 @@ internal sealed partial class ArchiveService
|
||||
/// Stream position is restored to its value at entry on both success and failure.
|
||||
/// </summary>
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
private async ValueTask<IFactory?> TryFindFactoryAsync(
|
||||
public async ValueTask<IFactory?> TryFindFactoryAsync(
|
||||
Stream stream,
|
||||
ReaderOptions readerOptions,
|
||||
CancellationToken cancellationToken
|
||||
@@ -270,7 +270,7 @@ internal sealed partial class ArchiveService
|
||||
}
|
||||
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
private async ValueTask<ArchiveDetection?> TryDetectArchiveAsync(
|
||||
public async ValueTask<ArchiveDetection?> TryDetectArchiveAsync(
|
||||
Stream stream,
|
||||
ReaderOptions readerOptions,
|
||||
CancellationToken cancellationToken
|
||||
@@ -7,7 +7,7 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveService
|
||||
internal sealed partial class ArchiveDetector
|
||||
{
|
||||
private static void DisposeProbeStream(Stream stream)
|
||||
{
|
||||
101
src/SharpCompress/Services/ArchiveDetector.cs
Normal file
101
src/SharpCompress/Services/ArchiveDetector.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveDetector(IFactoryCatalog catalog) : IArchiveDetector
|
||||
{
|
||||
public T FindFactory<T>(string filePath)
|
||||
where T : IFactory
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return FindFactory<T>(stream);
|
||||
}
|
||||
|
||||
public T FindFactory<T>(FileInfo finfo)
|
||||
where T : IFactory
|
||||
{
|
||||
finfo.NotNull(nameof(finfo));
|
||||
using Stream stream = finfo.OpenRead();
|
||||
return FindFactory<T>(stream);
|
||||
}
|
||||
|
||||
public T FindFactory<T>(Stream stream)
|
||||
where T : IFactory => FindFactory<T>(stream, ReaderOptions.ForExternalStream);
|
||||
|
||||
public bool IsArchive(string filePath, out ArchiveType? type) =>
|
||||
IsArchive(filePath, ReaderOptions.ForFilePath, out type);
|
||||
|
||||
public bool IsArchive(string filePath, ReaderOptions? readerOptions, out ArchiveType? type)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return IsArchive(stream, readerOptions ?? ReaderOptions.ForFilePath, out type);
|
||||
}
|
||||
|
||||
public bool IsArchive(Stream stream, out ArchiveType? type) =>
|
||||
IsArchive(stream, ReaderOptions.ForExternalStream, out type);
|
||||
|
||||
public bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
|
||||
type = factory?.KnownArchiveType;
|
||||
return factory is not null;
|
||||
}
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return await IsArchiveAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForFilePath,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
var factory = await TryFindFactoryAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
return (factory is not null, factory?.KnownArchiveType);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveExtractor
|
||||
internal sealed partial class ArchiveEntryExtractor
|
||||
{
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
public async ValueTask WriteToDirectoryAsync(
|
||||
50
src/SharpCompress/Services/ArchiveEntryExtractor.cs
Normal file
50
src/SharpCompress/Services/ArchiveEntryExtractor.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
// Destination streams are borrowed, not owned by the reusable extraction service.
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveEntryExtractor : IArchiveEntryExtractor
|
||||
{
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveProgress = true)]
|
||||
public async ValueTask WriteToAsync(
|
||||
IArchiveEntry archiveEntry,
|
||||
Stream streamToWriteTo,
|
||||
int? bufferSize,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
#if SYNC_ONLY
|
||||
using var entryStream = archiveEntry.OpenEntryStream();
|
||||
#else
|
||||
var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await using var entryStreamScope = entryStream.DisposeAsyncScope().ConfigureAwait(false);
|
||||
#endif
|
||||
var checkedStream = options is null
|
||||
? entryStream
|
||||
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
|
||||
var sourceStream = ExtractionStreamHelper.WrapWithProgress(
|
||||
checkedStream,
|
||||
archiveEntry,
|
||||
progress
|
||||
);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract to specific directory with progress reporting
|
||||
/// </summary>
|
||||
/// <param name="destinationDirectory">The folder to extract into.</param>
|
||||
/// <param name="options">Extraction options.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteToDirectory(
|
||||
IArchive archive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null
|
||||
)
|
||||
{
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
{
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
WriteAllToDirectory(reader, destinationDirectory, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToDirectoryInternal(archive, destinationDirectory, options, progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteToDirectoryInternal(
|
||||
IArchive archive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
options ??= new ExtractionOptions();
|
||||
var fullDestinationDirectoryPath = DirectoryManagement.GetFullDestinationDirectoryPath(
|
||||
destinationDirectory
|
||||
);
|
||||
|
||||
var totalBytes = archive.TotalUncompressedSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
entry.WriteEntryToDirectoryCore(fullDestinationDirectoryPath, options, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.WriteEntryToDirectoryCore(
|
||||
fullDestinationDirectoryPath,
|
||||
options,
|
||||
path => WriteToFile(entry, path, options)
|
||||
);
|
||||
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,8 @@ internal sealed partial class ArchiveExtractor
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await WriteEntryToDirectoryAsync(
|
||||
await readers
|
||||
.WriteEntryToDirectoryAsync(
|
||||
reader,
|
||||
destinationDirectory,
|
||||
options,
|
||||
@@ -110,7 +111,9 @@ internal sealed partial class ArchiveExtractor
|
||||
fullDestinationDirectoryPath,
|
||||
options,
|
||||
async (path, ct) =>
|
||||
await WriteToFileAsync(entry, path, options, ct).ConfigureAwait(false),
|
||||
await entries
|
||||
.WriteToFileAsync(entry, path, options, ct)
|
||||
.ConfigureAwait(false),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
@@ -1,82 +1,83 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
// Destination streams are borrowed, not owned by the reusable extraction service.
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveExtractor : IArchiveExtractor
|
||||
internal sealed partial class ArchiveExtractor(
|
||||
IArchiveService archives,
|
||||
IReaderExtractor readers,
|
||||
IArchiveEntryExtractor entries
|
||||
) : IArchiveExtractor
|
||||
{
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveProgress = true)]
|
||||
public async ValueTask WriteToAsync(
|
||||
IArchiveEntry archiveEntry,
|
||||
Stream streamToWriteTo,
|
||||
int? bufferSize,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
public void WriteToDirectory(
|
||||
string sourceArchive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
#if SYNC_ONLY
|
||||
using var entryStream = archiveEntry.OpenEntryStream();
|
||||
#else
|
||||
var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await using var entryStreamScope = entryStream.DisposeAsyncScope().ConfigureAwait(false);
|
||||
#endif
|
||||
var checkedStream = options is null
|
||||
? entryStream
|
||||
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
|
||||
var sourceStream = WrapWithProgress(checkedStream, archiveEntry, progress);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
using var archive = archives.OpenArchive(sourceArchive);
|
||||
WriteToDirectory(archive, destinationDirectory, options);
|
||||
}
|
||||
|
||||
private static Stream WrapWithProgress(
|
||||
Stream source,
|
||||
IEntry entry,
|
||||
/// <summary>
|
||||
/// Extract to specific directory with progress reporting
|
||||
/// </summary>
|
||||
/// <param name="destinationDirectory">The folder to extract into.</param>
|
||||
/// <param name="options">Extraction options.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteToDirectory(
|
||||
IArchive archive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null
|
||||
)
|
||||
{
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
{
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
readers.WriteAllToDirectory(reader, destinationDirectory, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToDirectoryInternal(archive, destinationDirectory, options, progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteToDirectoryInternal(
|
||||
IArchive archive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
if (progress is null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
var entryPath = entry.Key ?? string.Empty;
|
||||
var totalBytes = GetEntrySizeSafe(entry);
|
||||
return new ProgressReportingStream(
|
||||
source,
|
||||
progress,
|
||||
entryPath,
|
||||
totalBytes,
|
||||
leaveOpen: true
|
||||
options ??= new ExtractionOptions();
|
||||
var fullDestinationDirectoryPath = DirectoryManagement.GetFullDestinationDirectoryPath(
|
||||
destinationDirectory
|
||||
);
|
||||
}
|
||||
|
||||
private static long? GetEntrySizeSafe(IEntry entry)
|
||||
{
|
||||
try
|
||||
var totalBytes = archive.TotalUncompressedSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var size = entry.Size;
|
||||
return size >= 0 ? size : null;
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return null;
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
entry.WriteEntryToDirectoryCore(fullDestinationDirectoryPath, options, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.WriteEntryToDirectoryCore(
|
||||
fullDestinationDirectoryPath,
|
||||
options,
|
||||
path => entries.WriteToFile(entry, path, options)
|
||||
);
|
||||
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Archives.SevenZip;
|
||||
@@ -18,7 +19,12 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveService
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveInspector(
|
||||
IArchiveDetector detector,
|
||||
IArchiveService archives,
|
||||
IReaderService readers
|
||||
) : IArchiveInspector
|
||||
{
|
||||
/// <summary>
|
||||
/// Collects metadata for the archive at the given file path.
|
||||
@@ -50,7 +56,8 @@ internal sealed partial class ArchiveService
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
var options = readerOptions ?? ReaderOptions.ForFilePath;
|
||||
var detection = await DetectArchiveAsync(filePath, options, cancellationToken)
|
||||
var detection = await detector
|
||||
.DetectArchiveAsync(filePath, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (detection is null)
|
||||
{
|
||||
@@ -113,11 +120,8 @@ internal sealed partial class ArchiveService
|
||||
{
|
||||
using var archiveStream = new ArchiveOffsetStream(stream);
|
||||
var inspectionOptions = options with { LeaveStreamOpen = true };
|
||||
var detection = await TryDetectArchiveAsync(
|
||||
archiveStream,
|
||||
inspectionOptions,
|
||||
cancellationToken
|
||||
)
|
||||
var detection = await detector
|
||||
.TryDetectArchiveAsync(archiveStream, inspectionOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (detection is null)
|
||||
{
|
||||
@@ -127,14 +131,11 @@ internal sealed partial class ArchiveService
|
||||
if ((detection.SupportedApis & ArchiveAccessMode.Archive) != 0)
|
||||
{
|
||||
#if SYNC_ONLY
|
||||
using var archive = OpenArchive(archiveStream, inspectionOptions);
|
||||
using var archive = archives.OpenArchive(archiveStream, inspectionOptions);
|
||||
return InspectOpenedArchive(archive, detection, physicalSize, 1);
|
||||
#else
|
||||
await using var archive = await OpenAsyncArchive(
|
||||
archiveStream,
|
||||
inspectionOptions,
|
||||
cancellationToken
|
||||
)
|
||||
await using var archive = await archives
|
||||
.OpenAsyncArchive(archiveStream, inspectionOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await InspectOpenedArchiveAsync(
|
||||
archive,
|
||||
@@ -245,7 +246,8 @@ internal sealed partial class ArchiveService
|
||||
}
|
||||
|
||||
var options = readerOptions ?? ReaderOptions.ForFilePath;
|
||||
var detection = await DetectArchiveAsync(fileInfos[0].FullName, options, cancellationToken)
|
||||
var detection = await detector
|
||||
.DetectArchiveAsync(fileInfos[0].FullName, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (detection is null)
|
||||
{
|
||||
@@ -262,10 +264,11 @@ internal sealed partial class ArchiveService
|
||||
try
|
||||
{
|
||||
#if SYNC_ONLY
|
||||
using var archive = OpenArchive(fileInfos, options);
|
||||
using var archive = archives.OpenArchive(fileInfos, options);
|
||||
return InspectOpenedArchive(archive, detection, physicalSize, fileInfos.Count);
|
||||
#else
|
||||
await using var archive = await OpenAsyncArchive(fileInfos, options, cancellationToken)
|
||||
await using var archive = await archives
|
||||
.OpenAsyncArchive(fileInfos, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await InspectOpenedArchiveAsync(
|
||||
archive,
|
||||
@@ -321,11 +324,8 @@ internal sealed partial class ArchiveService
|
||||
{
|
||||
using var firstArchiveStream = new ArchiveOffsetStream(streams[0]);
|
||||
var inspectionOptions = options with { LeaveStreamOpen = true };
|
||||
var detection = await TryDetectArchiveAsync(
|
||||
firstArchiveStream,
|
||||
inspectionOptions,
|
||||
cancellationToken
|
||||
)
|
||||
var detection = await detector
|
||||
.TryDetectArchiveAsync(firstArchiveStream, inspectionOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (detection is null)
|
||||
{
|
||||
@@ -343,14 +343,11 @@ internal sealed partial class ArchiveService
|
||||
streams.Skip(1).Select(stream => new ArchiveOffsetStream(stream))
|
||||
);
|
||||
#if SYNC_ONLY
|
||||
using var archive = OpenArchive(archiveStreams, inspectionOptions);
|
||||
using var archive = archives.OpenArchive(archiveStreams, inspectionOptions);
|
||||
return InspectOpenedArchive(archive, detection, physicalSize, streams.Count);
|
||||
#else
|
||||
await using var archive = await OpenAsyncArchive(
|
||||
archiveStreams,
|
||||
inspectionOptions,
|
||||
cancellationToken
|
||||
)
|
||||
await using var archive = await archives
|
||||
.OpenAsyncArchive(archiveStreams, inspectionOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await InspectOpenedArchiveAsync(
|
||||
archive,
|
||||
@@ -607,7 +604,8 @@ internal sealed partial class ArchiveService
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
using var archiveStream = new ArchiveOffsetStream(stream);
|
||||
return await TryDetectArchiveAsync(archiveStream, options, cancellationToken)
|
||||
return await detector
|
||||
.TryDetectArchiveAsync(archiveStream, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ using AceMainHeader = SharpCompress.Common.Ace.Headers.AceMainHeader;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveService
|
||||
internal sealed partial class ArchiveInspector
|
||||
{
|
||||
private static ArchiveInformation InspectOpenedArchive(
|
||||
IArchive archive,
|
||||
@@ -235,7 +235,7 @@ internal sealed partial class ArchiveService
|
||||
private FileInfo[] GetArchiveFileParts(FileInfo firstPart, ReaderOptions options)
|
||||
{
|
||||
using Stream stream = firstPart.OpenRead();
|
||||
var factory = TryFindFactory(stream, options);
|
||||
var factory = detector.TryFindFactory(stream, options);
|
||||
if (factory is null)
|
||||
{
|
||||
return [firstPart];
|
||||
@@ -18,11 +18,8 @@ internal sealed partial class ArchiveService
|
||||
)
|
||||
{
|
||||
readerOptions ??= ReaderOptions.ForExternalStream;
|
||||
var factory = await FindFactoryAsync<IArchiveFactory>(
|
||||
stream,
|
||||
readerOptions,
|
||||
cancellationToken
|
||||
)
|
||||
var factory = await detector
|
||||
.FindFactoryAsync<IArchiveFactory>(stream, readerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(stream, readerOptions, cancellationToken)
|
||||
@@ -51,7 +48,8 @@ internal sealed partial class ArchiveService
|
||||
{
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
var factory = await FindFactoryAsync<IArchiveFactory>(fileInfo, options, cancellationToken)
|
||||
var factory = await detector
|
||||
.FindFactoryAsync<IArchiveFactory>(fileInfo, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(fileInfo, options, cancellationToken)
|
||||
@@ -81,11 +79,8 @@ internal sealed partial class ArchiveService
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
var factory = await FindFactoryAsync<IMultiArchiveFactory>(
|
||||
fileInfo,
|
||||
options,
|
||||
cancellationToken
|
||||
)
|
||||
var factory = await detector
|
||||
.FindFactoryAsync<IMultiArchiveFactory>(fileInfo, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(filesArray, options, cancellationToken)
|
||||
@@ -110,11 +105,8 @@ internal sealed partial class ArchiveService
|
||||
firstStream.NotNull(nameof(firstStream));
|
||||
options ??= ReaderOptions.ForExternalStream;
|
||||
|
||||
var factory = await FindFactoryAsync<IMultiArchiveFactory>(
|
||||
firstStream,
|
||||
options,
|
||||
cancellationToken
|
||||
)
|
||||
var factory = await detector
|
||||
.FindFactoryAsync<IMultiArchiveFactory>(firstStream, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(streamsArray, options, cancellationToken)
|
||||
|
||||
@@ -2,8 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
@@ -14,16 +12,14 @@ using SharpCompress.Readers;
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveService(
|
||||
IFactoryCatalog catalog,
|
||||
IReaderService readers,
|
||||
IArchiveExtractor extractor
|
||||
) : IArchiveService
|
||||
internal sealed partial class ArchiveService(IFactoryCatalog catalog, IArchiveDetector detector)
|
||||
: IArchiveService
|
||||
{
|
||||
public IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
readerOptions ??= ReaderOptions.ForExternalStream;
|
||||
return FindFactory<IArchiveFactory>(stream, readerOptions)
|
||||
return detector
|
||||
.FindFactory<IArchiveFactory>(stream, readerOptions)
|
||||
.OpenArchive(stream, readerOptions);
|
||||
}
|
||||
|
||||
@@ -52,7 +48,9 @@ internal sealed partial class ArchiveService(
|
||||
{
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
return FindFactory<IArchiveFactory>(fileInfo, options).OpenArchive(fileInfo, options);
|
||||
return detector
|
||||
.FindFactory<IArchiveFactory>(fileInfo, options)
|
||||
.OpenArchive(fileInfo, options);
|
||||
}
|
||||
|
||||
public IArchive OpenArchive(IReadOnlyList<FileInfo> fileInfos, ReaderOptions? options = null)
|
||||
@@ -73,7 +71,8 @@ internal sealed partial class ArchiveService(
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
return FindFactory<IMultiArchiveFactory>(fileInfo, options)
|
||||
return detector
|
||||
.FindFactory<IMultiArchiveFactory>(fileInfo, options)
|
||||
.OpenArchive(filesArray, options);
|
||||
}
|
||||
|
||||
@@ -94,114 +93,11 @@ internal sealed partial class ArchiveService(
|
||||
firstStream.NotNull(nameof(firstStream));
|
||||
options ??= ReaderOptions.ForExternalStream;
|
||||
|
||||
return FindFactory<IMultiArchiveFactory>(firstStream, options)
|
||||
return detector
|
||||
.FindFactory<IMultiArchiveFactory>(firstStream, options)
|
||||
.OpenArchive(streamsArray, options);
|
||||
}
|
||||
|
||||
public void WriteToDirectory(
|
||||
string sourceArchive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
)
|
||||
{
|
||||
using var archive = OpenArchive(sourceArchive);
|
||||
extractor.WriteToDirectory(archive, destinationDirectory, options);
|
||||
}
|
||||
|
||||
public T FindFactory<T>(string filePath)
|
||||
where T : IFactory
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return FindFactory<T>(stream);
|
||||
}
|
||||
|
||||
public T FindFactory<T>(FileInfo finfo)
|
||||
where T : IFactory
|
||||
{
|
||||
finfo.NotNull(nameof(finfo));
|
||||
using Stream stream = finfo.OpenRead();
|
||||
return FindFactory<T>(stream);
|
||||
}
|
||||
|
||||
public T FindFactory<T>(Stream stream)
|
||||
where T : IFactory => FindFactory<T>(stream, ReaderOptions.ForExternalStream);
|
||||
|
||||
public bool IsArchive(string filePath, out ArchiveType? type)
|
||||
{
|
||||
return IsArchive(filePath, ReaderOptions.ForFilePath, out type);
|
||||
}
|
||||
|
||||
public bool IsArchive(string filePath, ReaderOptions? readerOptions, out ArchiveType? type)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream s = File.OpenRead(filePath);
|
||||
return IsArchive(s, readerOptions ?? ReaderOptions.ForFilePath, out type);
|
||||
}
|
||||
|
||||
public bool IsArchive(Stream stream, out ArchiveType? type)
|
||||
{
|
||||
return IsArchive(stream, ReaderOptions.ForExternalStream, out type);
|
||||
}
|
||||
|
||||
public bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
|
||||
type = factory?.KnownArchiveType;
|
||||
return factory is not null;
|
||||
}
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return await IsArchiveAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForFilePath,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
var factory = await TryFindFactoryAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
return (factory is not null, factory?.KnownArchiveType);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetFileParts(string part1)
|
||||
{
|
||||
part1.NotNullOrEmpty(nameof(part1));
|
||||
|
||||
42
src/SharpCompress/Services/ExtractionStreamHelper.cs
Normal file
42
src/SharpCompress/Services/ExtractionStreamHelper.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
// Pure stream decoration shared by entry and reader workflows; it never takes stream ownership.
|
||||
internal static class ExtractionStreamHelper
|
||||
{
|
||||
public static Stream WrapWithProgress(
|
||||
Stream source,
|
||||
IEntry entry,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
if (progress is null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
return new ProgressReportingStream(
|
||||
source,
|
||||
progress,
|
||||
entry.Key ?? string.Empty,
|
||||
GetEntrySizeSafe(entry),
|
||||
leaveOpen: true
|
||||
);
|
||||
}
|
||||
|
||||
private static long? GetEntrySizeSafe(IEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var size = entry.Size;
|
||||
return size >= 0 ? size : null;
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveExtractor
|
||||
internal sealed partial class ReaderExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously, retaining filename
|
||||
@@ -92,7 +90,11 @@ internal sealed partial class ArchiveExtractor
|
||||
entryStream,
|
||||
options
|
||||
);
|
||||
var sourceStream = WrapWithProgress(checkedStream, reader.Entry);
|
||||
var sourceStream = ExtractionStreamHelper.WrapWithProgress(
|
||||
checkedStream,
|
||||
reader.Entry,
|
||||
reader.Entry.Options.Progress
|
||||
);
|
||||
await sourceStream
|
||||
.CopyToAsync(writableStream, options.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveExtractor
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ReaderExtractor : IReaderExtractor
|
||||
{
|
||||
public void WriteEntryTo(IReader reader, string filePath)
|
||||
{
|
||||
@@ -82,12 +82,11 @@ internal sealed partial class ArchiveExtractor
|
||||
entryStream,
|
||||
options
|
||||
);
|
||||
var sourceStream = WrapWithProgress(checkedStream, reader.Entry);
|
||||
var sourceStream = ExtractionStreamHelper.WrapWithProgress(
|
||||
checkedStream,
|
||||
reader.Entry,
|
||||
reader.Entry.Options.Progress
|
||||
);
|
||||
sourceStream.CopyTo(writableStream, options.BufferSize);
|
||||
}
|
||||
|
||||
private static Stream WrapWithProgress(Stream source, IEntry entry)
|
||||
{
|
||||
return WrapWithProgress(source, entry, entry.Options.Progress);
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,40 @@ internal partial class ServiceComposition
|
||||
.Bind<IArchiveService>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveService>()
|
||||
.Bind<IArchiveDetector>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveDetector>()
|
||||
.Bind<IArchiveInspector>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveInspector>()
|
||||
.Bind<IReaderExtractor>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ReaderExtractor>()
|
||||
.Bind<IArchiveEntryExtractor>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveEntryExtractor>()
|
||||
.Bind<IWriterService>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<WriterService>()
|
||||
.Bind<IArchiveExtractor>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveExtractor>()
|
||||
.Bind<IArchiveFileWriter>()
|
||||
.Bind<IWriterFileOperations>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveFileWriter>()
|
||||
.To<WriterFileOperations>()
|
||||
.Bind<IWritableArchiveFileOperations>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<WritableArchiveFileOperations>()
|
||||
.Root<IArchiveService>("Archives")
|
||||
.Root<IReaderService>("Readers")
|
||||
.Root<IWriterService>("Writers")
|
||||
.Root<IArchiveExtractor>("Extractor")
|
||||
.Root<IArchiveFileWriter>("FileWriter");
|
||||
.Root<IArchiveDetector>("Detector")
|
||||
.Root<IArchiveInspector>("Inspector")
|
||||
.Root<IReaderExtractor>("ReaderExtractor")
|
||||
.Root<IArchiveEntryExtractor>("EntryExtractor")
|
||||
.Root<IWriterFileOperations>("WriterFiles")
|
||||
.Root<IWritableArchiveFileOperations>("ArchiveFiles");
|
||||
}
|
||||
|
||||
internal static class LibraryDefaults
|
||||
|
||||
@@ -16,16 +16,34 @@ internal partial interface IReaderService;
|
||||
|
||||
internal partial interface IWriterService;
|
||||
|
||||
internal partial interface IArchiveFileWriter;
|
||||
internal partial interface IWriterFileOperations;
|
||||
|
||||
internal partial interface IWritableArchiveFileOperations;
|
||||
|
||||
internal partial interface IReaderExtractor;
|
||||
|
||||
internal partial interface IArchiveExtractor;
|
||||
|
||||
internal partial interface IArchiveService;
|
||||
|
||||
// Pure.DI cannot see methods emitted by another source generator. Only the sync signatures
|
||||
// generated by Zomp are declared here; Pure.DI generates the rest from the implementations.
|
||||
internal partial interface IArchiveService
|
||||
internal partial interface IArchiveDetector
|
||||
{
|
||||
ArchiveDetection? DetectArchive(string filePath);
|
||||
ArchiveDetection? DetectArchive(string filePath, ReaderOptions? readerOptions);
|
||||
ArchiveDetection? DetectArchive(Stream stream);
|
||||
ArchiveDetection? DetectArchive(Stream stream, ReaderOptions? readerOptions);
|
||||
T FindFactory<T>(FileInfo fileInfo, ReaderOptions readerOptions)
|
||||
where T : SharpCompress.Factories.IFactory;
|
||||
T FindFactory<T>(Stream stream, ReaderOptions readerOptions)
|
||||
where T : SharpCompress.Factories.IFactory;
|
||||
SharpCompress.Factories.IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions);
|
||||
ArchiveDetection? TryDetectArchive(Stream stream, ReaderOptions readerOptions);
|
||||
}
|
||||
|
||||
internal partial interface IArchiveInspector
|
||||
{
|
||||
ArchiveInformation? InspectArchive(string filePath);
|
||||
ArchiveInformation? InspectArchive(string filePath, ReaderOptions? readerOptions);
|
||||
ArchiveInformation? InspectArchive(Stream stream);
|
||||
@@ -40,7 +58,7 @@ internal partial interface IArchiveService
|
||||
);
|
||||
}
|
||||
|
||||
internal partial interface IArchiveExtractor
|
||||
internal partial interface IArchiveEntryExtractor
|
||||
{
|
||||
void WriteTo(
|
||||
IArchiveEntry archiveEntry,
|
||||
|
||||
@@ -2,12 +2,14 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common.Options;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveFileWriter
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed class WritableArchiveFileOperations : IWritableArchiveFileOperations
|
||||
{
|
||||
public void AddAllFromDirectory(
|
||||
IWritableArchive writableArchive,
|
||||
@@ -9,7 +9,7 @@ using SharpCompress.Writers;
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveFileWriter : IArchiveFileWriter
|
||||
internal sealed class WriterFileOperations : IWriterFileOperations
|
||||
{
|
||||
public void Write(IWriter writer, string entryPath, FileInfo source)
|
||||
{
|
||||
@@ -13,7 +13,7 @@ public static class IWriterExtensions
|
||||
public void Write(string entryPath, Stream source) => writer.Write(entryPath, source, null);
|
||||
|
||||
public void Write(string entryPath, FileInfo source) =>
|
||||
LibraryDefaults.Composition.FileWriter.Write(writer, entryPath, source);
|
||||
LibraryDefaults.Composition.WriterFiles.Write(writer, entryPath, source);
|
||||
|
||||
public void Write(string entryPath, string source) =>
|
||||
writer.Write(entryPath, new FileInfo(source));
|
||||
@@ -30,7 +30,7 @@ public static class IWriterExtensions
|
||||
Func<string, bool>? fileSearchFunc = null,
|
||||
SearchOption option = SearchOption.TopDirectoryOnly
|
||||
) =>
|
||||
LibraryDefaults.Composition.FileWriter.WriteAll(
|
||||
LibraryDefaults.Composition.WriterFiles.WriteAll(
|
||||
writer,
|
||||
directory,
|
||||
searchPattern,
|
||||
@@ -56,7 +56,7 @@ public static class IWriterExtensions
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await LibraryDefaults
|
||||
.Composition.FileWriter.WriteAsync(writer, entryPath, source, cancellationToken)
|
||||
.Composition.WriterFiles.WriteAsync(writer, entryPath, source, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public ValueTask WriteAsync(
|
||||
@@ -80,7 +80,7 @@ public static class IWriterExtensions
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await LibraryDefaults
|
||||
.Composition.FileWriter.WriteAllAsync(
|
||||
.Composition.WriterFiles.WriteAllAsync(
|
||||
writer,
|
||||
directory,
|
||||
searchPattern,
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Detection;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Providers;
|
||||
using SharpCompress.Providers.Default;
|
||||
@@ -24,6 +25,20 @@ public class ServiceCompositionTests : TestBase
|
||||
private static ServiceComposition CreateComposition(params IFactory[] factories) =>
|
||||
new(new FactoryCatalog(() => factories, () => TarWrapper.Wrappers));
|
||||
|
||||
private static object[] GetRoots(ServiceComposition composition) =>
|
||||
[
|
||||
composition.Archives,
|
||||
composition.Detector,
|
||||
composition.Inspector,
|
||||
composition.Readers,
|
||||
composition.Writers,
|
||||
composition.Extractor,
|
||||
composition.ReaderExtractor,
|
||||
composition.EntryExtractor,
|
||||
composition.WriterFiles,
|
||||
composition.ArchiveFiles,
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void Composition_RemainsAnInternalBuildTimeDetail()
|
||||
{
|
||||
@@ -36,13 +51,24 @@ public class ServiceCompositionTests : TestBase
|
||||
typeof(ReaderService),
|
||||
typeof(WriterService),
|
||||
typeof(ArchiveExtractor),
|
||||
typeof(ArchiveFileWriter),
|
||||
typeof(ExtractionStreamHelper),
|
||||
typeof(ArchiveDetector),
|
||||
typeof(ArchiveInspector),
|
||||
typeof(ReaderExtractor),
|
||||
typeof(ArchiveEntryExtractor),
|
||||
typeof(WriterFileOperations),
|
||||
typeof(WritableArchiveFileOperations),
|
||||
typeof(IFactoryCatalog),
|
||||
typeof(IArchiveService),
|
||||
typeof(IReaderService),
|
||||
typeof(IWriterService),
|
||||
typeof(IArchiveExtractor),
|
||||
typeof(IArchiveFileWriter),
|
||||
typeof(IArchiveDetector),
|
||||
typeof(IArchiveInspector),
|
||||
typeof(IReaderExtractor),
|
||||
typeof(IArchiveEntryExtractor),
|
||||
typeof(IWriterFileOperations),
|
||||
typeof(IWritableArchiveFileOperations),
|
||||
];
|
||||
Assert.All(
|
||||
serviceTypes,
|
||||
@@ -69,7 +95,12 @@ public class ServiceCompositionTests : TestBase
|
||||
(typeof(IReaderService), typeof(ReaderService)),
|
||||
(typeof(IWriterService), typeof(WriterService)),
|
||||
(typeof(IArchiveExtractor), typeof(ArchiveExtractor)),
|
||||
(typeof(IArchiveFileWriter), typeof(ArchiveFileWriter)),
|
||||
(typeof(IArchiveDetector), typeof(ArchiveDetector)),
|
||||
(typeof(IArchiveInspector), typeof(ArchiveInspector)),
|
||||
(typeof(IReaderExtractor), typeof(ReaderExtractor)),
|
||||
(typeof(IArchiveEntryExtractor), typeof(ArchiveEntryExtractor)),
|
||||
(typeof(IWriterFileOperations), typeof(WriterFileOperations)),
|
||||
(typeof(IWritableArchiveFileOperations), typeof(WritableArchiveFileOperations)),
|
||||
];
|
||||
foreach (var (contract, implementation) in services)
|
||||
{
|
||||
@@ -86,25 +117,9 @@ public class ServiceCompositionTests : TestBase
|
||||
);
|
||||
}
|
||||
}
|
||||
Assert.Equal(
|
||||
typeof(IArchiveService),
|
||||
typeof(ServiceComposition).GetProperty("Archives")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IReaderService),
|
||||
typeof(ServiceComposition).GetProperty("Readers")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IWriterService),
|
||||
typeof(ServiceComposition).GetProperty("Writers")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IArchiveExtractor),
|
||||
typeof(ServiceComposition).GetProperty("Extractor")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IArchiveFileWriter),
|
||||
typeof(ServiceComposition).GetProperty("FileWriter")!.PropertyType
|
||||
Assert.All(
|
||||
typeof(ServiceComposition).GetProperties(),
|
||||
root => Assert.Contains(services, service => service.Contract == root.PropertyType)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +129,7 @@ public class ServiceCompositionTests : TestBase
|
||||
IFactoryCatalog catalog = new TestFactoryCatalog();
|
||||
var composition = new ServiceComposition(catalog);
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
|
||||
Assert.Equal(ArchiveType.Zip, composition.Archives.DetectArchive(stream)?.ContainerType);
|
||||
Assert.Equal(ArchiveType.Zip, composition.Detector.DetectArchive(stream)?.ContainerType);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
@@ -130,36 +145,59 @@ public class ServiceCompositionTests : TestBase
|
||||
var composition = CreateComposition(new ZipFactory());
|
||||
var other = CreateComposition(new ZipFactory());
|
||||
var roots = await Task.WhenAll(
|
||||
Enumerable
|
||||
.Range(0, 10)
|
||||
.Select(_ =>
|
||||
Task.Run(() =>
|
||||
(
|
||||
composition.Archives,
|
||||
composition.Readers,
|
||||
composition.Writers,
|
||||
composition.Extractor,
|
||||
composition.FileWriter
|
||||
)
|
||||
Enumerable.Range(0, 10).Select(_ => Task.Run(() => GetRoots(composition)))
|
||||
);
|
||||
var expected = GetRoots(composition);
|
||||
var separate = GetRoots(other);
|
||||
for (var i = 0; i < expected.Length; i++)
|
||||
{
|
||||
Assert.All(roots, root => Assert.Same(expected[i], root[i]));
|
||||
Assert.NotSame(expected[i], separate[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Inspector_KeepsMetadataStateLocalToEachOperation()
|
||||
{
|
||||
var composition = CreateComposition(new ZipFactory(), new AceFactory());
|
||||
var expected = new[] { "Zip.deflate.zip", "Ace.store.ace" }
|
||||
.Select(name => Path.Combine(TEST_ARCHIVES_PATH, name))
|
||||
.Select(path =>
|
||||
(
|
||||
Path: path,
|
||||
Information: Assert.IsType<ArchiveInformation>(
|
||||
composition.Inspector.InspectArchive(path)
|
||||
)
|
||||
)
|
||||
)
|
||||
.ToArray();
|
||||
await Task.WhenAll(
|
||||
Enumerable
|
||||
.Range(0, 20)
|
||||
.Select(index =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var sample = expected[index % expected.Length];
|
||||
using var stream = File.OpenRead(sample.Path);
|
||||
var information = Assert.IsType<ArchiveInformation>(
|
||||
await composition.Inspector.InspectArchiveAsync(
|
||||
stream,
|
||||
CancellationToken.None
|
||||
)
|
||||
);
|
||||
Assert.Equal(sample.Information.EntryCount, information.EntryCount);
|
||||
Assert.Equal(
|
||||
sample.Information.UncompressedPayloadSize,
|
||||
information.UncompressedPayloadSize
|
||||
);
|
||||
Assert.Equal(
|
||||
sample.Information.CompressedPayloadSize,
|
||||
information.CompressedPayloadSize
|
||||
);
|
||||
Assert.Equal(0, stream.Position);
|
||||
})
|
||||
)
|
||||
);
|
||||
Assert.All(
|
||||
roots,
|
||||
root =>
|
||||
{
|
||||
Assert.Same(composition.Archives, root.Archives);
|
||||
Assert.Same(composition.Readers, root.Readers);
|
||||
Assert.Same(composition.Writers, root.Writers);
|
||||
Assert.Same(composition.Extractor, root.Extractor);
|
||||
Assert.Same(composition.FileWriter, root.FileWriter);
|
||||
}
|
||||
);
|
||||
Assert.NotSame(composition.Archives, other.Archives);
|
||||
Assert.NotSame(composition.Readers, other.Readers);
|
||||
Assert.NotSame(composition.Writers, other.Writers);
|
||||
Assert.NotSame(composition.Extractor, other.Extractor);
|
||||
Assert.NotSame(composition.FileWriter, other.FileWriter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -178,15 +216,15 @@ public class ServiceCompositionTests : TestBase
|
||||
using var stream = File.OpenRead(path);
|
||||
Assert.Equal(
|
||||
ArchiveType.Zip,
|
||||
zip.Archives.DetectArchive(stream)?.ContainerType
|
||||
zip.Detector.DetectArchive(stream)?.ContainerType
|
||||
);
|
||||
Assert.Null(empty.Archives.DetectArchive(stream));
|
||||
Assert.Null(empty.Detector.DetectArchive(stream));
|
||||
Assert.Equal(0, stream.Position);
|
||||
Assert.NotNull(
|
||||
await zip.Archives.InspectArchiveAsync(stream, CancellationToken.None)
|
||||
await zip.Inspector.InspectArchiveAsync(stream, CancellationToken.None)
|
||||
);
|
||||
Assert.Null(
|
||||
await empty.Archives.InspectArchiveAsync(stream, CancellationToken.None)
|
||||
await empty.Inspector.InspectArchiveAsync(stream, CancellationToken.None)
|
||||
);
|
||||
Assert.Equal(0, stream.Position);
|
||||
using var archive = zip.Archives.OpenArchive(stream);
|
||||
@@ -241,7 +279,7 @@ public class ServiceCompositionTests : TestBase
|
||||
);
|
||||
await foreach (var entry in archive.EntriesAsync)
|
||||
{
|
||||
await services.Extractor.WriteToAsync(
|
||||
await services.EntryExtractor.WriteToAsync(
|
||||
entry,
|
||||
output,
|
||||
null,
|
||||
@@ -252,7 +290,7 @@ public class ServiceCompositionTests : TestBase
|
||||
else
|
||||
{
|
||||
using var archive = services.Archives.OpenArchive(stream, readerOptions);
|
||||
services.Extractor.WriteTo(Assert.Single(archive.Entries), output, null);
|
||||
services.EntryExtractor.WriteTo(Assert.Single(archive.Entries), output, null);
|
||||
}
|
||||
Assert.True(provider.DecompressionCalls > 0);
|
||||
Assert.Equal("instance services", Encoding.UTF8.GetString(output.ToArray()));
|
||||
@@ -289,7 +327,7 @@ public class ServiceCompositionTests : TestBase
|
||||
cancellation.Cancel();
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
services.Archives.DetectArchiveAsync(stream, cancellation.Token).AsTask()
|
||||
services.Detector.DetectArchiveAsync(stream, cancellation.Token).AsTask()
|
||||
);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using SharpCompress.Common;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Services;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
@@ -15,6 +16,58 @@ namespace SharpCompress.Test;
|
||||
|
||||
public class ServiceWorkflowTests : TestBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task WriterFilesystemOperations_LeaveBorrowedOutputOpen(bool useAsync)
|
||||
{
|
||||
var composition = new ServiceComposition(
|
||||
new FactoryCatalog(() => [new ZipFactory()], () => TarWrapper.Wrappers)
|
||||
);
|
||||
var directory = CreateScratchDirectory("writer-source") + Path.DirectorySeparatorChar;
|
||||
File.WriteAllText(Path.Combine(directory, "payload.txt"), "writer payload");
|
||||
using var output = new MemoryStream();
|
||||
var options = new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true };
|
||||
if (useAsync)
|
||||
{
|
||||
await using var writer = await composition.Writers.OpenAsyncWriter(
|
||||
output,
|
||||
ArchiveType.Zip,
|
||||
options,
|
||||
CancellationToken.None
|
||||
);
|
||||
await composition.WriterFiles.WriteAllAsync(
|
||||
writer,
|
||||
directory,
|
||||
"*",
|
||||
null,
|
||||
SearchOption.TopDirectoryOnly,
|
||||
CancellationToken.None
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var writer = composition.Writers.OpenWriter(output, ArchiveType.Zip, options);
|
||||
composition.WriterFiles.WriteAll(
|
||||
writer,
|
||||
directory,
|
||||
"*",
|
||||
null,
|
||||
SearchOption.TopDirectoryOnly
|
||||
);
|
||||
}
|
||||
Assert.True(output.CanWrite);
|
||||
output.Position = 0;
|
||||
using var archive = composition.Archives.OpenArchive(
|
||||
output,
|
||||
ReaderOptions.ForExternalStream
|
||||
);
|
||||
var entry = Assert.Single(archive.Entries);
|
||||
using var content = entry.OpenEntryStream();
|
||||
using var text = new StreamReader(content);
|
||||
Assert.Equal("writer payload", text.ReadToEnd());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
@@ -44,20 +97,20 @@ public class ServiceWorkflowTests : TestBase
|
||||
}
|
||||
else
|
||||
{
|
||||
await composition.FileWriter.AddAllFromDirectoryAsync(
|
||||
await composition.ArchiveFiles.AddAllFromDirectoryAsync(
|
||||
archive,
|
||||
sourceDirectory,
|
||||
"*.*",
|
||||
SearchOption.AllDirectories,
|
||||
CancellationToken.None
|
||||
);
|
||||
await composition.FileWriter.AddEntryAsync(
|
||||
await composition.ArchiveFiles.AddEntryAsync(
|
||||
archive,
|
||||
"extra.txt",
|
||||
source,
|
||||
CancellationToken.None
|
||||
);
|
||||
await composition.FileWriter.SaveToAsync(
|
||||
await composition.ArchiveFiles.SaveToAsync(
|
||||
archive,
|
||||
destination,
|
||||
options,
|
||||
@@ -76,14 +129,14 @@ public class ServiceWorkflowTests : TestBase
|
||||
}
|
||||
else
|
||||
{
|
||||
composition.FileWriter.AddAllFromDirectory(
|
||||
composition.ArchiveFiles.AddAllFromDirectory(
|
||||
archive,
|
||||
sourceDirectory,
|
||||
"*.*",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
composition.FileWriter.AddEntry(archive, "extra.txt", source);
|
||||
composition.FileWriter.SaveTo(archive, destination, options);
|
||||
composition.ArchiveFiles.AddEntry(archive, "extra.txt", source);
|
||||
composition.ArchiveFiles.SaveTo(archive, destination, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +165,7 @@ public class ServiceWorkflowTests : TestBase
|
||||
}
|
||||
else
|
||||
{
|
||||
await composition.Extractor.WriteAllToDirectoryAsync(
|
||||
await composition.ReaderExtractor.WriteAllToDirectoryAsync(
|
||||
reader,
|
||||
readerDirectory,
|
||||
cancellationToken: CancellationToken.None
|
||||
@@ -135,7 +188,7 @@ public class ServiceWorkflowTests : TestBase
|
||||
}
|
||||
else
|
||||
{
|
||||
composition.Extractor.WriteAllToDirectory(reader, readerDirectory);
|
||||
composition.ReaderExtractor.WriteAllToDirectory(reader, readerDirectory);
|
||||
composition.Extractor.WriteToDirectory(archive, archiveDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user