diff --git a/USAGE.md b/USAGE.md index a06d2e83..1e3a6c78 100644 --- a/USAGE.md +++ b/USAGE.md @@ -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 + ); } ``` diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 86bee0cd..672382ff 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -109,7 +109,7 @@ public abstract class AbstractArchive : 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)." ); } diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 1ec3bfec..af2c9be4 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -11,52 +11,49 @@ public static class IArchiveEntryExtensions { private const int BufferSize = 81920; - /// - /// Extract entry to the specified stream. - /// /// The archive entry to extract. - /// The stream to write the entry content to. - /// Optional progress reporter for tracking extraction progress. - public static void WriteTo( - this IArchiveEntry archiveEntry, - Stream streamToWriteTo, - IProgress? progress = null - ) + extension(IArchiveEntry archiveEntry) { - if (archiveEntry.IsDirectory) + /// + /// Extract entry to the specified stream. + /// + /// The stream to write the entry content to. + /// Optional progress reporter for tracking extraction progress. + public void WriteTo(Stream streamToWriteTo, IProgress? 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); - } - - /// - /// Extract entry to the specified stream asynchronously. - /// - /// The archive entry to extract. - /// The stream to write the entry content to. - /// Cancellation token. - /// Optional progress reporter for tracking extraction progress. - public static async Task WriteToAsync( - this IArchiveEntry archiveEntry, - Stream streamToWriteTo, - CancellationToken cancellationToken = default, - IProgress? progress = null - ) - { - if (archiveEntry.IsDirectory) + /// + /// Extract entry to the specified stream asynchronously. + /// + /// The stream to write the entry content to. + /// Cancellation token. + /// Optional progress reporter for tracking extraction progress. + public async Task WriteToAsync( + Stream streamToWriteTo, + IProgress? 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 } } - /// - /// Extract to specific directory, retaining filename - /// - public static void WriteToDirectory( - this IArchiveEntry entry, - string destinationDirectory, - ExtractionOptions? options = null - ) => - ExtractionMethods.WriteEntryToDirectory( - entry, - destinationDirectory, - options, - entry.WriteToFile - ); + extension(IArchiveEntry entry) + { + /// + /// Extract to specific directory, retaining filename + /// + public void WriteToDirectory( + string destinationDirectory, + ExtractionOptions? options = null + ) => + ExtractionMethods.WriteEntryToDirectory( + entry, + destinationDirectory, + options, + entry.WriteToFile + ); - /// - /// Extract to specific directory asynchronously, retaining filename - /// - 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 - ); + /// + /// Extract to specific directory asynchronously, retaining filename + /// + public Task WriteToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + ExtractionMethods.WriteEntryToDirectoryAsync( + entry, + destinationDirectory, + options, + entry.WriteToFileAsync, + cancellationToken + ); - /// - /// Extract to specific file - /// - 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); - } - ); + /// + /// Extract to specific file + /// + 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); + } + ); - /// - /// Extract to specific file asynchronously - /// - 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 - ); + /// + /// Extract to specific file asynchronously + /// + 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 + ); + } } diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 485fdf4d..509524b1 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -128,7 +128,7 @@ internal static class ExtractionMethods IEntry entry, string destinationDirectory, ExtractionOptions? options, - Func writeAsync, + Func 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 openAndWriteAsync, + Func 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); } } diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs index 6480df1d..65c6b1fa 100644 --- a/src/SharpCompress/Readers/IReaderExtensions.cs +++ b/src/SharpCompress/Readers/IReaderExtensions.cs @@ -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); - } - - /// - /// Extract all remaining unread entries to specific directory, retaining filename - /// - 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); } - } - /// - /// Extract to specific directory, retaining filename - /// - 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); + } - /// - /// Extract to specific file - /// - public static void WriteEntryToFile( - this IReader reader, - string destinationFileName, - ExtractionOptions? options = null - ) => - ExtractionMethods.WriteEntryToFile( - reader.Entry, - destinationFileName, - options, - (x, fm) => + /// + /// Extract all remaining unread entries to specific directory, retaining filename + /// + 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); } - ); + } - /// - /// Extract to specific directory asynchronously, retaining filename - /// - public static async Task WriteEntryToDirectoryAsync( - this IReader reader, - string destinationDirectory, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - await ExtractionMethods - .WriteEntryToDirectoryAsync( + /// + /// Extract to specific directory, retaining filename + /// + 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 + ); - /// - /// Extract to specific file asynchronously - /// - public static async Task WriteEntryToFileAsync( - this IReader reader, - string destinationFileName, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - await ExtractionMethods - .WriteEntryToFileAsync( + /// + /// Extract to specific file + /// + 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); + } + ); - /// - /// Extract all remaining unread entries to specific directory asynchronously, retaining filename - /// - 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) + /// + /// Extract to specific directory asynchronously, retaining filename + /// + public async Task WriteEntryToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + await ExtractionMethods + .WriteEntryToDirectoryAsync( + reader.Entry, + destinationDirectory, + options, + reader.WriteEntryToFileAsync, + cancellationToken + ) .ConfigureAwait(false); + + /// + /// Extract to specific file asynchronously + /// + 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); + + /// + /// Extract all remaining unread entries to specific directory asynchronously, retaining filename + /// + 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); + } } } } diff --git a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs new file mode 100644 index 00000000..007f620f --- /dev/null +++ b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Linq; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Readers; +using Xunit; + +namespace SharpCompress.Test; + +/// +/// Tests for the ExtractAllEntries method behavior on both solid and non-solid +/// archives, including progress reporting and current usage restrictions. +/// +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(() => + { + 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); + } +} diff --git a/tests/SharpCompress.Test/ProgressReportTests.cs b/tests/SharpCompress.Test/ProgressReportTests.cs index 58ea4304..75fa2116 100644 --- a/tests/SharpCompress.Test/ProgressReportTests.cs +++ b/tests/SharpCompress.Test/ProgressReportTests.cs @@ -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); } }