VB.Net 4.5 压缩与进度






4.96/5 (8投票s)
.Net 4.5 压缩命名空间,压缩任意大小的文件
引言
VB.Net 中有许多压缩文件的示例。其中许多示例都使用了第三方库,这完全没有必要。人们曾说超过 4Gb 的文件和存档无法使用……我讨厌限制,并决定“对抗微软”
为了更好地理解 Zip 文件及其内容,我从头开始编写了自己的 Zip 文件生成器(仍在使用 Phil Katz 的 Zip 算法)。最终,我成功地逐字节构建了自己的 Zip 和 ZIP64 存档。
然后我审视了自己的工作,心想,现在我掌握了 Zip 文件(使用 PKZip 算法)的内部工作原理,让我们重新审视压缩命名空间,以确保我没有浪费时间。事实证明,我从这个项目中所获得的一切,就是对 Zip 文件以及如何内部构建它们的扎实的工作知识,因为那些关于 4Gb 限制等的传闻完全是胡说八道,Compression 命名空间会根据需要(即时)将大文件转换为 Zip64。
这是我的压缩方法, nicely 封装在一个易于使用的类中。它使用 .Net4.5 Compression 命名空间,直接从磁盘读取/写入文件。如果您在压缩过程中观察系统性能,不会使用额外的内存。
我希望这能对大家未来的工作有所帮助。
第一部分:Zipper 类详解
第二部分:复制、粘贴并运行。完整源代码
Zipper 类
属性
由于大多数属性名称都是自解释的,我将不再一一赘述,但可以在每个属性的 setter 方法中添加一个功能,以防止在压缩过程中进行更改。
如果我们向 Zipper 类添加一个私有成员(例如 _Compressing,一个布尔值),并在调用压缩方法时将其设置为 true,在进程完成后设置为 false,然后对每个 setter 方法执行以下操作,就可以防止在压缩过程中用户更改属性而导致的潜在错误。
Private _Source As String
Public Property SourceURL As String
Get
Return _Source
End Get
Set(value As String)
'Add this code
If _Compressing = True Then Exit Property
_Source = value
End Set
End Property
方法
GetSessionLength
Private Function GetSessionLength() As Int64
Dim sLen As Int64 = 0
For Each SessionFile As String In _SessionFiles
sLen += New FileInfo(SessionFile).Length
If Cancel = True Then Exit For
Next
Return sLen
End Function
当调用 Compression 方法时,所有文件/文件路径都会被添加到 List(Of String) _SessionFiles 中。此方法只需遍历每个条目并计算要读取和压缩的所有文件的总长度(字节)。这些信息以后可用于报告压缩过程的当前进度。
IsDir
Private Function IsDir(Source As String) As Int16
If File.Exists(Source) Then
Return 0
ElseIf Directory.Exists(Source) Then
Return -1
Else
Return 1
End If
End Function
此方法非常简单地返回一个值,该值确定源对象是文件、文件夹还是不存在。如果实际上是目录,则返回 -1 或 True;如果是文件,则返回 0 或 False;如果不存在,则返回 1。此方法在 Zipper 构造方法 New 中使用。
If IsDir(Source) <> 1 Then
_IsDir = IsDir(Source)
_Source = Source
Else
Throw New Exception("Source file or directory _
doesn't exist or cannot be accessed.")
End If
从这段代码可以看出,如果返回的值不是布尔值(-1 或 0),则会抛出异常。如果返回的值是布尔值,则设置 _IsDir 和 _Source 私有成员。
InlineAssignHelper
Private Function InlineAssignHelper(Of T)(ByRef target As T, _
value As T) As T
target = value
Return value
End Function
(我认为)此方法最初来自 C#,VB 中没有命名空间,因此必须手动添加。此方法只是将 Target 引用设置为参数“Value”并返回其值。在压缩例程中,它用于确定从文件中读取了多少字节。它基本上在同一次调用中返回两个不同位置(Target 和返回值)的相同值。
压缩
这是用户使用的主要公共方法,但不包含压缩例程,此方法为主要压缩方法“ZipIt”准备信息。
首先,将填充 _SessionFiles 列表,用户可以传入一个包含子目录和文件的目录,或者单个文件。如果源是目录,我们使用 Directory.Getfiles 方法扫描其中包含的所有文件。
_SessionFiles = Directory.GetFiles(SourceURL, "*", SearchOption.AllDirectories)
除了 Source 参数外,还有一个 Pattern 参数(此处我使用“*”=所有文件)和 SearchOptions(SearchOptions.AllDirectories = 包含子文件夹)。
在此示例中,我并未将这些参数提供给用户,但这些选项很容易实现。
顾名思义,Pattern 参数,例如“*.exe”将返回所有以 .exe 结尾的文件等。
SearchOptions 也可以设置为 TopDirectoriesOnly,这将忽略子目录。
如果用户选择了单个文件,我仍然会填充 _SessionFiles 列表,但只包含该单个文件。
_SessionFiles = New String() {SourceURL}
接下来,我调用前面提到的 GetSessionLength 方法并存储其值以供以后使用。
_SessionLength = GetSessionLength()
在调用 ZipIt 之前,我做的最后一件事是设置 zip 文件中条目的根目录。
If SourceIsDirectory And IncludeRootDir = False Then
_RootDir = SourceURL & "\"
Else
_RootDir = String.Join("\", SourceURL.Split("\").ToArray, _
0, SourceURL.Split("\").ToArray.Length - 1) & "\"
End If
对于熟悉 Zip 文件的人来说,您可以选择包含或排除源对象或对象的根目录。因为我不想在 Zip 条目名称中包含整个文件/目录路径,所以我存储了相关的根路径,稍后在创建 Zip 文件中的条目时会将其删除。
例如,如果 File_To_Be_Zipped 是“C:\Users\JoeBloggs\Documents\TestFile.doc”,我只想让条目名称为“TestFile.doc”,所以将“C:\Users\JoeBloggs\Documents\”替换为“”
或者一个目录“C:\Users\JoeBloggs\Documents\My Projects\”
包含根目录:zip 文件可能包含多个条目,如下所示:
My Projects\File1.doc
My Projects\File2.doc
My Projects\File3.doc
My Projects\SubDir\File1.doc
或者不包含根目录:zip 文件可能包含多个条目,如下所示:
File1.doc
File2.doc
File3.doc
SubDir\File1.doc
记录完所有这些细节后,我们进入主要的压缩方法“ZipItUp”。
ZipItUp
我开始声明一些私有成员。
BlockSizeToRead 设置为 1 MiB。这是我们尝试读取的最大字节数。
Dim BlockSizeToRead As Int32 = 1048576 '1Mib Buffer
Buffer 是每次重复中存储读取字节的字节数组。
Dim Buffer As Byte() = New Byte(BlockSizeToRead - 1) {}
BytesRead 和 TotalBytes 用于构建进度报告。BytesRead 存储每次重复中实际读取的字节数。
Dim BytesRead As Int64, TotalBytesRead As Int64
LiveProg 和 PrevProg 用于比较当前进度和先前进度,以便仅在进度百分比实际发生变化时才更新进度。对于巨大的压缩会话,使用 int 值可能在某些较慢的机器上响应不够。
Dim LiveProg As Int16 = 0
Dim PrevProg As Int16 = 0
接下来,我确定用户是否希望阻止覆盖现有文件或删除现有文件。
If File.Exists(_Target) And OverwriteTarget = False Then
Throw New Exception("Target File Already Exists.")
Else
File.Delete(_Target)
End If
主例程
Using FS As FileStream = New FileStream(_Target, _
FileMode.CreateNew, FileAccess.Write)
首先,我们创建一个新的文件流。在此处注意 FileAccess 为 Write 很重要。许多在线示例因大文件大小而失败,原因很简单,就是设置了错误的 FileAccess,例如 ReadWrite 将在内存中处理文件流,然后提交到磁盘。这意味着您不仅占用了 RAM(这发生得很快),并且可能导致大文件出现 OutOfMemoryException,而且由于文件很快被压缩到内存中,因此进度报告也变得无用。当 FileStream 使用 End Using 终止时,该流将被写入磁盘,同样,对于大文件,这可能需要额外的 20 秒,这意味着进度条停留在 100% 而用户仍在等待(没有磁盘读/写计算)不确定的时间。
接下来,创建一个新的 Archive 并将其附加到我们的文件流。
Using Archive As ZipArchive = New ZipArchive(FS, _
ZipArchiveMode.Create)
并创建一个新的 ZipEntry 对象。
Dim Entry As ZipArchiveEntry = Nothing
然后我们开始遍历要压缩的文件。
For Each SessionFile As String In _SessionFiles
SessionFile 存储每次重复中要添加到存档的当前文件。我们将创建一个新的文件流,这次用于读取要添加到文件中的当前文件的字节。同样,确保 FileAccess 为 Read。
Using Reader As FileStream = File.Open(SessionFile, _
FileMode.Open, FileAccess.Read)
我们现在创建一个新的 ArchiveEntry 句柄。
Entry = Archive.CreateEntry(SessionFile.Replace(_RootDir, ""), _
_Compression)
您现在可以看到 _RootDir 值在起作用。条目名称将去除完全限定路径,并使用 Replace 方法替换为相对值。压缩参数由用户设置,包括 Optimal、Fastest 或 None。
然后我们为存档中的条目创建一个流。
Using Writer As Stream = Entry.Open()
并以高达 1 MiB 的块读取源文件的字节。
While (InlineAssignHelper(BytesRead, _
Reader.Read(Buffer, 0, Buffer.Length - 1))) > 0
您可以在此处看到 InlineAssignHelper 方法的使用。它的返回值用于 While 语句,以查看是否已到达文件末尾。BytesRead 也用相同的值填充,这将在稍后使用。
在读取了字节块后,我们立即将其写回条目流。
Writer.Write(Buffer, 0, BytesRead)
TotalBytesRead 被更新。
TotalBytesRead += BytesRead
检查和更新进度。
LiveProg = CInt((100 / _SessionLength) * TotalBytesRead)
LiveProg 存储进度的当前状态,由于这是一个整数,对于较大的会话(实际上,任何大于 100 字节的内容),此值可能会被更新多次而进度实际上并未更改。除非值实际发生更改,否则没有必要尝试重复更新 UI 对象。
接下来的部分检查进度是否已更改,如果已更改,则更新 UI,同时记录新的进度以供以后比较。
If LiveProg <> PrevProg Then
PrevProg = LiveProg
RaiseEvent Progress(LiveProg)
End If
已实现 try\Catch 子句以捕获有问题的文件,这通常会捕获受保护或被您系统上其他进程打开的文件。
Catch Ex As Exception
TotalBytesRead += New FileInfo(SessionFile).Length
Console.WriteLine(String.Format("Unable to add file to _
archive: {0} Error:{1}", SessionFile, Ex.Message))
End Try
如果确实发生了错误,请务必从此时手动更新 TotalBytesRead。由于文件被跳过(通常在 Read 语句处),例程的进度将不会相应更新,并且会导致用户看到不准确的进度位置。
例如,如果此会话有 10 个文件,每个文件长 100 MiB,其中一个文件被跳过,那么 TotalBytesRead 将不会在主循环中更新。进度将报告完成 70%,而实际上是 80% 完成,甚至在整个过程实际完成后报告 90% 完成。
取消
在整个代码中,您会看到各种对 _Cancel 成员的引用。即使 Zipper 的一个实例在线程上运行,用户也可以将 Cancel 设置为 True。如果发生这种情况,当前进程以及所有后续进程都将被跳过。
好了,差不多就这样了。我希望这能让您对使用 .Net 4.5 中的 Compression 命名空间压缩任意大小文件的我的方法有一个很好的了解。
复制、粘贴并运行。
Zipper 类的完整源代码。
'INCLUDE FOLLOWING REFERENCES Requires .Net 4.5
'System.IO.Compression
'System.IO.Compression.FileSystem
Imports System.IO
Imports System.IO.Compression
Public Class Zipper
Public Event Progress(Percent As Integer)
Public Event Complete()
Public Event StatusChanged(Status As String)
Private _Cancel As Boolean
Public Property Cancel As Boolean
Get
Return _Cancel
End Get
Set(value As Boolean)
If _Cancel = True Then Exit Property
_Cancel = value
End Set
End Property
Private _Compression As CompressionLevel
Public Property CompressionLevel As CompressionLevel
Get
Return _Compression
End Get
Set(value As CompressionLevel)
_Compression = value
End Set
End Property
Private _Target As String
Public Property TargetURL As String
Get
Return _Target
End Get
Set(value As String)
_Target = value
End Set
End Property
Private _Source As String
Public Property SourceURL As String
Get
Return _Source
End Get
Set(value As String)
_Source = value
End Set
End Property
Private _IsDir As Boolean
Public ReadOnly Property SourceIsDirectory
Get
Return _IsDir
End Get
End Property
Private _Overwrite As Boolean
Public Property OverwriteTarget As Boolean
Get
Return _Overwrite
End Get
Set(value As Boolean)
_Overwrite = value
End Set
End Property
Private _IncludeRootDir As Boolean
Public Property IncludeRootDir As Boolean
Get
Return _IncludeRootDir
End Get
Set(value As Boolean)
_IncludeRootDir = value
End Set
End Property
Private _SessionLength As Int64
Private _SessionFiles As String()
Private _RootDir As String
Public Sub New(Source As String, Target As String, CompressionLevel As CompressionLevel)
_Overwrite = False
_IncludeRootDir = True
_Target = Target
_Compression = CompressionLevel
_Cancel = False
If IsDir(Source) <> 1 Then
_IsDir = IsDir(Source)
_Source = Source
Else
Throw New Exception("Source file or directory doesn't exist or cannot be accessed.")
End If
End Sub
Private Function GetSessionLength() As Int64
Dim sLen As Int64 = 0
For Each SessionFile As String In _SessionFiles
sLen += New FileInfo(SessionFile).Length
If Cancel = True Then Exit For
Next
Return sLen
End Function
Private Function IsDir(Source As String) As Int16
If File.Exists(Source) Then
Return 0
ElseIf Directory.Exists(Source) Then
Return -1
Else
Return 1
End If
End Function
Public Sub Compress()
RaiseEvent StatusChanged("Gathering Required Information.")
If SourceIsDirectory Then
_SessionFiles = Directory.GetFiles(SourceURL, "*", SearchOption.AllDirectories)
Else
_SessionFiles = New String() {SourceURL}
End If
RaiseEvent StatusChanged("Examining Files.")
_SessionLength = GetSessionLength()
If SourceIsDirectory And IncludeRootDir = False Then
_RootDir = SourceURL & "\"
Else
_RootDir = String.Join("\", SourceURL.Split("\").ToArray, _
0, SourceURL.Split("\").ToArray.Length - 1) & "\"
End If
RaiseEvent StatusChanged("Compressing.")
Try
ZipItUp()
Catch ex As Exception
MsgBox(ex.Message)
Exit Sub
End Try
If Cancel = True Then
RaiseEvent StatusChanged("Cancelled.")
RaiseEvent Progress(100)
Else
RaiseEvent StatusChanged("Complete.")
End If
RaiseEvent Complete()
End Sub
Private Sub ZipItUp()
If Cancel = True Then Exit Sub
Dim BlockSizeToRead As Int32 = 1048576 '1Mib Buffer
Dim Buffer As Byte() = New Byte(BlockSizeToRead - 1) {}
Dim BytesRead As Int64, TotalBytesRead As Int64
Dim LiveProg As Int16 = 0
Dim PrevProg As Int16 = 0
If File.Exists(_Target) And OverwriteTarget = False Then
Throw New Exception("Target File Already Exists.")
Else
File.Delete(_Target)
End If
Using FS As FileStream = New FileStream(_Target, FileMode.CreateNew, FileAccess.Write)
Using Archive As ZipArchive = New ZipArchive(FS, ZipArchiveMode.Create)
Dim Entry As ZipArchiveEntry = Nothing
For Each SessionFile As String In _SessionFiles
Try
Using Reader As FileStream = File.Open(SessionFile, FileMode.Open, _
FileAccess.Read)
Entry = Archive.CreateEntry(SessionFile.Replace(_RootDir, ""), _
_Compression)
Using Writer As Stream = Entry.Open()
While (InlineAssignHelper(BytesRead, _
Reader.Read(Buffer, 0, Buffer.Length - 1))) > 0
Writer.Write(Buffer, 0, BytesRead)
TotalBytesRead += BytesRead
LiveProg = CInt((100 / _SessionLength) * TotalBytesRead)
If LiveProg <> PrevProg Then
PrevProg = LiveProg
RaiseEvent Progress(LiveProg)
End If
If Cancel = True Then Exit While
End While
End Using
End Using
Catch Ex As Exception
TotalBytesRead += New FileInfo(SessionFile).Length
Console.WriteLine(String.Format("Unable to add file to archive: _
{0} Error:{1}", SessionFile, Ex.Message))
End Try
If Cancel = True Then Exit For
Next
End Using
End Using
If Cancel = True Then
File.Delete(_Target)
End If
End Sub
Private Function InlineAssignHelper(Of T)(ByRef target As T, value As T) As T
target = value
Return value
End Function
End Class