diff --git a/SevenZipExtractor.Tests/Test7Zip.cs b/SevenZipExtractor.Tests/Test7Zip.cs index 97cccfd..4649b7d 100644 --- a/SevenZipExtractor.Tests/Test7Zip.cs +++ b/SevenZipExtractor.Tests/Test7Zip.cs @@ -19,5 +19,17 @@ public void TestKnownFormatAndExtractToStream_OK() { this.TestExtractToStream(Resources.TestFiles.SevenZip, this.TestEntriesWithoutFolder, SevenZipFormat.SevenZip); } + + [TestMethod] + public void TestProgressWithArchiveExtraction_OK() + { + this.TestExtractArchiveWithProgress(Resources.TestFiles.SevenZip, SevenZipFormat.SevenZip); + } + + [TestMethod] + public void TestProgressWithEntryExtraction_OK() + { + this.TestExtractEntriesWithProgress(Resources.TestFiles.SevenZip, SevenZipFormat.SevenZip); + } } } \ No newline at end of file diff --git a/SevenZipExtractor.Tests/TestArj.cs b/SevenZipExtractor.Tests/TestArj.cs index 0af195f..ae372a9 100644 --- a/SevenZipExtractor.Tests/TestArj.cs +++ b/SevenZipExtractor.Tests/TestArj.cs @@ -21,5 +21,17 @@ public void Text_UnboxAndCast_OK() this.TestExtractToStream(Resources.TestFiles.ansimate_arj, testEntries, SevenZipFormat.Arj); } + + [TestMethod] + public void TestProgressWithArchiveExtraction_OK() + { + this.TestExtractArchiveWithProgress(Resources.TestFiles.ansimate_arj, SevenZipFormat.Arj); + } + + [TestMethod] + public void TestProgressWithEntryExtraction_OK() + { + this.TestExtractEntriesWithProgress(Resources.TestFiles.ansimate_arj, SevenZipFormat.Arj); + } } } \ No newline at end of file diff --git a/SevenZipExtractor.Tests/TestBase.cs b/SevenZipExtractor.Tests/TestBase.cs index 00bb63c..3441856 100644 --- a/SevenZipExtractor.Tests/TestBase.cs +++ b/SevenZipExtractor.Tests/TestBase.cs @@ -56,7 +56,95 @@ protected void TestExtractToStream(byte[] archiveBytes, IList exp } } } + } + protected void TestExtractEntriesWithProgress(byte[] archiveBytes, SevenZipFormat? sevenZipFormat = null) + { + MemoryStream memoryStream = new MemoryStream(archiveBytes); + + using (ArchiveFile archiveFile = new ArchiveFile(memoryStream, sevenZipFormat)) + { + foreach (var entry in archiveFile.Entries) + { + if (entry.IsFolder) + { + continue; + } + + using (MemoryStream entryMemoryStream = new MemoryStream()) + { + bool progressCalledAtBeginning = false; + bool progressCalledAtEnd = false; + + entry.Extract(entryMemoryStream, (s, e) => + { + if (e.Total > 0) + { + if (e.Completed == 0) + { + progressCalledAtBeginning = true; + } + else if (e.Completed == e.Total) + { + progressCalledAtEnd = true; + } + } + }); + + Assert.IsTrue(progressCalledAtBeginning, $"Progress callback was not called at the beginning of extracting file {entry.FileName}."); + Assert.IsTrue(progressCalledAtEnd, $"Progress callback was not called at the end of extracting file {entry.FileName}."); + } + } + } + } + + protected void TestExtractArchiveWithProgress(byte[] archiveBytes, SevenZipFormat? sevenZipFormat = null) + { + MemoryStream memoryStream = new MemoryStream(archiveBytes); + + string tempPath = Path.Combine(Path.GetTempPath(), "SevenZipExtractorUnitTests"); + Directory.CreateDirectory(tempPath); + + try + { + using (ArchiveFile archiveFile = new ArchiveFile(memoryStream, sevenZipFormat)) + { + int progressCalledAtBeginning = 0; + int progressCalledAtEnd = 0; + HashSet progressCalledForIndex = new HashSet(); + HashSet reportedTotalEntryCounts = new HashSet(); + + archiveFile.Extract(tempPath, true, (s, e) => + { + reportedTotalEntryCounts.Add(e.EntryCount); + progressCalledForIndex.Add(e.EntryIndex); + + if (e.Total > 0) + { + if (e.Completed == 0) + { + progressCalledAtBeginning++; + } + else if (e.Completed == e.Total) + { + progressCalledAtEnd++; + } + } + }); + + Assert.AreEqual(1, reportedTotalEntryCounts.Count, "None or more than one total file count reported in progress callback."); + + var entryCount = reportedTotalEntryCounts.First(); + Assert.IsTrue(entryCount > 0, "No entries to extract in test archive, or progress callback never called, or progress callback called with zero total entry count."); + Assert.AreEqual(entryCount, progressCalledForIndex.Count, "Progress callback was not called at all for one or more entries or was called more than expected."); + Assert.IsTrue(progressCalledAtBeginning > 0, "Progress callback was not called at the beginning of extraction."); + Assert.IsTrue(progressCalledAtEnd > 0, "Progress callback was not called at the end of extraction."); + } + } + finally + { + Directory.Delete(tempPath, true); + } } } } \ No newline at end of file diff --git a/SevenZipExtractor.Tests/TestLzh.cs b/SevenZipExtractor.Tests/TestLzh.cs index 0fe0f23..3e299f2 100644 --- a/SevenZipExtractor.Tests/TestLzh.cs +++ b/SevenZipExtractor.Tests/TestLzh.cs @@ -21,5 +21,17 @@ public void TestKnownFormatAndExtractToStream_OK() { this.TestExtractToStream(Resources.TestFiles.lzh, this.TestEntriesWithoutFolder, SevenZipFormat.Lzh); } + + [TestMethod] + public void TestProgressWithArchiveExtraction_OK() + { + this.TestExtractArchiveWithProgress(Resources.TestFiles.lzh, SevenZipFormat.Lzh); + } + + [TestMethod] + public void TestProgressWithEntryExtraction_OK() + { + this.TestExtractEntriesWithProgress(Resources.TestFiles.lzh, SevenZipFormat.Lzh); + } } } \ No newline at end of file diff --git a/SevenZipExtractor.Tests/TestRar.cs b/SevenZipExtractor.Tests/TestRar.cs index 4ba0242..548850e 100644 --- a/SevenZipExtractor.Tests/TestRar.cs +++ b/SevenZipExtractor.Tests/TestRar.cs @@ -15,6 +15,18 @@ public void TestGuessAndExtractToStream_OK() public void TestKnownFormatAndExtractToStream_OK() { this.TestExtractToStream(Resources.TestFiles.rar, this.TestEntriesWithFolder, SevenZipFormat.Rar5); + } + + [TestMethod] + public void TestProgressWithArchiveExtraction_OK() + { + this.TestExtractArchiveWithProgress(Resources.TestFiles.rar, SevenZipFormat.Rar5); + } + + [TestMethod] + public void TestProgressWithEntryExtraction_OK() + { + this.TestExtractEntriesWithProgress(Resources.TestFiles.rar, SevenZipFormat.Rar5); } } } \ No newline at end of file diff --git a/SevenZipExtractor.Tests/TestZip.cs b/SevenZipExtractor.Tests/TestZip.cs index de71bf3..d7d505e 100644 --- a/SevenZipExtractor.Tests/TestZip.cs +++ b/SevenZipExtractor.Tests/TestZip.cs @@ -15,6 +15,18 @@ public void TestGuessAndExtractToStream_OK() public void TestKnownFormatAndExtractToStream_OK() { this.TestExtractToStream(Resources.TestFiles.zip, this.TestEntriesWithFolder, SevenZipFormat.Zip); + } + + [TestMethod] + public void TestProgressWithArchiveExtraction_OK() + { + this.TestExtractArchiveWithProgress(Resources.TestFiles.zip, SevenZipFormat.Zip); + } + + [TestMethod] + public void TestProgressWithEntryExtraction_OK() + { + this.TestExtractEntriesWithProgress(Resources.TestFiles.zip, SevenZipFormat.Zip); } } } \ No newline at end of file diff --git a/SevenZipExtractor/ArchiveExtractionProgressEventArgs.cs b/SevenZipExtractor/ArchiveExtractionProgressEventArgs.cs new file mode 100644 index 0000000..0c90c31 --- /dev/null +++ b/SevenZipExtractor/ArchiveExtractionProgressEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SevenZipExtractor +{ + public class ArchiveExtractionProgressEventArgs : EntryExtractionProgressEventArgs + { + /// + /// The index of the entry currently being extracted. + /// + public uint EntryIndex {get; } + + /// + /// The total number of entries that will be extracted. + /// + public int EntryCount {get; } + + internal ArchiveExtractionProgressEventArgs(uint entryIndex, int entryCount, ulong completed, ulong total) : base(completed, total) + { + EntryIndex = entryIndex; + EntryCount = entryCount; + } + } +} diff --git a/SevenZipExtractor/ArchiveFile.cs b/SevenZipExtractor/ArchiveFile.cs index 8ccf66b..80d393a 100644 --- a/SevenZipExtractor/ArchiveFile.cs +++ b/SevenZipExtractor/ArchiveFile.cs @@ -75,7 +75,7 @@ public ArchiveFile(Stream archiveStream, SevenZipFormat? format = null, string l this.archiveStream = new InStreamWrapper(archiveStream); } - public void Extract(string outputFolder, bool overwrite = false) + public void Extract(string outputFolder, bool overwrite = false, EventHandler progressEventHandler = null) { this.Extract(entry => { @@ -92,10 +92,10 @@ public void Extract(string outputFolder, bool overwrite = false) } return null; - }); + }, progressEventHandler); } - public void Extract(Func getOutputPath) + public void Extract(Func getOutputPath, EventHandler progressEventHandler = null) { IList fileStreams = new List(); @@ -128,7 +128,9 @@ public void Extract(Func getOutputPath) fileStreams.Add(File.Create(outputPath)); } - this.archive.Extract(null, 0xFFFFFFFF, 0, new ArchiveStreamsCallback(fileStreams)); + ArchiveStreamsCallback extractCallback = new ArchiveStreamsCallback(fileStreams, progressEventHandler); + this.archive.Extract(null, 0xFFFFFFFF, 0, extractCallback); + extractCallback.InvokeFinalProgressCallback(); } finally { diff --git a/SevenZipExtractor/ArchiveStreamCallback.cs b/SevenZipExtractor/ArchiveStreamCallback.cs index e71e225..1335ba2 100644 --- a/SevenZipExtractor/ArchiveStreamCallback.cs +++ b/SevenZipExtractor/ArchiveStreamCallback.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; namespace SevenZipExtractor { @@ -6,19 +7,29 @@ internal class ArchiveStreamCallback : IArchiveExtractCallback { private readonly uint fileNumber; private readonly Stream stream; + private readonly EventHandler progressEventHandler; + + private ulong currentCompleteValue; + private ulong currentTotal; + private bool finalProgressReported = false; - public ArchiveStreamCallback(uint fileNumber, Stream stream) + public ArchiveStreamCallback(uint fileNumber, Stream stream, EventHandler progressEventHandler) { this.fileNumber = fileNumber; this.stream = stream; + this.progressEventHandler = progressEventHandler; } public void SetTotal(ulong total) { + this.currentTotal = total; + this.InvokeProgressCallback(); } public void SetCompleted(ref ulong completeValue) { + this.currentCompleteValue = completeValue; + this.InvokeProgressCallback(); } public int GetStream(uint index, out ISequentialOutStream outStream, AskMode askExtractMode) @@ -41,5 +52,27 @@ public void PrepareOperation(AskMode askExtractMode) public void SetOperationResult(OperationResult resultEOperationResult) { } + + public void InvokeFinalProgressCallback() + { + if (!this.finalProgressReported) + { + // 7z doesn't invoke SetCompleted for all formats when an entry is fully extracted, so we fake it. + this.SetCompleted(ref this.currentTotal); + } + } + + private void InvokeProgressCallback() + { + progressEventHandler?.Invoke( + this, + new EntryExtractionProgressEventArgs(this.currentCompleteValue, this.currentTotal) + ); + + if (this.currentCompleteValue == this.currentTotal) + { + this.finalProgressReported = true; + } + } } } \ No newline at end of file diff --git a/SevenZipExtractor/ArchiveStreamsCallback.cs b/SevenZipExtractor/ArchiveStreamsCallback.cs index b8f1fca..c4fd83b 100644 --- a/SevenZipExtractor/ArchiveStreamsCallback.cs +++ b/SevenZipExtractor/ArchiveStreamsCallback.cs @@ -1,27 +1,50 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; namespace SevenZipExtractor { internal class ArchiveStreamsCallback : IArchiveExtractCallback { private readonly IList streams; + private readonly int streamCount; + private readonly EventHandler progressEventHandler; - public ArchiveStreamsCallback(IList streams) + private uint currentIndex; + private ulong currentTotal; + private ulong currentCompleteValue; + private bool isCurrentValidForProgress; + private bool finalProgressReported = false; + + public ArchiveStreamsCallback(IList streams, EventHandler progressEventHandler) { this.streams = streams; + this.streamCount = streams.Where(s => s != null).Count(); + this.progressEventHandler = progressEventHandler; } public void SetTotal(ulong total) { + this.currentTotal = total; } public void SetCompleted(ref ulong completeValue) { + this.currentCompleteValue = completeValue; + + // If completeValue is 0, currentIndex has not yet been set correctly, since GetStream is initially called after SetCompleted. + if (completeValue > 0 && this.isCurrentValidForProgress) + { + this.InvokeProgressCallback(); + } } public int GetStream(uint index, out ISequentialOutStream outStream, AskMode askExtractMode) { + this.currentIndex = index; + this.isCurrentValidForProgress = false; + if (askExtractMode != AskMode.kExtract) { outStream = null; @@ -34,7 +57,7 @@ public int GetStream(uint index, out ISequentialOutStream outStream, AskMode ask return 0; } - Stream stream = this.streams[(int) index]; + Stream stream = this.streams[(int)index]; if (stream == null) { @@ -42,6 +65,10 @@ public int GetStream(uint index, out ISequentialOutStream outStream, AskMode ask return 0; } + // SetTotal and SetCompleted are called before GetStream, so now that currentIndex is correct, we invoke the progress callback. + this.isCurrentValidForProgress = true; + this.InvokeProgressCallback(); + outStream = new OutStreamWrapper(stream); return 0; @@ -54,5 +81,27 @@ public void PrepareOperation(AskMode askExtractMode) public void SetOperationResult(OperationResult resultEOperationResult) { } + + public void InvokeFinalProgressCallback() + { + if (!this.finalProgressReported) + { + // 7z doesn't invoke SetCompleted for all formats when an entry is fully extracted, so we fake it. + this.SetCompleted(ref this.currentTotal); + } + } + + private void InvokeProgressCallback() + { + progressEventHandler?.Invoke( + this, + new ArchiveExtractionProgressEventArgs(this.currentIndex, this.streamCount, this.currentCompleteValue, this.currentTotal) + ); + + if (this.currentCompleteValue == this.currentTotal) + { + this.finalProgressReported = true; + } + } } } \ No newline at end of file diff --git a/SevenZipExtractor/Entry.cs b/SevenZipExtractor/Entry.cs index 13220b0..d5cf8ce 100644 --- a/SevenZipExtractor/Entry.cs +++ b/SevenZipExtractor/Entry.cs @@ -86,7 +86,7 @@ internal Entry(IInArchive archive, uint index) /// public bool IsSplitAfter { get; set; } - public void Extract(string fileName, bool preserveTimestamp = true) + public void Extract(string fileName, bool preserveTimestamp = true, EventHandler progressEventHandler = null) { if (this.IsFolder) { @@ -103,7 +103,7 @@ public void Extract(string fileName, bool preserveTimestamp = true) using (FileStream fileStream = File.Create(fileName)) { - this.Extract(fileStream); + this.Extract(fileStream, progressEventHandler); } if (preserveTimestamp) @@ -111,9 +111,12 @@ public void Extract(string fileName, bool preserveTimestamp = true) File.SetLastWriteTime(fileName, this.LastWriteTime); } } - public void Extract(Stream stream) + + public void Extract(Stream stream, EventHandler progressEventHandler = null) { - this.archive.Extract(new[] { this.index }, 1, 0, new ArchiveStreamCallback(this.index, stream)); + ArchiveStreamCallback extractCallback = new ArchiveStreamCallback(this.index, stream, progressEventHandler); + this.archive.Extract(new[] { this.index }, 1, 0, extractCallback); + extractCallback.InvokeFinalProgressCallback(); } } } diff --git a/SevenZipExtractor/EntryExtractionProgressEventArgs.cs b/SevenZipExtractor/EntryExtractionProgressEventArgs.cs new file mode 100644 index 0000000..b94443d --- /dev/null +++ b/SevenZipExtractor/EntryExtractionProgressEventArgs.cs @@ -0,0 +1,23 @@ +using System; + +namespace SevenZipExtractor +{ + public class EntryExtractionProgressEventArgs : EventArgs + { + /// + /// Number of bytes completed. Can be packed or unpacked size depending on format. + /// + public ulong Completed { get; } + + /// + /// The total number of bytes to extract. Not set for some formats. + /// + public ulong Total { get; } + + internal EntryExtractionProgressEventArgs(ulong completed, ulong total) + { + Completed = completed; + Total = total; + } + } +} \ No newline at end of file