Merge pull request #1077 from adamhathcock/copilot/provide-interface-for-entries

Remove ExtractAllEntries restriction for non-SOLID archives
This commit is contained in:
Adam Hathcock
2025-12-23 15:47:52 +00:00
committed by GitHub
7 changed files with 313 additions and 233 deletions

View File

@@ -87,20 +87,17 @@ memoryStream.Position = 0;
### Extract all files from a rar file to a directory using RarArchive
Note: Extracting a solid rar or 7z file needs to be done in sequential order to get acceptable decompression speed.
It is explicitly recommended to use `ExtractAllEntries` when extracting an entire `IArchive` instead of iterating over all its `Entries`.
Alternatively, use `IArchive.WriteToDirectory`.
`ExtractAllEntries` is primarily intended for solid archives (like solid Rar) or 7Zip archives, where sequential extraction provides the best performance. For general/simple extraction with any supported archive type, use `archive.WriteToDirectory()` instead.
```C#
using (var archive = RarArchive.Open("Test.rar"))
{
using (var reader = archive.ExtractAllEntries())
// Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types
archive.WriteToDirectory(@"D:\temp", new ExtractionOptions()
{
reader.WriteAllToDirectory(@"D:\temp", new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
ExtractFullPath = true,
Overwrite = true
});
}
```
@@ -116,6 +113,41 @@ using (var archive = RarArchive.Open("Test.rar"))
}
```
### Extract solid Rar or 7Zip archives with manual progress reporting
`ExtractAllEntries` only works for solid archives (Rar) or 7Zip archives. For optimal performance with these archive types, use this method:
```C#
using (var archive = RarArchive.Open("archive.rar")) // Must be solid Rar or 7Zip
{
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
{
// Calculate total size for progress reporting
double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size);
long completed = 0;
using (var reader = archive.ExtractAllEntries())
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryToDirectory(@"D:\output", new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
completed += reader.Entry.Size;
double progress = completed / totalSize;
Console.WriteLine($"Progress: {progress:P}");
}
}
}
}
}
```
### Use ReaderFactory to autodetect archive type and Open the entry stream
```C#
@@ -298,14 +330,12 @@ using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.
```C#
using (var archive = ZipArchive.Open("archive.zip"))
{
using (var reader = archive.ExtractAllEntries())
{
await reader.WriteAllToDirectoryAsync(
@"C:\output",
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
cancellationToken
);
}
// Simple async extraction - works for all archive types
await archive.WriteToDirectoryAsync(
@"C:\output",
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
cancellationToken
);
}
```

View File

@@ -109,7 +109,7 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive
{
if (!IsSolid && Type != ArchiveType.SevenZip)
{
throw new InvalidOperationException(
throw new SharpCompressException(
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
);
}

View File

@@ -11,52 +11,49 @@ public static class IArchiveEntryExtensions
{
private const int BufferSize = 81920;
/// <summary>
/// Extract entry to the specified stream.
/// </summary>
/// <param name="archiveEntry">The archive entry to extract.</param>
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public static void WriteTo(
this IArchiveEntry archiveEntry,
Stream streamToWriteTo,
IProgress<ProgressReport>? progress = null
)
extension(IArchiveEntry archiveEntry)
{
if (archiveEntry.IsDirectory)
/// <summary>
/// Extract entry to the specified stream.
/// </summary>
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public void WriteTo(Stream streamToWriteTo, IProgress<ProgressReport>? progress = null)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
using var entryStream = archiveEntry.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
sourceStream.CopyTo(streamToWriteTo, BufferSize);
}
using var entryStream = archiveEntry.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
sourceStream.CopyTo(streamToWriteTo, BufferSize);
}
/// <summary>
/// Extract entry to the specified stream asynchronously.
/// </summary>
/// <param name="archiveEntry">The archive entry to extract.</param>
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public static async Task WriteToAsync(
this IArchiveEntry archiveEntry,
Stream streamToWriteTo,
CancellationToken cancellationToken = default,
IProgress<ProgressReport>? progress = null
)
{
if (archiveEntry.IsDirectory)
/// <summary>
/// Extract entry to the specified stream asynchronously.
/// </summary>
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public async Task WriteToAsync(
Stream streamToWriteTo,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
using var entryStream = archiveEntry.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
await sourceStream
.CopyToAsync(streamToWriteTo, BufferSize, cancellationToken)
.ConfigureAwait(false);
using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken);
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
await sourceStream
.CopyToAsync(streamToWriteTo, BufferSize, cancellationToken)
.ConfigureAwait(false);
}
}
private static Stream WrapWithProgress(
@@ -71,7 +68,7 @@ public static class IArchiveEntryExtensions
}
var entryPath = entry.Key ?? string.Empty;
long? totalBytes = GetEntrySizeSafe(entry);
var totalBytes = GetEntrySizeSafe(entry);
return new ProgressReportingStream(
source,
progress,
@@ -94,77 +91,71 @@ public static class IArchiveEntryExtensions
}
}
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
public static void WriteToDirectory(
this IArchiveEntry entry,
string destinationDirectory,
ExtractionOptions? options = null
) =>
ExtractionMethods.WriteEntryToDirectory(
entry,
destinationDirectory,
options,
entry.WriteToFile
);
extension(IArchiveEntry entry)
{
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
public void WriteToDirectory(
string destinationDirectory,
ExtractionOptions? options = null
) =>
ExtractionMethods.WriteEntryToDirectory(
entry,
destinationDirectory,
options,
entry.WriteToFile
);
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
/// </summary>
public static Task WriteToDirectoryAsync(
this IArchiveEntry entry,
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
ExtractionMethods.WriteEntryToDirectoryAsync(
entry,
destinationDirectory,
options,
(x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken),
cancellationToken
);
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
/// </summary>
public Task WriteToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
ExtractionMethods.WriteEntryToDirectoryAsync(
entry,
destinationDirectory,
options,
entry.WriteToFileAsync,
cancellationToken
);
/// <summary>
/// Extract to specific file
/// </summary>
public static void WriteToFile(
this IArchiveEntry entry,
string destinationFileName,
ExtractionOptions? options = null
) =>
ExtractionMethods.WriteEntryToFile(
entry,
destinationFileName,
options,
(x, fm) =>
{
using var fs = File.Open(destinationFileName, fm);
entry.WriteTo(fs);
}
);
/// <summary>
/// Extract to specific file
/// </summary>
public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) =>
ExtractionMethods.WriteEntryToFile(
entry,
destinationFileName,
options,
(x, fm) =>
{
using var fs = File.Open(destinationFileName, fm);
entry.WriteTo(fs);
}
);
/// <summary>
/// Extract to specific file asynchronously
/// </summary>
public static Task WriteToFileAsync(
this IArchiveEntry entry,
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
ExtractionMethods.WriteEntryToFileAsync(
entry,
destinationFileName,
options,
async (x, fm) =>
{
using var fs = File.Open(destinationFileName, fm);
await entry
.WriteToAsync(fs, progress: null, cancellationToken: cancellationToken)
.ConfigureAwait(false);
},
cancellationToken
);
/// <summary>
/// Extract to specific file asynchronously
/// </summary>
public Task WriteToFileAsync(
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
ExtractionMethods.WriteEntryToFileAsync(
entry,
destinationFileName,
options,
async (x, fm, ct) =>
{
using var fs = File.Open(destinationFileName, fm);
await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
},
cancellationToken
);
}
}

View File

@@ -128,7 +128,7 @@ internal static class ExtractionMethods
IEntry entry,
string destinationDirectory,
ExtractionOptions? options,
Func<string, ExtractionOptions?, Task> writeAsync,
Func<string, ExtractionOptions?, CancellationToken, Task> writeAsync,
CancellationToken cancellationToken = default
)
{
@@ -189,7 +189,7 @@ internal static class ExtractionMethods
"Entry is trying to write a file outside of the destination directory."
);
}
await writeAsync(destinationFileName, options).ConfigureAwait(false);
await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false);
}
else if (options.ExtractFullPath && !Directory.Exists(destinationFileName))
{
@@ -201,7 +201,7 @@ internal static class ExtractionMethods
IEntry entry,
string destinationFileName,
ExtractionOptions? options,
Func<string, FileMode, Task> openAndWriteAsync,
Func<string, FileMode, CancellationToken, Task> openAndWriteAsync,
CancellationToken cancellationToken = default
)
{
@@ -225,7 +225,8 @@ internal static class ExtractionMethods
fm = FileMode.CreateNew;
}
await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false);
await openAndWriteAsync(destinationFileName, fm, cancellationToken)
.ConfigureAwait(false);
entry.PreserveExtractionOptions(destinationFileName, options);
}
}

View File

@@ -7,124 +7,121 @@ namespace SharpCompress.Readers;
public static class IReaderExtensions
{
public static void WriteEntryTo(this IReader reader, string filePath)
extension(IReader reader)
{
using Stream stream = File.Open(filePath, FileMode.Create, FileAccess.Write);
reader.WriteEntryTo(stream);
}
public static void WriteEntryTo(this IReader reader, FileInfo filePath)
{
using Stream stream = filePath.Open(FileMode.Create);
reader.WriteEntryTo(stream);
}
/// <summary>
/// Extract all remaining unread entries to specific directory, retaining filename
/// </summary>
public static void WriteAllToDirectory(
this IReader reader,
string destinationDirectory,
ExtractionOptions? options = null
)
{
while (reader.MoveToNextEntry())
public void WriteEntryTo(string filePath)
{
reader.WriteEntryToDirectory(destinationDirectory, options);
using Stream stream = File.Open(filePath, FileMode.Create, FileAccess.Write);
reader.WriteEntryTo(stream);
}
}
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
public static void WriteEntryToDirectory(
this IReader reader,
string destinationDirectory,
ExtractionOptions? options = null
) =>
ExtractionMethods.WriteEntryToDirectory(
reader.Entry,
destinationDirectory,
options,
reader.WriteEntryToFile
);
public void WriteEntryTo(FileInfo filePath)
{
using Stream stream = filePath.Open(FileMode.Create);
reader.WriteEntryTo(stream);
}
/// <summary>
/// Extract to specific file
/// </summary>
public static void WriteEntryToFile(
this IReader reader,
string destinationFileName,
ExtractionOptions? options = null
) =>
ExtractionMethods.WriteEntryToFile(
reader.Entry,
destinationFileName,
options,
(x, fm) =>
/// <summary>
/// Extract all remaining unread entries to specific directory, retaining filename
/// </summary>
public void WriteAllToDirectory(
string destinationDirectory,
ExtractionOptions? options = null
)
{
while (reader.MoveToNextEntry())
{
using var fs = File.Open(destinationFileName, fm);
reader.WriteEntryTo(fs);
reader.WriteEntryToDirectory(destinationDirectory, options);
}
);
}
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
/// </summary>
public static async Task WriteEntryToDirectoryAsync(
this IReader reader,
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToDirectoryAsync(
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
public void WriteEntryToDirectory(
string destinationDirectory,
ExtractionOptions? options = null
) =>
ExtractionMethods.WriteEntryToDirectory(
reader.Entry,
destinationDirectory,
options,
(fileName, opts) => reader.WriteEntryToFileAsync(fileName, opts, cancellationToken),
cancellationToken
)
.ConfigureAwait(false);
reader.WriteEntryToFile
);
/// <summary>
/// Extract to specific file asynchronously
/// </summary>
public static async Task WriteEntryToFileAsync(
this IReader reader,
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToFileAsync(
/// <summary>
/// Extract to specific file
/// </summary>
public void WriteEntryToFile(
string destinationFileName,
ExtractionOptions? options = null
) =>
ExtractionMethods.WriteEntryToFile(
reader.Entry,
destinationFileName,
options,
async (x, fm) =>
(x, fm) =>
{
using var fs = File.Open(destinationFileName, fm);
await reader.WriteEntryToAsync(fs, cancellationToken).ConfigureAwait(false);
},
cancellationToken
)
.ConfigureAwait(false);
reader.WriteEntryTo(fs);
}
);
/// <summary>
/// Extract all remaining unread entries to specific directory asynchronously, retaining filename
/// </summary>
public static async Task WriteAllToDirectoryAsync(
this IReader reader,
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
)
{
while (reader.MoveToNextEntry())
{
await reader
.WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken)
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
/// </summary>
public async Task WriteEntryToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToDirectoryAsync(
reader.Entry,
destinationDirectory,
options,
reader.WriteEntryToFileAsync,
cancellationToken
)
.ConfigureAwait(false);
/// <summary>
/// Extract to specific file asynchronously
/// </summary>
public async Task WriteEntryToFileAsync(
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToFileAsync(
reader.Entry,
destinationFileName,
options,
async (x, fm, ct) =>
{
using var fs = File.Open(destinationFileName, fm);
await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false);
},
cancellationToken
)
.ConfigureAwait(false);
/// <summary>
/// Extract all remaining unread entries to specific directory asynchronously, retaining filename
/// </summary>
public async Task WriteAllToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
)
{
while (await reader.MoveToNextEntryAsync(cancellationToken))
{
await reader
.WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken)
.ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Readers;
using Xunit;
namespace SharpCompress.Test;
/// <summary>
/// Tests for the ExtractAllEntries method behavior on both solid and non-solid
/// archives, including progress reporting and current usage restrictions.
/// </summary>
public class ExtractAllEntriesTests : TestBase
{
[Fact]
public void ExtractAllEntries_WithProgressReporting_NonSolidArchive()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip");
using var archive = ArchiveFactory.Open(archivePath);
Assert.Throws<SharpCompressException>(() =>
{
using var reader = archive.ExtractAllEntries();
});
}
[Fact]
public void ExtractAllEntries_WithProgressReporting_SolidArchive()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Rar.solid.rar");
using var archive = ArchiveFactory.Open(archivePath);
Assert.True(archive.IsSolid);
// Calculate total size like user code does
double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size);
long completed = 0;
var progressReports = 0;
using var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
completed += reader.Entry.Size;
var progress = completed / totalSize;
progressReports++;
Assert.True(progress >= 0 && progress <= 1.0);
}
}
Assert.True(progressReports > 0);
}
}

View File

@@ -188,7 +188,7 @@ public class ProgressReportTests : TestBase
if (!entry.IsDirectory)
{
using var extractedStream = new MemoryStream();
await entry.WriteToAsync(extractedStream, CancellationToken.None, progress);
await entry.WriteToAsync(extractedStream, progress, CancellationToken.None);
}
}
@@ -410,7 +410,7 @@ public class ProgressReportTests : TestBase
if (!entry.IsDirectory)
{
using var extractedStream = new MemoryStream();
await entry.WriteToAsync(extractedStream, CancellationToken.None, progress);
await entry.WriteToAsync(extractedStream, progress, CancellationToken.None);
}
}