diff --git a/AGENTS.md b/AGENTS.md index c7b4a5a63..ae36265aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,12 +32,34 @@ SharpCompress is a pure C# compression library supporting multiple archive forma - Use CSharpier for code formatting to ensure consistent style across the project - CSharpier is configured as a local tool in `.config/dotnet-tools.json` -- **To format code, run the task: `format` task (which runs `dotnet csharpier .` from project root)** -- Restore tools with: `dotnet tool restore` -- Format files from the project root with: `dotnet csharpier .` -- Configure your IDE to format on save using CSharpier for the best experience + +### Commands + +1. **Restore tools** (first time only): + ```bash + dotnet tool restore + ``` + +2. **Check if files are formatted correctly** (doesn't modify files): + ```bash + dotnet csharpier check . + ``` + - Exit code 0: All files are properly formatted + - Exit code 1: Some files need formatting (will show which files and differences) + +3. **Format files** (modifies files): + ```bash + dotnet csharpier format . + ``` + - Formats all files in the project to match CSharpier style + - Run from project root directory + +4. **Configure your IDE** to format on save using CSharpier for the best experience + +### Additional Notes - The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.) - Let CSharpier handle code style while `.editorconfig` handles editor behavior +- Always run `dotnet csharpier check .` before committing to verify formatting ## Project Setup and Structure diff --git a/src/SharpCompress/Archives/IArchiveExtensions.cs b/src/SharpCompress/Archives/IArchiveExtensions.cs index d9701da30..0d39c6e2c 100644 --- a/src/SharpCompress/Archives/IArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveExtensions.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Readers; @@ -10,76 +10,159 @@ namespace SharpCompress.Archives; public static class IArchiveExtensions { - /// - /// Extract to specific directory, retaining filename - /// - public static void WriteToDirectory( - this IArchive archive, - string destinationDirectory, - ExtractionOptions? options = null - ) - { - using var reader = archive.ExtractAllEntries(); - reader.WriteAllToDirectory(destinationDirectory, options); - } - - /// - /// Extracts the archive to the destination directory. Directories will be created as needed. - /// /// The archive to extract. - /// The folder to extract into. - /// Optional progress report callback. - /// Optional cancellation token. - public static void ExtractToDirectory( - this IArchive archive, - string destination, - Action? progressReport = null, - CancellationToken cancellationToken = default - ) + extension(IArchive archive) { - // Prepare for progress reporting - var totalBytes = archive.TotalUncompressSize; - var bytesRead = 0L; - - // Tracking for created directories. - var seenDirectories = new HashSet(); + /// + /// Extract to specific directory with progress reporting + /// + /// The folder to extract into. + /// Extraction options. + /// Optional progress reporter for tracking extraction progress. + public void WriteToDirectory( + string destinationDirectory, + ExtractionOptions? options = null, + IProgress? progress = null + ) + { + // For solid archives (Rar, 7Zip), use the optimized reader-based approach + if (archive.IsSolid || archive.Type == ArchiveType.SevenZip) + { + using var reader = archive.ExtractAllEntries(); + reader.WriteAllToDirectory(destinationDirectory, options); + } + else + { + // For non-solid archives, extract entries directly + archive.WriteToDirectoryInternal(destinationDirectory, options, progress); + } + } - // Extract - foreach (var entry in archive.Entries) + private void WriteToDirectoryInternal( + string destinationDirectory, + ExtractionOptions? options, + IProgress? progress + ) { - cancellationToken.ThrowIfCancellationRequested(); + // Prepare for progress reporting + var totalBytes = archive.TotalUncompressSize; + var bytesRead = 0L; + + // Tracking for created directories. + var seenDirectories = new HashSet(); - if (entry.IsDirectory) + // Extract + foreach (var entry in archive.Entries) { - var dirPath = Path.Combine(destination, entry.Key.NotNull("Entry Key is null")); - if ( - Path.GetDirectoryName(dirPath + "/") is { } emptyDirectory - && seenDirectories.Add(dirPath) - ) + if (entry.IsDirectory) { - Directory.CreateDirectory(emptyDirectory); + var dirPath = Path.Combine( + destinationDirectory, + entry.Key.NotNull("Entry Key is null") + ); + if ( + Path.GetDirectoryName(dirPath + "/") is { } parentDirectory + && seenDirectories.Add(dirPath) + ) + { + Directory.CreateDirectory(parentDirectory); + } + continue; } - continue; + + // Use the entry's WriteToDirectory method which respects ExtractionOptions + entry.WriteToDirectory(destinationDirectory, options); + + // Update progress + bytesRead += entry.Size; + progress?.Report( + new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes) + ); } + } - // Create each directory if not already created - var path = Path.Combine(destination, entry.Key.NotNull("Entry Key is null")); - if (Path.GetDirectoryName(path) is { } directory) + /// + /// Extract to specific directory asynchronously with progress reporting and cancellation support + /// + /// The folder to extract into. + /// Extraction options. + /// Optional progress reporter for tracking extraction progress. + /// Optional cancellation token. + public async Task WriteToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + IProgress? progress = null, + CancellationToken cancellationToken = default + ) + { + // For solid archives (Rar, 7Zip), use the optimized reader-based approach + if (archive.IsSolid || archive.Type == ArchiveType.SevenZip) + { + using var reader = archive.ExtractAllEntries(); + await reader.WriteAllToDirectoryAsync( + destinationDirectory, + options, + cancellationToken + ); + } + else { - if (!Directory.Exists(directory) && !seenDirectories.Contains(directory)) + // For non-solid archives, extract entries directly + await archive.WriteToDirectoryAsyncInternal( + destinationDirectory, + options, + progress, + cancellationToken + ); + } + } + + private async Task WriteToDirectoryAsyncInternal( + string destinationDirectory, + ExtractionOptions? options, + IProgress? progress, + CancellationToken cancellationToken + ) + { + // Prepare for progress reporting + var totalBytes = archive.TotalUncompressSize; + var bytesRead = 0L; + + // Tracking for created directories. + var seenDirectories = new HashSet(); + + // Extract + foreach (var entry in archive.Entries) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (entry.IsDirectory) { - Directory.CreateDirectory(directory); - seenDirectories.Add(directory); + var dirPath = Path.Combine( + destinationDirectory, + entry.Key.NotNull("Entry Key is null") + ); + if ( + Path.GetDirectoryName(dirPath + "/") is { } parentDirectory + && seenDirectories.Add(dirPath) + ) + { + Directory.CreateDirectory(parentDirectory); + } + continue; } - } - // Write file - using var fs = File.OpenWrite(path); - entry.WriteTo(fs); + // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions + await entry + .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken) + .ConfigureAwait(false); - // Update progress - bytesRead += entry.Size; - progressReport?.Invoke(bytesRead / (double)totalBytes); + // Update progress + bytesRead += entry.Size; + progress?.Report( + new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes) + ); + } } } } diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 2f47e1215..916c9e2c0 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -261,7 +261,7 @@ protected void ArchiveExtractToDirectory( testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using (var archive = ArchiveFactory.Open(new FileInfo(testArchive), readerOptions)) { - archive.ExtractToDirectory(SCRATCH_FILES_PATH); + archive.WriteToDirectory(SCRATCH_FILES_PATH); } VerifyFiles(); } diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index 01fe9ca83..cd93a3c18 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -194,4 +194,38 @@ await entry.WriteToDirectoryAsync( } VerifyFiles(); } + + [Fact] + public async Task Zip_Deflate_Archive_WriteToDirectoryAsync() + { + using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"))) + using (var archive = ZipArchive.Open(stream)) + { + await archive.WriteToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + VerifyFiles(); + } + + [Fact] + public async Task Zip_Deflate_Archive_WriteToDirectoryAsync_WithProgress() + { + var progressReports = new System.Collections.Generic.List(); + var progress = new Progress(report => progressReports.Add(report)); + + using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"))) + using (var archive = ZipArchive.Open(stream)) + { + await archive.WriteToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, + progress + ); + } + + VerifyFiles(); + Assert.True(progressReports.Count > 0, "Progress reports should be generated"); + } }