1 #Region "Microsoft.VisualBasic::1d531d05aafd7ea58451795acc1a03ad, 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 ZipLib
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, IsSourceFolderZip
54     
55     '     Sub: AddToArchive, DirectoryArchive, ExtractToFileInternal, FileArchive, ImprovedExtractToDirectory
56     '          ImprovedExtractToFile
57     
58     
59     ' /********************************************************************************/
60
61 #End Region
62
63 Imports System.IO
64 Imports System.IO.Compression
65 Imports System.Runtime.CompilerServices
66 Imports Microsoft.VisualBasic.ApplicationServices
67 Imports Microsoft.VisualBasic.CommandLine.Reflection
68 Imports Microsoft.VisualBasic.Language
69 Imports Microsoft.VisualBasic.Language.UnixBash
70 Imports Microsoft.VisualBasic.Scripting.MetaData
71
72 Namespace ApplicationServices
73
74 #If NET_40 = 0 Then
75
76     ''' <summary>
77     ''' Creating Zip Files Easily in .NET 4.5
78     ''' Tim Corey, 11 May 2012
79     ''' 
80     ''' http://www.codeproject.com/Articles/381661/Creating-Zip-Files-Easily-in-NET
81     ''' </summary>
82     ''' <remarks></remarks>
83     ''' 
84     <Package("IO.ZIP", Description:="Creating Zip Files Easily in .NET 4.6",
85          Publisher:="Tim Corey",
86          Url:="http://www.codeproject.com/Articles/381661/Creating-Zip-Files-Easily-in-NET")>
87     Public Module ZipLib
88
89         ''' <summary>
90         ''' Used to specify what our overwrite policy
91         ''' is for files we are extracting.
92         ''' </summary>
93         Public Enum Overwrite
94             Always
95             IfNewer
96             Never
97         End Enum
98
99         ''' <summary>
100         ''' Used to identify what we will do if we are
101         ''' trying to create a zip file and it already
102         ''' exists.
103         ''' </summary>
104         Public Enum ArchiveAction
105             Merge
106             Replace
107             [Error]
108             Ignore
109         End Enum
110
111         ''' <summary>
112         ''' Unzips the specified file to the given folder in a safe
113         ''' manner.  This plans for missing paths and existing files
114         ''' and handles them gracefully.
115         ''' </summary>
116         ''' <param name="sourceArchiveFileName">
117         ''' The name of the zip file to be extracted
118         ''' </param>
119         ''' <param name="destinationDirectoryName">
120         ''' The directory to extract the zip file to
121         ''' </param>
122         ''' <param name="overwriteMethod">
123         ''' Specifies how we are going to handle an existing file.
124         ''' The default is IfNewer.
125         ''' </param>
126         ''' 
127         <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.")>
128         Public Sub ImprovedExtractToDirectory(<Parameter("Zip""The name of the zip file to be extracted")> sourceArchiveFileName$,
129                                               <Parameter("Dir""The directory to extract the zip file to")> destinationDirectoryName$,
130                                               <Parameter("Overwrite.HowTo""Specifies how we are going to handle an existing file. The default is IfNewer.")>
131                                               Optional overwriteMethod As Overwrite = Overwrite.IfNewer,
132                                               Optional extractToFlat As Boolean = False)
133
134             ' Opens the zip file up to be read
135             Using archive As ZipArchive = ZipFile.OpenRead(sourceArchiveFileName)
136
137                 Dim rootDir$ = Nothing
138                 Dim isFolderArchive = sourceArchiveFileName.IsSourceFolderZip(folder:=rootDir)
139                 Dim fullName$
140
141                 Loops through each file in the zip file
142                 For Each file As ZipArchiveEntry In archive.Entries
143                     If extractToFlat AndAlso isFolderArchive Then
144                         fullName = file.FullName.Replace(rootDir, "")
145                     Else
146                         fullName = file.FullName
147                     End If
148
149                     If file.IsADirectoryEntry Then
150                         Call Path _
151                             .Combine(destinationDirectoryName, fullName) _
152                             .MkDIR
153                     Else
154                         Call file.ExtractToFileInternal(
155                             destinationPath:=destinationDirectoryName,
156                             overwriteMethod:=overwriteMethod,
157                             overridesFullName:=fullName
158                         )
159                     End If
160                 Next
161             End Using
162         End Sub
163
164         ''' <summary>
165         ''' 判断目标zip文件是否是直接将文件夹进行压缩的
166         ''' 如果是直接将文件夹压缩的,那么肯定会在每一个entry的起始存在一个共同的文件夹名
167         ''' 例如:
168         ''' 
169         ''' ```
170         ''' 95-1.D/
171         ''' 95-1.D/AcqData/
172         ''' 95-1.D/AcqData/Contents.xml
173         ''' 95-1.D/AcqData/Devices.xml
174         ''' ```
175         ''' </summary>
176         ''' <param name="zip"></param>
177         ''' <returns></returns>
178         <Extension>
179         Public Function IsSourceFolderZip(zip$, Optional ByRef folder$ = NothingAs Boolean
180             Using archive As ZipArchive = ZipFile.OpenRead(zip)
181                 Dim fileNames = archive.Entries _
182                     .Where(Function(e) Not e.IsADirectoryEntry) _
183                     .Select(Function(file) file.FullName) _
184                     .ToArray
185                 Dim rootDir = archive.Entries _
186                     .Where(Function(e) e.IsADirectoryEntry) _
187                     .Select(Function(d) d.FullName) _
188                     .OrderBy(Function(d) d.Length) _
189                     .FirstOrDefault
190
191                 If rootDir.StringEmpty OrElse rootDir = "/" OrElse rootDir = "\" Then
192                     ' 没有root文件夹,说明不是
193                     Return False
194                 End If
195
196                 If fileNames.All(Function(path) path.StartsWith(rootDir)) Then
197                     folder = rootDir
198                 Else
199                     folder = Nothing
200                 End If
201
202                 Return Not folder Is Nothing
203             End Using
204         End Function
205
206         Public Function ExtractToSelfDirectory(zip$, Optional overwriteMethod As Overwrite = Overwrite.IfNewer) As String
207             Dim Dir As String = FileIO.FileSystem.GetParentPath(zip)
208             Dim Name As String = BaseName(zip)
209             Dir = Dir & "/" & Name
210             Call ImprovedExtractToDirectory(zip, Dir, overwriteMethod)
211
212             Return Dir
213         End Function
214
215         <MethodImpl(MethodImplOptions.AggressiveInlining)>
216         <Extension>
217         Public Function IsADirectoryEntry(file As ZipArchiveEntry) As Boolean
218             Return file.FullName.Last = "/"OrElse file.FullName.Last = "\"c
219         End Function
220
221         <Extension> Private Sub ExtractToFileInternal(file As ZipArchiveEntry, destinationPath$, overwriteMethod As Overwrite, overridesFullName$)
222             Gets the complete path for the destination file, including any
223             ' relative paths that were in the zip file
224             Dim destinationFileName As String = Path.Combine(destinationPath, overridesFullName Or file.FullName.AsDefault)
225
226             Gets just the new path, minus the file name so we can create the
227             ' directory if it does not exist
228             Dim destinationFilePath As String = Path.GetDirectoryName(destinationFileName)
229
230             ' Creates the directory (if it doesn't exist) for the new path
231             ' 2018-2-2 在原先的代码之中直接使用CreateDirectory,如果目标文件夹存在的话会报错
232             ' 在这里使用安全一点的mkdir函数
233             Call destinationFilePath.MkDIR(throwEx:=False)
234
235             ' Determines what to do with the file based upon the
236             ' method of overwriting chosen
237             Select Case overwriteMethod
238                 Case Overwrite.Always
239
240                     ' Just put the file in and overwrite anything that is found
241                     file.ExtractToFile(destinationFileName, True)
242
243                 Case Overwrite.IfNewer
244                     ' Checks to see if the file exists, and if so, if it should
245                     ' be overwritten
246                     If Not IO.File.Exists(destinationFileName) OrElse IO.File.GetLastWriteTime(destinationFileName) < file.LastWriteTime Then
247                         ' Either the file didn't exist or this file is newer, so
248                         ' we will extract it and overwrite any existing file
249                         file.ExtractToFile(destinationFileName, True)
250                     End If
251
252                 Case Overwrite.Never
253                     ' Put the file in if it is new but ignores the 
254                     ' file if it already exists
255                     If Not IO.File.Exists(destinationFileName) Then
256                         file.ExtractToFile(destinationFileName)
257                     End If
258
259                 Case Else
260             End Select
261         End Sub
262
263         ''' <summary>
264         ''' Safely extracts a single file from a zip file
265         ''' </summary>
266         ''' <param name="file">
267         ''' The zip entry we are pulling the file from
268         ''' </param>
269         ''' <param name="destinationPath">
270         ''' The root of where the file is going
271         ''' </param>
272         ''' <param name="overwriteMethod">
273         ''' Specifies how we are going to handle an existing file.
274         ''' The default is Overwrite.IfNewer.
275         ''' </param>
276         ''' 
277         <ExportAPI("Extract"Info:="Safely extracts a single file from a zip file.")>
278         <MethodImpl(MethodImplOptions.AggressiveInlining)>
279         <Extension> Public Sub ImprovedExtractToFile(<Parameter("Zip.Entry""The zip entry we are pulling the file from")>
280                                                      file As ZipArchiveEntry,
281                                                      destinationPath$,
282                                                      Optional overwriteMethod As Overwrite = Overwrite.IfNewer)
283             Call file.ExtractToFileInternal(destinationPath, overwriteMethod, Nothing)
284         End Sub
285
286         <ExportAPI("File.Zip")>
287         Public Sub FileArchive(file$, SaveZip$,
288                                Optional action As ArchiveAction = ArchiveAction.Replace,
289                                Optional fileOverwrite As Overwrite = Overwrite.IfNewer,
290                                Optional compression As CompressionLevel = CompressionLevel.Optimal)
291
292             Call SaveZip.ParentPath.MkDIR
293             Call {file}.AddToArchive(SaveZip, action, fileOverwrite, compression)
294         End Sub
295
296         ''' <summary>
297         ''' 
298         ''' </summary>
299         ''' <param name="DIR$"></param>
300         ''' <param name="saveZip$"></param>
301         ''' <param name="action"></param>
302         ''' <param name="fileOverwrite"></param>
303         ''' <param name="compression"></param>
304         ''' <param name="flatDirectory">
305         ''' 当这个参数为FALSE的时候,zip文件之中会保留有原来的文件夹的树形结构,
306         ''' 反之,则zip文件之中不会存在任何文件夹结构,所有的文件都会被保存在zip文件里面的根目录之中
307         ''' 
308         ''' 这个参数默认为False,即保留有原来的文件夹树形结构
309         ''' </param>
310         <ExportAPI("DIR.Zip")>
311         Public Sub DirectoryArchive(DIR$, saveZip$,
312                                     Optional action As ArchiveAction = ArchiveAction.Replace,
313                                     Optional fileOverwrite As Overwrite = Overwrite.IfNewer,
314                                     Optional compression As CompressionLevel = CompressionLevel.Optimal,
315                                     Optional flatDirectory As Boolean = False)
316
317             ' 2018-7-28 如果rel是空字符串
318             ' 那么再压缩函数之中只会将文件名作为entry,即实现无文件树的效果
319             ' 反之会使用相对路径生成文件树,即树状的非flat结构
320             Dim rel$ = DIR Or "".When(flatDirectory)
321
322             If Not rel.StringEmpty Then
323                 rel = rel.GetDirectoryFullPath
324             End If
325
326             Call saveZip.ParentPath.MkDIR
327             Call (ls - l - r - "*.*" <= DIR) _
328                 .AddToArchive(
329                     archiveFullName:=saveZip,
330                     action:=action,
331                     fileOverwrite:=fileOverwrite,
332                     compression:=compression,
333                     relativeDIR:=rel.Replace("/"c, "\"c).Trim("\"c)
334                  )
335         End Sub
336
337         ''' <summary>
338         ''' Allows you to add files to an archive, whether the archive
339         ''' already exists or not
340         ''' </summary>
341         ''' <param name="archiveFullName">
342         ''' The name of the archive to you want to add your files to
343         ''' </param>
344         ''' <param name="files">
345         ''' A set of file names that are to be added
346         ''' </param>
347         ''' <param name="action">
348         ''' Specifies how we are going to handle an existing archive
349         ''' </param>
350         ''' <param name="compression">
351         ''' Specifies what type of compression to use - defaults to Optimal
352         ''' </param>
353         ''' 
354         <ExportAPI("Zip.Add.Files"Info:="Allows you to add files to an archive, whether the archive already exists or not")>
355         <Extension>
356         Public Sub AddToArchive(<Parameter("files""A set of file names that are to be added")> files As IEnumerable(Of String),
357                                 <Parameter("Zip""The name of the archive to you want to add your files to")> archiveFullName$,
358                                 Optional action As ArchiveAction = ArchiveAction.Replace,
359                                 Optional fileOverwrite As Overwrite = Overwrite.IfNewer,
360                                 Optional compression As CompressionLevel = CompressionLevel.Optimal,
361                                 Optional relativeDIR$ = Nothing)
362
363             'Identifies the mode we will be using - the default is Create
364             Dim mode As ZipArchiveMode = ZipArchiveMode.Create
365
366             'Determines if the zip file even exists
367             Dim archiveExists As Boolean = IO.File.Exists(archiveFullName)
368
369             'Figures out what to do based upon our specified overwrite method
370             Select Case action
371                 Case ArchiveAction.Merge
372                     'Sets the mode to update if the file exists, otherwise
373                     'the default of Create is fine
374                     If archiveExists Then
375                         mode = ZipArchiveMode.Update
376                     End If
377
378                 Case ArchiveAction.Replace
379                     'Deletes the file if it exists.  Either way, the default
380                     'mode of Create is fine
381                     If archiveExists Then
382                         IO.File.Delete(archiveFullName)
383                     End If
384
385                 Case ArchiveAction.[Error]
386                     'Throws an error if the file exists
387                     If archiveExists Then
388                         Throw New IOException($"The zip file {archiveFullName.ToFileURL.CLIPath} already exists.")
389                     End If
390
391                 Case ArchiveAction.Ignore
392                     'Closes the method silently and does nothing
393                     If archiveExists Then
394                         Return
395                     End If
396
397                 Case Else
398
399             End Select
400
401             'Opens the zip file in the mode we specified
402             Using zipFile As ZipArchive = IO.Compression.ZipFile.Open(archiveFullName, mode)
403
404                 'This is a bit of a hack and should be refactored - I am
405                 'doing a similar foreach loop for both modes, but for Create
406                 'I am doing very little work while Update gets a lot of
407                 'code.  This also does not handle any other mode (of
408                 'which there currently wouldn't be one since we don't
409                 'use Read here).
410
411                 If mode = ZipArchiveMode.Create Then
412                     Dim entryName$
413
414                     For Each path As String In files
415                         ' Adds the file to the archive
416                         If relativeDIR.StringEmpty Then
417                             entryName = IO.Path.GetFileName(path)
418                         Else
419                             entryName = RelativePath(relativeDIR, path, appendParent:=False, fixZipPath:=True)
420                         End If
421
422                         Call zipFile.CreateEntryFromFile(path, entryName, compression)
423                     Next
424                 Else
425                     For Each path As String In files
426                         Dim fileInZip = (From f In zipFile.Entries Where f.Name = IO.Path.GetFileName(path)).FirstOrDefault()
427
428                         Select Case fileOverwrite
429                             Case Overwrite.Always
430
431                                 'Deletes the file if it is found
432                                 If fileInZip IsNot Nothing Then
433                                     fileInZip.Delete()
434                                 End If
435
436                                 'Adds the file to the archive
437                                 zipFile.CreateEntryFromFile(path, IO.Path.GetFileName(path), compression)
438
439                             Case Overwrite.IfNewer
440
441                                 'This is a bit trickier - we only delete the file if it is
442                                 'newer, but if it is newer or if the file isn't already in
443                                 'the zip file, we will write it to the zip file
444
445                                 If fileInZip IsNot Nothing Then
446
447                                     'Deletes the file only if it is older than our file.
448                                     'Note that the file will be ignored if the existing file
449                                     'in the archive is newer.
450                                     If fileInZip.LastWriteTime < IO.File.GetLastWriteTime(path) Then
451                                         fileInZip.Delete()
452
453                                         'Adds the file to the archive
454                                         zipFile.CreateEntryFromFile(path, IO.Path.GetFileName(path), compression)
455                                     End If
456                                 Else
457                                     'The file wasn't already in the zip file so add it to the archive
458                                     zipFile.CreateEntryFromFile(path, IO.Path.GetFileName(path), compression)
459                                 End If
460
461                             Case Overwrite.Never
462
463                                 'Don't do anything - this is a decision that you need to
464                                 'consider, however, since this will mean that no file will
465                                 'be writte.  You could write a second copy to the zip with
466                                 'the same name (not sure that is wise, however).
467
468                             Case Else
469
470                         End Select
471                     Next
472                 End If
473             End Using
474         End Sub
475     End Module
476 #End If
477 End Namespace