mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-04 05:25:00 +00:00
Add opt-in multi-threading support with SupportsMultiThreadedExtraction flag
- Added IArchive.SupportsMultiThreadedExtraction property to indicate if multi-threading is supported - Added ReaderOptions.EnableMultiThreadedExtraction option to opt-in to multi-threading - Updated SeekableZipFilePart, TarFilePart, and SeekableFilePart to check the flag - Added test to verify multi-threading flag behavior - Multi-threading is now disabled by default for backward compatibility Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
@@ -145,6 +145,19 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
|
||||
/// </summary>
|
||||
public virtual bool IsEncrypted => false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether multi-threaded extraction is supported for this archive.
|
||||
/// Multi-threading is supported when:
|
||||
/// 1. The archive is opened from a FileInfo or file path (not a stream)
|
||||
/// 2. Multi-threading is explicitly enabled in ReaderOptions
|
||||
/// 3. The archive is not SOLID (SOLID archives should use sequential extraction)
|
||||
/// </summary>
|
||||
public virtual bool SupportsMultiThreadedExtraction =>
|
||||
_sourceStream is not null
|
||||
&& _sourceStream.IsFileMode
|
||||
&& ReaderOptions.EnableMultiThreadedExtraction
|
||||
&& !IsSolid;
|
||||
|
||||
/// <summary>
|
||||
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
|
||||
/// </summary>
|
||||
|
||||
@@ -44,4 +44,12 @@ public interface IArchive : IDisposable
|
||||
/// Returns whether the archive is encrypted.
|
||||
/// </summary>
|
||||
bool IsEncrypted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether multi-threaded extraction is supported for this archive.
|
||||
/// Multi-threading is supported when the archive is opened from a FileInfo or file path
|
||||
/// (not a stream) and the format supports random access (e.g., Zip, Tar, Rar).
|
||||
/// SOLID archives (some Rar, all 7Zip) should use sequential extraction for best performance.
|
||||
/// </summary>
|
||||
bool SupportsMultiThreadedExtraction { get; }
|
||||
}
|
||||
|
||||
@@ -27,9 +27,13 @@ internal class SeekableFilePart : RarFilePart
|
||||
{
|
||||
Stream streamToUse;
|
||||
|
||||
// If the stream is a SourceStream in file mode, create an independent stream
|
||||
// to support concurrent multi-threaded extraction
|
||||
if (_stream is SourceStream sourceStream && sourceStream.IsFileMode)
|
||||
// If the stream is a SourceStream in file mode with multi-threading enabled,
|
||||
// create an independent stream to support concurrent extraction
|
||||
if (
|
||||
_stream is SourceStream sourceStream
|
||||
&& sourceStream.IsFileMode
|
||||
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
|
||||
)
|
||||
{
|
||||
var independentStream = sourceStream.CreateIndependentStream(0);
|
||||
if (independentStream is not null)
|
||||
|
||||
@@ -23,9 +23,13 @@ internal sealed class TarFilePart : FilePart
|
||||
{
|
||||
if (_seekableStream is not null)
|
||||
{
|
||||
// If the seekable stream is a SourceStream in file mode, create an independent stream
|
||||
// to support concurrent multi-threaded extraction
|
||||
if (_seekableStream is SourceStream sourceStream && sourceStream.IsFileMode)
|
||||
// If the seekable stream is a SourceStream in file mode with multi-threading enabled,
|
||||
// create an independent stream to support concurrent extraction
|
||||
if (
|
||||
_seekableStream is SourceStream sourceStream
|
||||
&& sourceStream.IsFileMode
|
||||
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
|
||||
)
|
||||
{
|
||||
var independentStream = sourceStream.CreateIndependentStream(0);
|
||||
if (independentStream is not null)
|
||||
|
||||
@@ -61,11 +61,15 @@ internal class SeekableZipFilePart : ZipFilePart
|
||||
|
||||
private void LoadLocalHeader()
|
||||
{
|
||||
// Use an independent stream for loading the header if possible
|
||||
// Use an independent stream for loading the header if multi-threading is enabled
|
||||
Stream streamToUse = BaseStream;
|
||||
bool disposeStream = false;
|
||||
|
||||
if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
|
||||
if (
|
||||
BaseStream is SourceStream sourceStream
|
||||
&& sourceStream.IsFileMode
|
||||
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
|
||||
)
|
||||
{
|
||||
var independentStream = sourceStream.CreateIndependentStream(0);
|
||||
if (independentStream is not null)
|
||||
@@ -110,11 +114,15 @@ internal class SeekableZipFilePart : ZipFilePart
|
||||
|
||||
private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Use an independent stream for loading the header if possible
|
||||
// Use an independent stream for loading the header if multi-threading is enabled
|
||||
Stream streamToUse = BaseStream;
|
||||
bool disposeStream = false;
|
||||
|
||||
if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
|
||||
if (
|
||||
BaseStream is SourceStream sourceStream
|
||||
&& sourceStream.IsFileMode
|
||||
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
|
||||
)
|
||||
{
|
||||
var independentStream = sourceStream.CreateIndependentStream(0);
|
||||
if (independentStream is not null)
|
||||
@@ -162,9 +170,13 @@ internal class SeekableZipFilePart : ZipFilePart
|
||||
|
||||
protected override Stream CreateBaseStream()
|
||||
{
|
||||
// If BaseStream is a SourceStream in file mode, create an independent stream
|
||||
// to support concurrent multi-threaded extraction
|
||||
if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
|
||||
// If BaseStream is a SourceStream in file mode with multi-threading enabled,
|
||||
// create an independent stream to support concurrent extraction
|
||||
if (
|
||||
BaseStream is SourceStream sourceStream
|
||||
&& sourceStream.IsFileMode
|
||||
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
|
||||
)
|
||||
{
|
||||
// Create a new independent stream for this entry
|
||||
var independentStream = sourceStream.CreateIndependentStream(0);
|
||||
|
||||
@@ -28,4 +28,12 @@ public class ReaderOptions : OptionsBase
|
||||
/// When set, progress updates will be reported as entries are extracted.
|
||||
/// </summary>
|
||||
public IProgress<ProgressReport>? Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable multi-threaded extraction support when the archive is opened from a FileInfo or file path.
|
||||
/// When enabled, multiple threads can extract different entries concurrently by creating
|
||||
/// independent file streams. This is only effective for archives opened from files, not streams.
|
||||
/// Default is false for backward compatibility.
|
||||
/// </summary>
|
||||
public bool EnableMultiThreadedExtraction { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,16 @@ public class TarMultiThreadTests : TestBase
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
|
||||
var fileInfo = new FileInfo(testArchive);
|
||||
|
||||
using var archive = TarArchive.OpenArchive(fileInfo);
|
||||
var options = new SharpCompress.Readers.ReaderOptions
|
||||
{
|
||||
EnableMultiThreadedExtraction = true,
|
||||
};
|
||||
|
||||
using var archive = TarArchive.OpenArchive(fileInfo, options);
|
||||
|
||||
// Verify multi-threading is supported
|
||||
Assert.True(archive.SupportsMultiThreadedExtraction);
|
||||
|
||||
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
|
||||
|
||||
// Extract multiple entries concurrently
|
||||
@@ -62,7 +71,12 @@ public class TarMultiThreadTests : TestBase
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
|
||||
var fileInfo = new FileInfo(testArchive);
|
||||
|
||||
using var archive = TarArchive.OpenArchive(fileInfo);
|
||||
var options = new SharpCompress.Readers.ReaderOptions
|
||||
{
|
||||
EnableMultiThreadedExtraction = true,
|
||||
};
|
||||
|
||||
using var archive = TarArchive.OpenArchive(fileInfo, options);
|
||||
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
|
||||
|
||||
// Extract multiple entries concurrently
|
||||
|
||||
@@ -11,6 +11,35 @@ namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipMultiThreadTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void Zip_Archive_Without_MultiThreading_Enabled()
|
||||
{
|
||||
// Test that extraction still works when multi-threading is NOT enabled
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
|
||||
var fileInfo = new FileInfo(testArchive);
|
||||
|
||||
// Default options - multi-threading disabled
|
||||
using var archive = ZipArchive.OpenArchive(fileInfo);
|
||||
|
||||
// Verify multi-threading is NOT supported
|
||||
Assert.False(archive.SupportsMultiThreadedExtraction);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
|
||||
|
||||
var dir = Path.GetDirectoryName(outputFile);
|
||||
if (dir != null)
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var fileStream = File.Create(outputFile);
|
||||
entryStream.CopyTo(fileStream);
|
||||
|
||||
Assert.True(File.Exists(outputFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Archive_Concurrent_Extraction_From_FileInfo()
|
||||
{
|
||||
@@ -18,7 +47,16 @@ public class ZipMultiThreadTests : TestBase
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
|
||||
var fileInfo = new FileInfo(testArchive);
|
||||
|
||||
using var archive = ZipArchive.OpenArchive(fileInfo);
|
||||
var options = new SharpCompress.Readers.ReaderOptions
|
||||
{
|
||||
EnableMultiThreadedExtraction = true,
|
||||
};
|
||||
|
||||
using var archive = ZipArchive.OpenArchive(fileInfo, options);
|
||||
|
||||
// Verify multi-threading is supported
|
||||
Assert.True(archive.SupportsMultiThreadedExtraction);
|
||||
|
||||
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
|
||||
|
||||
// Extract multiple entries concurrently
|
||||
@@ -62,7 +100,12 @@ public class ZipMultiThreadTests : TestBase
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
|
||||
var fileInfo = new FileInfo(testArchive);
|
||||
|
||||
using var archive = ZipArchive.OpenArchive(fileInfo);
|
||||
var options = new SharpCompress.Readers.ReaderOptions
|
||||
{
|
||||
EnableMultiThreadedExtraction = true,
|
||||
};
|
||||
|
||||
using var archive = ZipArchive.OpenArchive(fileInfo, options);
|
||||
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
|
||||
|
||||
// Extract multiple entries concurrently
|
||||
@@ -105,7 +148,12 @@ public class ZipMultiThreadTests : TestBase
|
||||
// Test concurrent extraction when opening from path (should use FileInfo internally)
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
|
||||
|
||||
using var archive = ZipArchive.OpenArchive(testArchive);
|
||||
var options = new SharpCompress.Readers.ReaderOptions
|
||||
{
|
||||
EnableMultiThreadedExtraction = true,
|
||||
};
|
||||
|
||||
using var archive = ZipArchive.OpenArchive(testArchive, options);
|
||||
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
|
||||
|
||||
// Extract multiple entries concurrently
|
||||
|
||||
Reference in New Issue
Block a user