I was wondering, how can I get the percentage of this being done, so I can display it on a progress bar?
ZipFile.CreateFromDirectory("C:\temp\folder", "C:\temp\folder.zip")
and also
ZipFile.ExtractToDirectory("C:\temp\folder.zip", "C:\temp\folder")
This doesnt have any events or callbacks that you can use to report progress. Simply means you cant with the .Net version. If you used the 7-Zip library you can do this easily.
I came across this question while checking for related questions for the identical question, asked for C# code. It is true that the .NET static ZipFile
class does not offer progress reporting. However, it is not hard to do using the ZipArchive
implementation, available since earlier versions of .NET.
The key is to use a Stream
wrapper that will report bytes read and written, and insert that in the data pipeline while creating or extracting the archive.
I wrote a version in C# for an answer to the other question, and since I didn't find any VB.NET examples, figured it would be helpful to include a VB.NET version on this question.
(Arguably, I could include both examples in a single answer and propose closing one of the questions as a duplicate of the other. But since it's doubtful the close vote would result in an actual closure, the connection between the two questions would not be as obvious as it should be. I think for best visibility to future users trying to find the solution appropriate for their needs, leaving this as two different questions is better.)
The foundation of the solution is the Stream
wrapper class:
StreamWithProgress.vb
Imports System.IO
Public Class StreamWithProgress
Inherits Stream
' NOTE For illustration purposes. For production code, one would want To
' override *all* of the virtual methods, delegating to the base _stream object,
' to ensure performance optimizations in the base _stream object aren't
' bypassed.
Private ReadOnly _stream As Stream
Private ReadOnly _readProgress As IProgress(Of Integer)
Private ReadOnly _writeProgress As IProgress(Of Integer)
Public Sub New(Stream As Stream, readProgress As IProgress(Of Integer), writeProgress As IProgress(Of Integer))
_stream = Stream
_readProgress = readProgress
_writeProgress = writeProgress
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return _stream.CanRead
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return _stream.CanSeek
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return _stream.CanWrite
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Return _stream.Length
End Get
End Property
Public Overrides Property Position As Long
Get
Return _stream.Position
End Get
Set(value As Long)
_stream.Position = value
End Set
End Property
Public Overrides Sub Flush()
_stream.Flush()
End Sub
Public Overrides Sub SetLength(value As Long)
_stream.SetLength(value)
End Sub
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Return _stream.Seek(offset, origin)
End Function
Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
_stream.Write(buffer, offset, count)
_writeProgress?.Report(count)
End Sub
Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
Dim bytesRead As Integer = _stream.Read(buffer, offset, count)
_readProgress?.Report(bytesRead)
Return bytesRead
End Function
End Class
The wrapper class can be used to implement progress-aware versions of the ZipFile
static methods:
ZipFileWithProgress.vb
Imports System.IO
Imports System.IO.Compression
NotInheritable Class ZipFileWithProgress
Private Sub New()
End Sub
Public Shared Sub CreateFromDirectory(
sourceDirectoryName As String,
destinationArchiveFileName As String,
progress As IProgress(Of Double))
sourceDirectoryName = Path.GetFullPath(sourceDirectoryName)
Dim sourceFiles As FileInfo() = New DirectoryInfo(sourceDirectoryName).GetFiles("*", SearchOption.AllDirectories)
Dim totalBytes As Double = sourceFiles.Sum(Function(f) f.Length)
Dim currentBytes As Long = 0
Using archive As ZipArchive = ZipFile.Open(destinationArchiveFileName, ZipArchiveMode.Create)
For Each fileInfo As FileInfo In sourceFiles
' NOTE: naive method To Get Sub-path from file name, relative to
' input directory. Production code should be more robust than this.
' Either use Path class Or similar to parse directory separators And
' reconstruct output file name, Or change this entire method to be
' recursive so that it can follow the sub-directories And include them
' in the entry name as they are processed.
Dim entryName As String = fileInfo.FullName.Substring(sourceDirectoryName.Length + 1)
Dim entry As ZipArchiveEntry = archive.CreateEntry(entryName)
entry.LastWriteTime = fileInfo.LastWriteTime
Using inputStream As Stream = File.OpenRead(fileInfo.FullName)
Using outputStream As Stream = entry.Open()
Dim progressStream As Stream = New StreamWithProgress(inputStream,
New BasicProgress(Of Integer)(
Sub(i)
currentBytes += i
progress.Report(currentBytes / totalBytes)
End Sub), Nothing)
progressStream.CopyTo(outputStream)
End Using
End Using
Next
End Using
End Sub
Public Shared Sub ExtractToDirectory(
sourceArchiveFileName As String,
destinationDirectoryName As String,
progress As IProgress(Of Double))
Using archive As ZipArchive = ZipFile.OpenRead(sourceArchiveFileName)
Dim totalBytes As Double = archive.Entries.Sum(Function(e) e.Length)
Dim currentBytes As Long = 0
For Each entry As ZipArchiveEntry In archive.Entries
Dim fileName As String = Path.Combine(destinationDirectoryName, entry.FullName)
Directory.CreateDirectory(Path.GetDirectoryName(fileName))
Using inputStream As Stream = entry.Open()
Using outputStream As Stream = File.OpenWrite(fileName)
Dim progressStream As Stream = New StreamWithProgress(outputStream, Nothing,
New BasicProgress(Of Integer)(
Sub(i)
currentBytes += i
progress.Report(currentBytes / totalBytes)
End Sub))
inputStream.CopyTo(progressStream)
End Using
End Using
File.SetLastWriteTime(fileName, entry.LastWriteTime.LocalDateTime)
Next
End Using
End Sub
End Class
The .NET built-in implementation of IProgress(Of T)
is intended for use in contexts where there is a UI thread where progress reporting events should be raised. As such, when used in a console program, like which I used to test this code, it will default to using the thread pool to raise the events, allowing for the possibility of out-of-order reports. To address this, the above uses a simpler implementation of IProgress(Of T)
, one that simply invokes the handler directly and synchronously.
BasicProgress.vb
Class BasicProgress(Of T)
Implements IProgress(Of T)
Private ReadOnly _handler As Action(Of T)
Public Sub New(handler As Action(Of T))
_handler = handler
End Sub
Private Sub Report(value As T) Implements IProgress(Of T).Report
_handler(value)
End Sub
End Class
And naturally, it's useful to have an example with which to test and demonstrate the code.
Module1.vb
Imports System.IO
Module Module1
Sub Main(args As String())
Dim sourceDirectory As String = args(0),
archive As String = args(1),
archiveDirectory As String = Path.GetDirectoryName(Path.GetFullPath(archive)),
unpackDirectoryName As String = Guid.NewGuid().ToString()
File.Delete(archive)
ZipFileWithProgress.CreateFromDirectory(sourceDirectory, archive,
New BasicProgress(Of Double)(
Sub(p)
Console.WriteLine($"{p:P2} archiving complete")
End Sub))
ZipFileWithProgress.ExtractToDirectory(archive, unpackDirectoryName,
New BasicProgress(Of Double)(
Sub(p)
Console.WriteLine($"{p:P0} extracting complete")
End Sub))
End Sub
End Module
Additional notes regarding this implementation can be found in my answer to the related question.