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$ = Nothing) As 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 = "/"c 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 |