1 #Region "Microsoft.VisualBasic::3367932a0f97dccebcec7503dadbaf7c, Microsoft.VisualBasic.Core\ApplicationServices\ZipLib.vb"
2
3     ' Author:
4     
5     '       asuka (amethyst.asuka@gcmodeller.org)
6     '       xie (genetics@smrucc.org)
7     '       xieguigang (xie.guigang@live.com)
8     
9     ' Copyright (c) 2018 GPL3 Licensed
10     
11     
12     ' GNU GENERAL PUBLIC LICENSE (GPL3)
13     
14     
15     ' This program is free software: you can redistribute it and/or modify
16     ' it under the terms of the GNU General Public License as published by
17     ' the Free Software Foundation, either version 3 of the License, or
18     ' (at your option) any later version.
19     
20     ' This program is distributed in the hope that it will be useful,
21     ' but WITHOUT ANY WARRANTY; without even the implied warranty of
22     ' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23     ' GNU General Public License for more details.
24     
25     ' You should have received a copy of the GNU General Public License
26     ' along with this program. If not, see <http://www.gnu.org/licenses/>.
27
28
29
30     ' /********************************************************************************/
31
32     ' Summaries:
33
34     '     Module GZip
35     
36     
37     '         Enum Overwrite
38     
39     '             Always, IfNewer, Never
40     
41     
42     
43     '         Enum ArchiveAction
44     
45     '             [Error], Ignore, Merge, Replace
46     
47     '  
48     
49     
50     
51     '  
52     
53     '     Function: ExtractToSelfDirectory, IsADirectoryEntry
54     
55     '     Sub: AddToArchive, DirectoryArchive, FileArchive, ImprovedExtractToDirectory, ImprovedExtractToFile
56     
57     
58     ' /********************************************************************************/
59
60 #End Region
61
62 Imports System.IO
63 Imports System.IO.Compression
64 Imports System.Runtime.CompilerServices
65 Imports Microsoft.VisualBasic.ApplicationServices
66 Imports Microsoft.VisualBasic.CommandLine.Reflection
67 Imports Microsoft.VisualBasic.Language
68 Imports Microsoft.VisualBasic.Language.UnixBash
69 Imports Microsoft.VisualBasic.Scripting.MetaData
70
71 Namespace ApplicationServices
72
73 #If NET_40 = 0 Then
74
75     ''' <summary>
76     ''' Creating Zip Files Easily in .NET 4.5
77     ''' Tim Corey, 11 May 2012
78     ''' 
79     ''' http://www.codeproject.com/Articles/381661/Creating-Zip-Files-Easily-in-NET
80     ''' </summary>
81     ''' <remarks></remarks>
82     ''' 
83     <Package("IO.ZIP", Description:="Creating Zip Files Easily in .NET 4.6",
84          Publisher:="Tim Corey",
85          Url:="http://www.codeproject.com/Articles/381661/Creating-Zip-Files-Easily-in-NET")>
86     Public Module GZip
87
88         ''' <summary>
89         ''' Used to specify what our overwrite policy
90         ''' is for files we are extracting.
91         ''' </summary>
92         Public Enum Overwrite
93             Always
94             IfNewer
95             Never
96         End Enum
97
98         ''' <summary>
99         ''' Used to identify what we will do if we are
100         ''' trying to create a zip file and it already
101         ''' exists.
102         ''' </summary>
103         Public Enum ArchiveAction
104             Merge
105             Replace
106             [Error]
107             Ignore
108         End Enum
109
110         ''' <summary>
111         ''' Unzips the specified file to the given folder in a safe
112         ''' manner.  This plans for missing paths and existing files
113         ''' and handles them gracefully.
114         ''' </summary>
115         ''' <param name="sourceArchiveFileName">
116         ''' The name of the zip file to be extracted
117         ''' </param>
118         ''' <param name="destinationDirectoryName">
119         ''' The directory to extract the zip file to
120         ''' </param>
121         ''' <param name="overwriteMethod">
122         ''' Specifies how we are going to handle an existing file.
123         ''' The default is IfNewer.
124         ''' </param>
125         ''' 
126         <ExportAPI("ExtractToDir"Info:="Unzips the specified file to the given folder in a safe manner. This plans for missing paths and existing files and handles them gracefully.")>
127         Public Sub ImprovedExtractToDirectory(<Parameter("Zip""The name of the zip file to be extracted")> sourceArchiveFileName$,
128                                               <Parameter("Dir""The directory to extract the zip file to")> destinationDirectoryName$,
129                                               <Parameter("Overwrite.HowTo""Specifies how we are going to handle an existing file. The default is IfNewer.")>
130                                               Optional overwriteMethod As Overwrite = Overwrite.IfNewer,
131                                               Optional extractToFlat As Boolean = False)
132
133             ' Opens the zip file up to be read
134             Using archive As ZipArchive = ZipFile.OpenRead(sourceArchiveFileName)
135
136                 Dim rootDir$ = Nothing
137                 Dim isFolderArchive = sourceArchiveFileName.IsSourceFolderZip(folder:=rootDir)
138                 Dim fullName$
139
140                 Loops through each file in the zip file
141                 For Each file As ZipArchiveEntry In archive.Entries
142                     If extractToFlat AndAlso isFolderArchive Then
143                         fullName = file.FullName.Replace(rootDir, "")
144                     Else
145                         fullName = file.FullName
146                     End If
147
148                     If file.IsADirectoryEntry Then
149                         Call Path _
150                             .Combine(destinationDirectoryName, fullName) _
151                             .MkDIR
152                     Else
153                         Call file.ExtractToFileInternal(
154                             destinationPath:=destinationDirectoryName,
155                             overwriteMethod:=overwriteMethod,
156                             overridesFullName:=fullName
157                         )
158                     End If
159                 Next
160             End Using
161         End Sub
162
163         ''' <summary>
164         ''' 判断目标zip文件是否是直接将文件夹进行压缩的
165         ''' 如果是直接将文件夹压缩的,那么肯定会在每一个entry的起始存在一个共同的文件夹名
166         ''' 例如:
167         ''' 
168         ''' ```
169         ''' 95-1.D/
170         ''' 95-1.D/AcqData/
171         ''' 95-1.D/AcqData/Contents.xml
172         ''' 95-1.D/AcqData/Devices.xml
173         ''' ```
174         ''' </summary>
175         ''' <param name="zip"></param>
176         ''' <returns></returns>
177         <Extension>
178         Public Function IsSourceFolderZip(zip$, Optional ByRef folder$ = NothingAs Boolean
179             Using archive As ZipArchive = ZipFile.OpenRead(zip)
180                 Dim fileNames = archive.Entries _
181                     .Where(Function(e) Not e.IsADirectoryEntry) _
182                     .Select(Function(file) file.FullName) _
183                     .ToArray
184                 Dim rootDir = archive.Entries _
185                     .Where(Function(e) e.IsADirectoryEntry) _
186                     .Select(Function(d) d.FullName) _
187                     .OrderBy(Function(d) d.Length) _
188                     .FirstOrDefault
189
190                 If rootDir.StringEmpty OrElse rootDir = "/" OrElse rootDir = "\" Then
191                     ' 没有root文件夹,说明不是
192                     Return False
193                 End If
194
195                 If fileNames.All(Function(path) path.StartsWith(rootDir)) Then
196                     folder = rootDir
197                 Else
198                     folder = Nothing
199                 End If
200
201                 Return Not folder Is Nothing
202             End Using
203         End Function
204
205         Public Function ExtractToSelfDirectory(zip$, Optional overwriteMethod As Overwrite = Overwrite.IfNewer) As String
206             Dim Dir As String = FileIO.FileSystem.GetParentPath(zip)
207             Dim Name As String = BaseName(zip)
208             Dir = Dir & "/" & Name
209             Call ImprovedExtractToDirectory(zip, Dir, overwriteMethod)
210
211             Return Dir
212         End Function
213
214         <MethodImpl(MethodImplOptions.AggressiveInlining)>
215         <Extension>
216         Public Function IsADirectoryEntry(file As ZipArchiveEntry) As Boolean
217             Return file.FullName.Last = "/"OrElse file.FullName.Last = "\"c
218         End Function
219
220         <Extension> Private Sub ExtractToFileInternal(file As ZipArchiveEntry, destinationPath$, overwriteMethod As Overwrite, overridesFullName$)
221             Gets the complete path for the destination file, including any
222             ' relative paths that were in the zip file
223             Dim destinationFileName As String = Path.Combine(destinationPath, overridesFullName Or file.FullName.AsDefault)
224
225             Gets just the new path, minus the file name so we can create the
226             ' directory if it does not exist
227             Dim destinationFilePath As String = Path.GetDirectoryName(destinationFileName)
228
229             ' Creates the directory (if it doesn't exist) for the new path
230             ' 2018-2-2 在原先的代码之中直接使用CreateDirectory,如果目标文件夹存在的话会报错
231             ' 在这里使用安全一点的mkdir函数
232             Call destinationFilePath.MkDIR(throwEx:=False)
233
234             ' Determines what to do with the file based upon the
235             ' method of overwriting chosen
236             Select Case overwriteMethod
237                 Case Overwrite.Always
238
239                     ' Just put the file in and overwrite anything that is found
240                     file.ExtractToFile(destinationFileName, True)
241
242                 Case Overwrite.IfNewer
243                     ' Checks to see if the file exists, and if so, if it should
244                     ' be overwritten
245                     If Not IO.File.Exists(destinationFileName) OrElse IO.File.GetLastWriteTime(destinationFileName) < file.LastWriteTime Then
246                         ' Either the file didn't exist or this file is newer, so
247                         ' we will extract it and overwrite any existing file
248                         file.ExtractToFile(destinationFileName, True)
249                     End If
250
251                 Case Overwrite.Never
252                     ' Put the file in if it is new but ignores the 
253                     ' file if it already exists
254                     If Not IO.File.Exists(destinationFileName) Then
255                         file.ExtractToFile(destinationFileName)
256                     End If
257
258                 Case Else
259             End Select
260         End Sub
261
262         ''' <summary>
263         ''' Safely extracts a single file from a zip file
264         ''' </summary>
265         ''' <param name="file">
266         ''' The zip entry we are pulling the file from
267         ''' </param>
268         ''' <param name="destinationPath">
269         ''' The root of where the file is going
270         ''' </param>
271         ''' <param name="overwriteMethod">
272         ''' Specifies how we are going to handle an existing file.
273         ''' The default is Overwrite.IfNewer.
274         ''' </param>
275         ''' 
276         <ExportAPI("Extract"Info:="Safely extracts a single file from a zip file.")>
277         <MethodImpl(MethodImplOptions.AggressiveInlining)>
278         <Extension> Public Sub ImprovedExtractToFile(<Parameter("Zip.Entry""The zip entry we are pulling the file from")>
279                                                      file As ZipArchiveEntry,
280                                                      destinationPath$,
281                                                      Optional overwriteMethod As Overwrite = Overwrite.IfNewer)
282             Call file.ExtractToFileInternal(destinationPath, overwriteMethod, Nothing)
283         End Sub
284
285         <ExportAPI("File.Zip")>
286         Public Sub FileArchive(file$, SaveZip$,
287                                Optional action As ArchiveAction = ArchiveAction.Replace,
288                                Optional fileOverwrite As Overwrite = Overwrite.IfNewer,
289                                Optional compression As CompressionLevel = CompressionLevel.Optimal)
290
291             Call SaveZip.ParentPath.MkDIR
292             Call {file}.AddToArchive(SaveZip, action, fileOverwrite, compression)
293         End Sub
294
295         <ExportAPI("DIR.Zip")>
296         Public Sub DirectoryArchive(DIR$, saveZip$,
297                                     Optional action As ArchiveAction = ArchiveAction.Replace,
298                                     Optional fileOverwrite As Overwrite = Overwrite.IfNewer,
299                                     Optional compression As CompressionLevel = CompressionLevel.Optimal,
300                                     Optional flatDirectory As Boolean = False)
301
302             Dim rel$ = DIR Or "".AsDefault(Function() flatDirectory)
303
304             Call saveZip.ParentPath.MkDIR
305             Call rel.SetValue(AddressOf GetDirectoryFullPath)
306             Call (ls - l - r - "*.*" <= DIR) _
307                 .AddToArchive(
308                     archiveFullName:=saveZip,
309                     action:=action,
310                     fileOverwrite:=fileOverwrite,
311                     compression:=compression,
312                     relativeDIR:=rel.Replace("/"c, "\"c).Trim("\"c)
313                  )
314         End Sub
315
316         ''' <summary>
317         ''' Allows you to add files to an archive, whether the archive
318         ''' already exists or not
319         ''' </summary>
320         ''' <param name="archiveFullName">
321         ''' The name of the archive to you want to add your files to
322         ''' </param>
323         ''' <param name="files">
324         ''' A set of file names that are to be added
325         ''' </param>
326         ''' <param name="action">
327         ''' Specifies how we are going to handle an existing archive
328         ''' </param>
329         ''' <param name="compression">
330         ''' Specifies what type of compression to use - defaults to Optimal
331         ''' </param>
332         ''' 
333         <ExportAPI("Zip.Add.Files"Info:="Allows you to add files to an archive, whether the archive already exists or not")>
334         <Extension>
335         Public Sub AddToArchive(<Parameter("files""A set of file names that are to be added")> files As IEnumerable(Of String),
336                                 <Parameter("Zip""The name of the archive to you want to add your files to")> archiveFullName$,
337                                 Optional action As ArchiveAction = ArchiveAction.Replace,
338                                 Optional fileOverwrite As Overwrite = Overwrite.IfNewer,
339                                 Optional compression As CompressionLevel = CompressionLevel.Optimal,
340                                 Optional relativeDIR$ = Nothing)
341
342             'Identifies the mode we will be using - the default is Create
343             Dim mode As ZipArchiveMode = ZipArchiveMode.Create
344
345             'Determines if the zip file even exists
346             Dim archiveExists As Boolean = IO.File.Exists(archiveFullName)
347
348             'Figures out what to do based upon our specified overwrite method
349             Select Case action
350                 Case ArchiveAction.Merge
351                     'Sets the mode to update if the file exists, otherwise
352                     'the default of Create is fine
353                     If archiveExists Then
354                         mode = ZipArchiveMode.Update
355                     End If
356
357                 Case ArchiveAction.Replace
358                     'Deletes the file if it exists.  Either way, the default
359                     'mode of Create is fine
360                     If archiveExists Then
361                         IO.File.Delete(archiveFullName)
362                     End If
363
364                 Case ArchiveAction.[Error]
365                     'Throws an error if the file exists
366                     If archiveExists Then
367                         Throw New IOException($"The zip file {archiveFullName.ToFileURL.CLIPath} already exists.")
368                     End If
369
370                 Case ArchiveAction.Ignore
371                     'Closes the method silently and does nothing
372                     If archiveExists Then
373                         Return
374                     End If
375
376                 Case Else
377
378             End Select
379
380             'Opens the zip file in the mode we specified
381             Using zipFile As ZipArchive = IO.Compression.ZipFile.Open(archiveFullName, mode)
382
383                 'This is a bit of a hack and should be refactored - I am
384                 'doing a similar foreach loop for both modes, but for Create
385                 'I am doing very little work while Update gets a lot of
386                 'code.  This also does not handle any other mode (of
387                 'which there currently wouldn't be one since we don't
388                 'use Read here).
389
390                 If mode = ZipArchiveMode.Create Then
391                     Dim entryName$
392
393                     For Each path As String In files
394                         ' Adds the file to the archive
395                         If relativeDIR.StringEmpty Then
396                             entryName = IO.Path.GetFileName(path)
397                         Else
398                             entryName = RelativePath(relativeDIR, path, appendParent:=False)
399                         End If
400
401                         Call zipFile.CreateEntryFromFile(path, entryName, compression)
402                     Next
403                 Else
404                     For Each path As String In files
405                         Dim fileInZip = (From f In zipFile.Entries Where f.Name = IO.Path.GetFileName(path)).FirstOrDefault()
406
407                         Select Case fileOverwrite
408                             Case Overwrite.Always
409
410                                 'Deletes the file if it is found
411                                 If fileInZip IsNot Nothing Then
412                                     fileInZip.Delete()
413                                 End If
414
415                                 'Adds the file to the archive
416                                 zipFile.CreateEntryFromFile(path, IO.Path.GetFileName(path), compression)
417
418                             Case Overwrite.IfNewer
419
420                                 'This is a bit trickier - we only delete the file if it is
421                                 'newer, but if it is newer or if the file isn't already in
422                                 'the zip file, we will write it to the zip file
423
424                                 If fileInZip IsNot Nothing Then
425
426                                     'Deletes the file only if it is older than our file.
427                                     'Note that the file will be ignored if the existing file
428                                     'in the archive is newer.
429                                     If fileInZip.LastWriteTime < IO.File.GetLastWriteTime(path) Then
430                                         fileInZip.Delete()
431
432                                         'Adds the file to the archive
433                                         zipFile.CreateEntryFromFile(path, IO.Path.GetFileName(path), compression)
434                                     End If
435                                 Else
436                                     'The file wasn't already in the zip file so add it to the archive
437                                     zipFile.CreateEntryFromFile(path, IO.Path.GetFileName(path), compression)
438                                 End If
439
440                             Case Overwrite.Never
441
442                                 'Don't do anything - this is a decision that you need to
443                                 'consider, however, since this will mean that no file will
444                                 'be writte.  You could write a second copy to the zip with
445                                 'the same name (not sure that is wise, however).
446
447                             Case Else
448
449                         End Select
450                     Next
451                 End If
452             End Using
453         End Sub
454
455         '<Example.Code>     
456
457         'Imports System.Collections.Generic
458         'Imports System.IO
459         'Imports System.IO.Compression
460         'Imports Helpers
461
462         ' 'Example 1
463         ' Private Shared Sub SimpleZip(dirToZip As String, zipName As String)
464         ' ZipFile.CreateFromDirectory(dirToZip, zipName)
465         ' End Sub
466
467         ' 'Example 2
468         ' Private Shared Sub SimpleZip(dirToZip As String, zipName As String, compression As CompressionLevel, includeRoot As Boolean)
469         ' ZipFile.CreateFromDirectory(dirToZip, zipName, compression, includeRoot)
470         ' End Sub
471
472         ' 'Example 3
473         ' Private Shared Sub SimpleUnzip(zipName As String, dirToUnzipTo As String)
474         ' ZipFile.ExtractToDirectory(zipName, dirToUnzipTo)
475         ' End Sub
476
477         ' 'Example 4
478         ' Private Shared Sub SmarterUnzip(zipName As String, dirToUnzipTo As String)
479         ' 'This stores the path where the file should be unzipped to,
480         ' 'including any subfolders that the file was originally in.
481         ' Dim fileUnzipFullPath As String
482
483         ' 'This is the full name of the destination file including
484         ' 'the path
485         ' Dim fileUnzipFullName As String
486
487         ' 'Opens the zip file up to be read
488         ' Using archive As ZipArchive = ZipFile.OpenRead(zipName)
489         ' 'Loops through each file in the zip file
490         ' For Each file As ZipArchiveEntry In archive.Entries
491         ' 'Outputs relevant file information to the console
492         ' Console.WriteLine("File Name: {0}", file.Name)
493         ' Console.WriteLine("File Size: {0} bytes", file.Length)
494         ' Console.WriteLine("Compression Ratio: {0}", (CDbl(file.CompressedLength) / file.Length).ToString("0.0%"))
495
496         ' 'Identifies the destination file name and path
497         ' fileUnzipFullName = Path.Combine(dirToUnzipTo, file.FullName)
498
499         ' 'Extracts the files to the output folder in a safer manner
500         ' If Not System.IO.File.Exists(fileUnzipFullName) Then
501         ' 'Calculates what the new full path for the unzipped file should be
502         ' fileUnzipFullPath = Path.GetDirectoryName(fileUnzipFullName)
503
504         ' 'Creates the directory (if it doesn't exist) for the new path
505         ' Directory.CreateDirectory(fileUnzipFullPath)
506
507         ' 'Extracts the file to (potentially new) path
508         ' file.ExtractToFile(fileUnzipFullName)
509         ' End If
510         ' Next
511         ' End Using
512         ' End Sub
513
514         ' 'Example 5
515         ' Private Shared Sub ManuallyCreateZipFile(zipName As String)
516         ' 'Creates a new, blank zip file to work with - the file will be
517         ' 'finalized when the using statement completes
518         ' Using newFile As ZipArchive = ZipFile.Open(zipName, ZipArchiveMode.Create)
519         ' 'Here are two hard-coded files that we will be adding to the zip
520         ' 'file.  If you don't have these files in your system, this will
521         ' 'fail.  Either create them or change the file names.
522         ' newFile.CreateEntryFromFile("C:\Temp\File1.txt""File1.txt")
523         ' newFile.CreateEntryFromFile("C:\Temp\File2.txt""File2.txt", CompressionLevel.Fastest)
524         ' End Using
525         ' End Sub
526
527         ' 'Example 6
528         ' Private Shared Sub ManuallyUpdateZipFile(zipName As String)
529         ' 'Opens the existing file like we opened the new file (just changed
530         ' 'the ZipArchiveMode to Update
531         ' Using modFile As ZipArchive = ZipFile.Open(zipName, ZipArchiveMode.Update)
532         ' 'Here are two hard-coded files that we will be adding to the zip
533         ' 'file.  If you don't have these files in your system, this will
534         ' 'fail.  Either create them or change the file names.  Also, note
535         ' 'that their names are changed when they are put into the zip file.
536         ' modFile.CreateEntryFromFile("C:\Temp\File1.txt""File10.txt")
537
538         ' 'We could also add the code from Example 4 here to read
539         ' 'the contents of the open zip file as well.
540         ' modFile.CreateEntryFromFile("C:\Temp\File2.txt""File20.txt", CompressionLevel.Fastest)
541         ' End Using
542         ' End Sub
543
544         ' 'Example 7
545         ' Private Shared Sub CallingImprovedExtractToDirectory(zipName As String, dirToUnzipTo As String)
546         ' 'This performs a similar function to Example 3, only now we are doing it
547         ' 'safely (we won't crash because of predictable and preventable errors). 
548         ' 'The result is something you don't have to think about - it just works.
549         ' Compression.ImprovedExtractToDirectory(zipName, dirToUnzipTo, Compression.Overwrite.IfNewer)
550         ' End Sub
551
552         ' 'Example 8
553         ' Private Shared Sub CallingImprovedExtractToFile(zipName As String, dirToUnzipTo As String)
554         ' 'Opens the zip file up to be read
555         ' Using archive As ZipArchive = ZipFile.OpenRead(zipName)
556         ' 'Loops through each file in the zip file
557         ' For Each file As ZipArchiveEntry In archive.Entries
558         ' 'Outputs relevant file information to the console
559         ' Console.WriteLine("File Name: {0}", file.Name)
560         ' Console.WriteLine("File Size: {0} bytes", file.Length)
561         ' Console.WriteLine("Compression Ratio: {0}", (CDbl(file.CompressedLength) / file.Length).ToString("0.0%"))
562
563         ' 'This is the new call
564         ' Compression.ImprovedExtractToFile(file, dirToUnzipTo, Compression.Overwrite.Always)
565         ' Next
566         ' End Using
567         ' End Sub
568
569         ' 'Example 9
570         ' Private Shared Sub CallingAddToArchive(zipName As String)
571         ' 'This creates our list of files to be added
572         ' Dim filesToArchive As New List(Of String)()
573
574         ' 'Here we are adding two hard-coded files to our list
575         ' filesToArchive.Add("C:\Temp\File1.txt")
576         ' filesToArchive.Add("C:\Temp\File2.txt")
577
578         ' Compression.AddToArchive(zipName, filesToArchive, Compression.ArchiveAction.Replace, Compression.Overwrite.IfNewer, CompressionLevel.Optimal)
579         ' End Sub
580     End Module
581 #End If
582 End Namespace