1 | // ZipFile.Save.cs
|
---|
2 | // ------------------------------------------------------------------
|
---|
3 | //
|
---|
4 | // Copyright (c) 2009 Dino Chiesa.
|
---|
5 | // All rights reserved.
|
---|
6 | //
|
---|
7 | // This code module is part of DotNetZip, a zipfile class library.
|
---|
8 | //
|
---|
9 | // ------------------------------------------------------------------
|
---|
10 | //
|
---|
11 | // This code is licensed under the Microsoft Public License.
|
---|
12 | // See the file License.txt for the license details.
|
---|
13 | // More info on: http://dotnetzip.codeplex.com
|
---|
14 | //
|
---|
15 | // ------------------------------------------------------------------
|
---|
16 | //
|
---|
17 | // last saved (in emacs):
|
---|
18 | // Time-stamp: <2011-August-05 13:31:23>
|
---|
19 | //
|
---|
20 | // ------------------------------------------------------------------
|
---|
21 | //
|
---|
22 | // This module defines the methods for Save operations on zip files.
|
---|
23 | //
|
---|
24 | // ------------------------------------------------------------------
|
---|
25 | //
|
---|
26 |
|
---|
27 |
|
---|
28 | using System;
|
---|
29 | using System.IO;
|
---|
30 | using System.Collections.Generic;
|
---|
31 |
|
---|
32 | namespace OfficeOpenXml.Packaging.Ionic.Zip
|
---|
33 | {
|
---|
34 |
|
---|
35 | internal partial class ZipFile
|
---|
36 | {
|
---|
37 |
|
---|
38 | /// <summary>
|
---|
39 | /// Delete file with retry on UnauthorizedAccessException.
|
---|
40 | /// </summary>
|
---|
41 | ///
|
---|
42 | /// <remarks>
|
---|
43 | /// <para>
|
---|
44 | /// When calling File.Delete() on a file that has been "recently"
|
---|
45 | /// created, the call sometimes fails with
|
---|
46 | /// UnauthorizedAccessException. This method simply retries the Delete 3
|
---|
47 | /// times with a sleep between tries.
|
---|
48 | /// </para>
|
---|
49 | /// </remarks>
|
---|
50 | ///
|
---|
51 | /// <param name='filename'>the name of the file to be deleted</param>
|
---|
52 | private void DeleteFileWithRetry(string filename)
|
---|
53 | {
|
---|
54 | bool done = false;
|
---|
55 | int nRetries = 3;
|
---|
56 | for (int i=0; i < nRetries && !done; i++)
|
---|
57 | {
|
---|
58 | try
|
---|
59 | {
|
---|
60 | File.Delete(filename);
|
---|
61 | done = true;
|
---|
62 | }
|
---|
63 | catch (System.UnauthorizedAccessException)
|
---|
64 | {
|
---|
65 | Console.WriteLine("************************************************** Retry delete.");
|
---|
66 | System.Threading.Thread.Sleep(200+i*200);
|
---|
67 | }
|
---|
68 | }
|
---|
69 | }
|
---|
70 |
|
---|
71 |
|
---|
72 | /// <summary>
|
---|
73 | /// Saves the Zip archive to a file, specified by the Name property of the
|
---|
74 | /// <c>ZipFile</c>.
|
---|
75 | /// </summary>
|
---|
76 | ///
|
---|
77 | /// <remarks>
|
---|
78 | /// <para>
|
---|
79 | /// The <c>ZipFile</c> instance is written to storage, typically a zip file
|
---|
80 | /// in a filesystem, only when the caller calls <c>Save</c>. In the typical
|
---|
81 | /// case, the Save operation writes the zip content to a temporary file, and
|
---|
82 | /// then renames the temporary file to the desired name. If necessary, this
|
---|
83 | /// method will delete a pre-existing file before the rename.
|
---|
84 | /// </para>
|
---|
85 | ///
|
---|
86 | /// <para>
|
---|
87 | /// The <see cref="ZipFile.Name"/> property is specified either explicitly,
|
---|
88 | /// or implicitly using one of the parameterized ZipFile constructors. For
|
---|
89 | /// COM Automation clients, the <c>Name</c> property must be set explicitly,
|
---|
90 | /// because COM Automation clients cannot call parameterized constructors.
|
---|
91 | /// </para>
|
---|
92 | ///
|
---|
93 | /// <para>
|
---|
94 | /// When using a filesystem file for the Zip output, it is possible to call
|
---|
95 | /// <c>Save</c> multiple times on the <c>ZipFile</c> instance. With each
|
---|
96 | /// call the zip content is re-written to the same output file.
|
---|
97 | /// </para>
|
---|
98 | ///
|
---|
99 | /// <para>
|
---|
100 | /// Data for entries that have been added to the <c>ZipFile</c> instance is
|
---|
101 | /// written to the output when the <c>Save</c> method is called. This means
|
---|
102 | /// that the input streams for those entries must be available at the time
|
---|
103 | /// the application calls <c>Save</c>. If, for example, the application
|
---|
104 | /// adds entries with <c>AddEntry</c> using a dynamically-allocated
|
---|
105 | /// <c>MemoryStream</c>, the memory stream must not have been disposed
|
---|
106 | /// before the call to <c>Save</c>. See the <see
|
---|
107 | /// cref="ZipEntry.InputStream"/> property for more discussion of the
|
---|
108 | /// availability requirements of the input stream for an entry, and an
|
---|
109 | /// approach for providing just-in-time stream lifecycle management.
|
---|
110 | /// </para>
|
---|
111 | ///
|
---|
112 | /// </remarks>
|
---|
113 | ///
|
---|
114 | /// <seealso cref="Ionic.Zip.ZipFile.AddEntry(String, System.IO.Stream)"/>
|
---|
115 | ///
|
---|
116 | /// <exception cref="Ionic.Zip.BadStateException">
|
---|
117 | /// Thrown if you haven't specified a location or stream for saving the zip,
|
---|
118 | /// either in the constructor or by setting the Name property, or if you try
|
---|
119 | /// to save a regular zip archive to a filename with a .exe extension.
|
---|
120 | /// </exception>
|
---|
121 | ///
|
---|
122 | /// <exception cref="System.OverflowException">
|
---|
123 | /// Thrown if <see cref="MaxOutputSegmentSize"/> is non-zero, and the number
|
---|
124 | /// of segments that would be generated for the spanned zip file during the
|
---|
125 | /// save operation exceeds 99. If this happens, you need to increase the
|
---|
126 | /// segment size.
|
---|
127 | /// </exception>
|
---|
128 | ///
|
---|
129 | public void Save()
|
---|
130 | {
|
---|
131 | try
|
---|
132 | {
|
---|
133 | bool thisSaveUsedZip64 = false;
|
---|
134 | _saveOperationCanceled = false;
|
---|
135 | _numberOfSegmentsForMostRecentSave = 0;
|
---|
136 | OnSaveStarted();
|
---|
137 |
|
---|
138 | if (WriteStream == null)
|
---|
139 | throw new BadStateException("You haven't specified where to save the zip.");
|
---|
140 |
|
---|
141 | if (_name != null && _name.EndsWith(".exe") && !_SavingSfx)
|
---|
142 | throw new BadStateException("You specified an EXE for a plain zip file.");
|
---|
143 |
|
---|
144 | // check if modified, before saving.
|
---|
145 | if (!_contentsChanged)
|
---|
146 | {
|
---|
147 | OnSaveCompleted();
|
---|
148 | if (Verbose) StatusMessageTextWriter.WriteLine("No save is necessary....");
|
---|
149 | return;
|
---|
150 | }
|
---|
151 |
|
---|
152 | Reset(true);
|
---|
153 |
|
---|
154 | if (Verbose) StatusMessageTextWriter.WriteLine("saving....");
|
---|
155 |
|
---|
156 | // validate the number of entries
|
---|
157 | if (_entries.Count >= 0xFFFF && _zip64 == Zip64Option.Never)
|
---|
158 | throw new ZipException("The number of entries is 65535 or greater. Consider setting the UseZip64WhenSaving property on the ZipFile instance.");
|
---|
159 |
|
---|
160 |
|
---|
161 | // write an entry in the zip for each file
|
---|
162 | int n = 0;
|
---|
163 | // workitem 9831
|
---|
164 | ICollection<ZipEntry> c = (SortEntriesBeforeSaving) ? EntriesSorted : Entries;
|
---|
165 | foreach (ZipEntry e in c) // _entries.Values
|
---|
166 | {
|
---|
167 | OnSaveEntry(n, e, true);
|
---|
168 | e.Write(WriteStream);
|
---|
169 | if (_saveOperationCanceled)
|
---|
170 | break;
|
---|
171 |
|
---|
172 | n++;
|
---|
173 | OnSaveEntry(n, e, false);
|
---|
174 | if (_saveOperationCanceled)
|
---|
175 | break;
|
---|
176 |
|
---|
177 | // Some entries can be skipped during the save.
|
---|
178 | if (e.IncludedInMostRecentSave)
|
---|
179 | thisSaveUsedZip64 |= e.OutputUsedZip64.Value;
|
---|
180 | }
|
---|
181 |
|
---|
182 |
|
---|
183 |
|
---|
184 | if (_saveOperationCanceled)
|
---|
185 | return;
|
---|
186 |
|
---|
187 | var zss = WriteStream as ZipSegmentedStream;
|
---|
188 |
|
---|
189 | _numberOfSegmentsForMostRecentSave = (zss!=null)
|
---|
190 | ? zss.CurrentSegment
|
---|
191 | : 1;
|
---|
192 |
|
---|
193 | bool directoryNeededZip64 =
|
---|
194 | ZipOutput.WriteCentralDirectoryStructure
|
---|
195 | (WriteStream,
|
---|
196 | c,
|
---|
197 | _numberOfSegmentsForMostRecentSave,
|
---|
198 | _zip64,
|
---|
199 | Comment,
|
---|
200 | new ZipContainer(this));
|
---|
201 |
|
---|
202 | OnSaveEvent(ZipProgressEventType.Saving_AfterSaveTempArchive);
|
---|
203 |
|
---|
204 | _hasBeenSaved = true;
|
---|
205 | _contentsChanged = false;
|
---|
206 |
|
---|
207 | thisSaveUsedZip64 |= directoryNeededZip64;
|
---|
208 | _OutputUsesZip64 = new Nullable<bool>(thisSaveUsedZip64);
|
---|
209 |
|
---|
210 |
|
---|
211 | // do the rename as necessary
|
---|
212 | if (_name != null &&
|
---|
213 | (_temporaryFileName!=null || zss != null))
|
---|
214 | {
|
---|
215 | // _temporaryFileName may remain null if we are writing to a stream.
|
---|
216 | // only close the stream if there is a file behind it.
|
---|
217 | #if NETCF
|
---|
218 | WriteStream.Close();
|
---|
219 | #else
|
---|
220 | WriteStream.Dispose();
|
---|
221 | #endif
|
---|
222 | if (_saveOperationCanceled)
|
---|
223 | return;
|
---|
224 |
|
---|
225 | if (_fileAlreadyExists && this._readstream != null)
|
---|
226 | {
|
---|
227 | // This means we opened and read a zip file.
|
---|
228 | // If we are now saving to the same file, we need to close the
|
---|
229 | // orig file, first.
|
---|
230 | this._readstream.Close();
|
---|
231 | this._readstream = null;
|
---|
232 | // the archiveStream for each entry needs to be null
|
---|
233 | foreach (var e in c)
|
---|
234 | {
|
---|
235 | var zss1 = e._archiveStream as ZipSegmentedStream;
|
---|
236 | if (zss1 != null)
|
---|
237 | #if NETCF
|
---|
238 | zss1.Close();
|
---|
239 | #else
|
---|
240 | zss1.Dispose();
|
---|
241 | #endif
|
---|
242 | e._archiveStream = null;
|
---|
243 | }
|
---|
244 | }
|
---|
245 |
|
---|
246 | string tmpName = null;
|
---|
247 | if (File.Exists(_name))
|
---|
248 | {
|
---|
249 | // the steps:
|
---|
250 | //
|
---|
251 | // 1. Delete tmpName
|
---|
252 | // 2. move existing zip to tmpName
|
---|
253 | // 3. rename (File.Move) working file to name of existing zip
|
---|
254 | // 4. delete tmpName
|
---|
255 | //
|
---|
256 | // This series of steps avoids the exception,
|
---|
257 | // System.IO.IOException:
|
---|
258 | // "Cannot create a file when that file already exists."
|
---|
259 | //
|
---|
260 | // Cannot just call File.Replace() here because
|
---|
261 | // there is a possibility that the TEMP volume is different
|
---|
262 | // that the volume for the final file (c:\ vs d:\).
|
---|
263 | // So we need to do a Delete+Move pair.
|
---|
264 | //
|
---|
265 | // But, when doing the delete, Windows allows a process to
|
---|
266 | // delete the file, even though it is held open by, say, a
|
---|
267 | // virus scanner. It gets internally marked as "delete
|
---|
268 | // pending". The file does not actually get removed from the
|
---|
269 | // file system, it is still there after the File.Delete
|
---|
270 | // call.
|
---|
271 | //
|
---|
272 | // Therefore, we need to move the existing zip, which may be
|
---|
273 | // held open, to some other name. Then rename our working
|
---|
274 | // file to the desired name, then delete (possibly delete
|
---|
275 | // pending) the "other name".
|
---|
276 | //
|
---|
277 | // Ideally this would be transactional. It's possible that the
|
---|
278 | // delete succeeds and the move fails. Lacking transactions, if
|
---|
279 | // this kind of failure happens, we're hosed, and this logic will
|
---|
280 | // throw on the next File.Move().
|
---|
281 | //
|
---|
282 | //File.Delete(_name);
|
---|
283 | // workitem 10447
|
---|
284 | #if NETCF || SILVERLIGHT
|
---|
285 | tmpName = _name + "." + SharedUtilities.GenerateRandomStringImpl(8,0) + ".tmp";
|
---|
286 | #else
|
---|
287 | tmpName = _name + "." + Path.GetRandomFileName();
|
---|
288 | #endif
|
---|
289 | if (File.Exists(tmpName))
|
---|
290 | DeleteFileWithRetry(tmpName);
|
---|
291 | File.Move(_name, tmpName);
|
---|
292 | }
|
---|
293 |
|
---|
294 | OnSaveEvent(ZipProgressEventType.Saving_BeforeRenameTempArchive);
|
---|
295 | File.Move((zss != null) ? zss.CurrentTempName : _temporaryFileName,
|
---|
296 | _name);
|
---|
297 |
|
---|
298 | OnSaveEvent(ZipProgressEventType.Saving_AfterRenameTempArchive);
|
---|
299 |
|
---|
300 | if (tmpName != null)
|
---|
301 | {
|
---|
302 | try
|
---|
303 | {
|
---|
304 | // not critical
|
---|
305 | if (File.Exists(tmpName))
|
---|
306 | File.Delete(tmpName);
|
---|
307 | }
|
---|
308 | catch
|
---|
309 | {
|
---|
310 | // don't care about exceptions here.
|
---|
311 | }
|
---|
312 |
|
---|
313 | }
|
---|
314 | _fileAlreadyExists = true;
|
---|
315 | }
|
---|
316 |
|
---|
317 | NotifyEntriesSaveComplete(c);
|
---|
318 | OnSaveCompleted();
|
---|
319 | _JustSaved = true;
|
---|
320 | }
|
---|
321 |
|
---|
322 | // workitem 5043
|
---|
323 | finally
|
---|
324 | {
|
---|
325 | CleanupAfterSaveOperation();
|
---|
326 | }
|
---|
327 |
|
---|
328 | return;
|
---|
329 | }
|
---|
330 |
|
---|
331 |
|
---|
332 |
|
---|
333 | private static void NotifyEntriesSaveComplete(ICollection<ZipEntry> c)
|
---|
334 | {
|
---|
335 | foreach (ZipEntry e in c)
|
---|
336 | {
|
---|
337 | e.NotifySaveComplete();
|
---|
338 | }
|
---|
339 | }
|
---|
340 |
|
---|
341 |
|
---|
342 | private void RemoveTempFile()
|
---|
343 | {
|
---|
344 | try
|
---|
345 | {
|
---|
346 | if (File.Exists(_temporaryFileName))
|
---|
347 | {
|
---|
348 | File.Delete(_temporaryFileName);
|
---|
349 | }
|
---|
350 | }
|
---|
351 | catch (IOException ex1)
|
---|
352 | {
|
---|
353 | if (Verbose)
|
---|
354 | StatusMessageTextWriter.WriteLine("ZipFile::Save: could not delete temp file: {0}.", ex1.Message);
|
---|
355 | }
|
---|
356 | }
|
---|
357 |
|
---|
358 |
|
---|
359 | private void CleanupAfterSaveOperation()
|
---|
360 | {
|
---|
361 | if (_name != null)
|
---|
362 | {
|
---|
363 | // close the stream if there is a file behind it.
|
---|
364 | if (_writestream != null)
|
---|
365 | {
|
---|
366 | try
|
---|
367 | {
|
---|
368 | // workitem 7704
|
---|
369 | #if NETCF
|
---|
370 | _writestream.Close();
|
---|
371 | #else
|
---|
372 | _writestream.Dispose();
|
---|
373 | #endif
|
---|
374 | }
|
---|
375 | catch (System.IO.IOException) { }
|
---|
376 | }
|
---|
377 | _writestream = null;
|
---|
378 |
|
---|
379 | if (_temporaryFileName != null)
|
---|
380 | {
|
---|
381 | RemoveTempFile();
|
---|
382 | _temporaryFileName = null;
|
---|
383 | }
|
---|
384 | }
|
---|
385 | }
|
---|
386 |
|
---|
387 |
|
---|
388 | /// <summary>
|
---|
389 | /// Save the file to a new zipfile, with the given name.
|
---|
390 | /// </summary>
|
---|
391 | ///
|
---|
392 | /// <remarks>
|
---|
393 | /// <para>
|
---|
394 | /// This method allows the application to explicitly specify the name of the zip
|
---|
395 | /// file when saving. Use this when creating a new zip file, or when
|
---|
396 | /// updating a zip archive.
|
---|
397 | /// </para>
|
---|
398 | ///
|
---|
399 | /// <para>
|
---|
400 | /// An application can also save a zip archive in several places by calling this
|
---|
401 | /// method multiple times in succession, with different filenames.
|
---|
402 | /// </para>
|
---|
403 | ///
|
---|
404 | /// <para>
|
---|
405 | /// The <c>ZipFile</c> instance is written to storage, typically a zip file in a
|
---|
406 | /// filesystem, only when the caller calls <c>Save</c>. The Save operation writes
|
---|
407 | /// the zip content to a temporary file, and then renames the temporary file
|
---|
408 | /// to the desired name. If necessary, this method will delete a pre-existing file
|
---|
409 | /// before the rename.
|
---|
410 | /// </para>
|
---|
411 | ///
|
---|
412 | /// </remarks>
|
---|
413 | ///
|
---|
414 | /// <exception cref="System.ArgumentException">
|
---|
415 | /// Thrown if you specify a directory for the filename.
|
---|
416 | /// </exception>
|
---|
417 | ///
|
---|
418 | /// <param name="fileName">
|
---|
419 | /// The name of the zip archive to save to. Existing files will
|
---|
420 | /// be overwritten with great prejudice.
|
---|
421 | /// </param>
|
---|
422 | ///
|
---|
423 | /// <example>
|
---|
424 | /// This example shows how to create and Save a zip file.
|
---|
425 | /// <code>
|
---|
426 | /// using (ZipFile zip = new ZipFile())
|
---|
427 | /// {
|
---|
428 | /// zip.AddDirectory(@"c:\reports\January");
|
---|
429 | /// zip.Save("January.zip");
|
---|
430 | /// }
|
---|
431 | /// </code>
|
---|
432 | ///
|
---|
433 | /// <code lang="VB">
|
---|
434 | /// Using zip As New ZipFile()
|
---|
435 | /// zip.AddDirectory("c:\reports\January")
|
---|
436 | /// zip.Save("January.zip")
|
---|
437 | /// End Using
|
---|
438 | /// </code>
|
---|
439 | ///
|
---|
440 | /// </example>
|
---|
441 | ///
|
---|
442 | /// <example>
|
---|
443 | /// This example shows how to update a zip file.
|
---|
444 | /// <code>
|
---|
445 | /// using (ZipFile zip = ZipFile.Read("ExistingArchive.zip"))
|
---|
446 | /// {
|
---|
447 | /// zip.AddFile("NewData.csv");
|
---|
448 | /// zip.Save("UpdatedArchive.zip");
|
---|
449 | /// }
|
---|
450 | /// </code>
|
---|
451 | ///
|
---|
452 | /// <code lang="VB">
|
---|
453 | /// Using zip As ZipFile = ZipFile.Read("ExistingArchive.zip")
|
---|
454 | /// zip.AddFile("NewData.csv")
|
---|
455 | /// zip.Save("UpdatedArchive.zip")
|
---|
456 | /// End Using
|
---|
457 | /// </code>
|
---|
458 | ///
|
---|
459 | /// </example>
|
---|
460 | public void Save(String fileName)
|
---|
461 | {
|
---|
462 | // Check for the case where we are re-saving a zip archive
|
---|
463 | // that was originally instantiated with a stream. In that case,
|
---|
464 | // the _name will be null. If so, we set _writestream to null,
|
---|
465 | // which insures that we'll cons up a new WriteStream (with a filesystem
|
---|
466 | // file backing it) in the Save() method.
|
---|
467 | if (_name == null)
|
---|
468 | _writestream = null;
|
---|
469 |
|
---|
470 | else _readName = _name; // workitem 13915
|
---|
471 |
|
---|
472 | _name = fileName;
|
---|
473 | if (Directory.Exists(_name))
|
---|
474 | throw new ZipException("Bad Directory", new System.ArgumentException("That name specifies an existing directory. Please specify a filename.", "fileName"));
|
---|
475 | _contentsChanged = true;
|
---|
476 | _fileAlreadyExists = File.Exists(_name);
|
---|
477 | Save();
|
---|
478 | }
|
---|
479 |
|
---|
480 |
|
---|
481 | /// <summary>
|
---|
482 | /// Save the zip archive to the specified stream.
|
---|
483 | /// </summary>
|
---|
484 | ///
|
---|
485 | /// <remarks>
|
---|
486 | /// <para>
|
---|
487 | /// The <c>ZipFile</c> instance is written to storage - typically a zip file
|
---|
488 | /// in a filesystem, but using this overload, the storage can be anything
|
---|
489 | /// accessible via a writable stream - only when the caller calls <c>Save</c>.
|
---|
490 | /// </para>
|
---|
491 | ///
|
---|
492 | /// <para>
|
---|
493 | /// Use this method to save the zip content to a stream directly. A common
|
---|
494 | /// scenario is an ASP.NET application that dynamically generates a zip file
|
---|
495 | /// and allows the browser to download it. The application can call
|
---|
496 | /// <c>Save(Response.OutputStream)</c> to write a zipfile directly to the
|
---|
497 | /// output stream, without creating a zip file on the disk on the ASP.NET
|
---|
498 | /// server.
|
---|
499 | /// </para>
|
---|
500 | ///
|
---|
501 | /// <para>
|
---|
502 | /// Be careful when saving a file to a non-seekable stream, including
|
---|
503 | /// <c>Response.OutputStream</c>. When DotNetZip writes to a non-seekable
|
---|
504 | /// stream, the zip archive is formatted in such a way that may not be
|
---|
505 | /// compatible with all zip tools on all platforms. It's a perfectly legal
|
---|
506 | /// and compliant zip file, but some people have reported problems opening
|
---|
507 | /// files produced this way using the Mac OS archive utility.
|
---|
508 | /// </para>
|
---|
509 | ///
|
---|
510 | /// </remarks>
|
---|
511 | ///
|
---|
512 | /// <example>
|
---|
513 | ///
|
---|
514 | /// This example saves the zipfile content into a MemoryStream, and
|
---|
515 | /// then gets the array of bytes from that MemoryStream.
|
---|
516 | ///
|
---|
517 | /// <code lang="C#">
|
---|
518 | /// using (var zip = new Ionic.Zip.ZipFile())
|
---|
519 | /// {
|
---|
520 | /// zip.CompressionLevel= Ionic.Zlib.CompressionLevel.BestCompression;
|
---|
521 | /// zip.Password = "VerySecret.";
|
---|
522 | /// zip.Encryption = EncryptionAlgorithm.WinZipAes128;
|
---|
523 | /// zip.AddFile(sourceFileName);
|
---|
524 | /// MemoryStream output = new MemoryStream();
|
---|
525 | /// zip.Save(output);
|
---|
526 | ///
|
---|
527 | /// byte[] zipbytes = output.ToArray();
|
---|
528 | /// }
|
---|
529 | /// </code>
|
---|
530 | /// </example>
|
---|
531 | ///
|
---|
532 | /// <example>
|
---|
533 | /// <para>
|
---|
534 | /// This example shows a pitfall you should avoid. DO NOT read
|
---|
535 | /// from a stream, then try to save to the same stream. DO
|
---|
536 | /// NOT DO THIS:
|
---|
537 | /// </para>
|
---|
538 | ///
|
---|
539 | /// <code lang="C#">
|
---|
540 | /// using (var fs = new FileSteeam(filename, FileMode.Open))
|
---|
541 | /// {
|
---|
542 | /// using (var zip = Ionic.Zip.ZipFile.Read(inputStream))
|
---|
543 | /// {
|
---|
544 | /// zip.AddEntry("Name1.txt", "this is the content");
|
---|
545 | /// zip.Save(inputStream); // NO NO NO!!
|
---|
546 | /// }
|
---|
547 | /// }
|
---|
548 | /// </code>
|
---|
549 | ///
|
---|
550 | /// <para>
|
---|
551 | /// Better like this:
|
---|
552 | /// </para>
|
---|
553 | ///
|
---|
554 | /// <code lang="C#">
|
---|
555 | /// using (var zip = Ionic.Zip.ZipFile.Read(filename))
|
---|
556 | /// {
|
---|
557 | /// zip.AddEntry("Name1.txt", "this is the content");
|
---|
558 | /// zip.Save(); // YES!
|
---|
559 | /// }
|
---|
560 | /// </code>
|
---|
561 | ///
|
---|
562 | /// </example>
|
---|
563 | ///
|
---|
564 | /// <param name="outputStream">
|
---|
565 | /// The <c>System.IO.Stream</c> to write to. It must be
|
---|
566 | /// writable. If you created the ZipFile instanct by calling
|
---|
567 | /// ZipFile.Read(), this stream must not be the same stream
|
---|
568 | /// you passed to ZipFile.Read().
|
---|
569 | /// </param>
|
---|
570 | public void Save(Stream outputStream)
|
---|
571 | {
|
---|
572 | if (outputStream == null)
|
---|
573 | throw new ArgumentNullException("outputStream");
|
---|
574 | if (!outputStream.CanWrite)
|
---|
575 | throw new ArgumentException("Must be a writable stream.", "outputStream");
|
---|
576 |
|
---|
577 | // if we had a filename to save to, we are now obliterating it.
|
---|
578 | _name = null;
|
---|
579 |
|
---|
580 | _writestream = new CountingStream(outputStream);
|
---|
581 |
|
---|
582 | _contentsChanged = true;
|
---|
583 | _fileAlreadyExists = false;
|
---|
584 | Save();
|
---|
585 | }
|
---|
586 |
|
---|
587 |
|
---|
588 | }
|
---|
589 |
|
---|
590 |
|
---|
591 |
|
---|
592 | internal static class ZipOutput
|
---|
593 | {
|
---|
594 | public static bool WriteCentralDirectoryStructure(Stream s,
|
---|
595 | ICollection<ZipEntry> entries,
|
---|
596 | uint numSegments,
|
---|
597 | Zip64Option zip64,
|
---|
598 | String comment,
|
---|
599 | ZipContainer container)
|
---|
600 | {
|
---|
601 | var zss = s as ZipSegmentedStream;
|
---|
602 | if (zss != null)
|
---|
603 | zss.ContiguousWrite = true;
|
---|
604 |
|
---|
605 | // write to a memory stream in order to keep the
|
---|
606 | // CDR contiguous
|
---|
607 | Int64 aLength = 0;
|
---|
608 | using (var ms = new MemoryStream())
|
---|
609 | {
|
---|
610 | foreach (ZipEntry e in entries)
|
---|
611 | {
|
---|
612 | if (e.IncludedInMostRecentSave)
|
---|
613 | {
|
---|
614 | // this writes a ZipDirEntry corresponding to the ZipEntry
|
---|
615 | e.WriteCentralDirectoryEntry(ms);
|
---|
616 | }
|
---|
617 | }
|
---|
618 | var a = ms.ToArray();
|
---|
619 | s.Write(a, 0, a.Length);
|
---|
620 | aLength = a.Length;
|
---|
621 | }
|
---|
622 |
|
---|
623 |
|
---|
624 | // We need to keep track of the start and
|
---|
625 | // Finish of the Central Directory Structure.
|
---|
626 |
|
---|
627 | // Cannot always use WriteStream.Length or Position; some streams do
|
---|
628 | // not support these. (eg, ASP.NET Response.OutputStream) In those
|
---|
629 | // cases we have a CountingStream.
|
---|
630 |
|
---|
631 | // Also, we cannot just set Start as s.Position bfore the write, and Finish
|
---|
632 | // as s.Position after the write. In a split zip, the write may actually
|
---|
633 | // flip to the next segment. In that case, Start will be zero. But we
|
---|
634 | // don't know that til after we know the size of the thing to write. So the
|
---|
635 | // answer is to compute the directory, then ask the ZipSegmentedStream which
|
---|
636 | // segment that directory would fall in, it it were written. Then, include
|
---|
637 | // that data into the directory, and finally, write the directory to the
|
---|
638 | // output stream.
|
---|
639 |
|
---|
640 | var output = s as CountingStream;
|
---|
641 | long Finish = (output != null) ? output.ComputedPosition : s.Position; // BytesWritten
|
---|
642 | long Start = Finish - aLength;
|
---|
643 |
|
---|
644 | // need to know which segment the EOCD record starts in
|
---|
645 | UInt32 startSegment = (zss != null)
|
---|
646 | ? zss.CurrentSegment
|
---|
647 | : 0;
|
---|
648 |
|
---|
649 | Int64 SizeOfCentralDirectory = Finish - Start;
|
---|
650 |
|
---|
651 | int countOfEntries = CountEntries(entries);
|
---|
652 |
|
---|
653 | bool needZip64CentralDirectory =
|
---|
654 | zip64 == Zip64Option.Always ||
|
---|
655 | countOfEntries >= 0xFFFF ||
|
---|
656 | SizeOfCentralDirectory > 0xFFFFFFFF ||
|
---|
657 | Start > 0xFFFFFFFF;
|
---|
658 |
|
---|
659 | byte[] a2 = null;
|
---|
660 |
|
---|
661 | // emit ZIP64 extensions as required
|
---|
662 | if (needZip64CentralDirectory)
|
---|
663 | {
|
---|
664 | if (zip64 == Zip64Option.Never)
|
---|
665 | {
|
---|
666 | #if NETCF
|
---|
667 | throw new ZipException("The archive requires a ZIP64 Central Directory. Consider enabling ZIP64 extensions.");
|
---|
668 | #else
|
---|
669 | System.Diagnostics.StackFrame sf = new System.Diagnostics.StackFrame(1);
|
---|
670 | if (sf.GetMethod().DeclaringType == typeof(ZipFile))
|
---|
671 | throw new ZipException("The archive requires a ZIP64 Central Directory. Consider setting the ZipFile.UseZip64WhenSaving property.");
|
---|
672 | else
|
---|
673 | throw new ZipException("The archive requires a ZIP64 Central Directory. Consider setting the ZipOutputStream.EnableZip64 property.");
|
---|
674 | #endif
|
---|
675 |
|
---|
676 | }
|
---|
677 |
|
---|
678 | var a = GenZip64EndOfCentralDirectory(Start, Finish, countOfEntries, numSegments);
|
---|
679 | a2 = GenCentralDirectoryFooter(Start, Finish, zip64, countOfEntries, comment, container);
|
---|
680 | if (startSegment != 0)
|
---|
681 | {
|
---|
682 | UInt32 thisSegment = zss.ComputeSegment(a.Length + a2.Length);
|
---|
683 | int i = 16;
|
---|
684 | // number of this disk
|
---|
685 | Array.Copy(BitConverter.GetBytes(thisSegment), 0, a, i, 4);
|
---|
686 | i += 4;
|
---|
687 | // number of the disk with the start of the central directory
|
---|
688 | //Array.Copy(BitConverter.GetBytes(startSegment), 0, a, i, 4);
|
---|
689 | Array.Copy(BitConverter.GetBytes(thisSegment), 0, a, i, 4);
|
---|
690 |
|
---|
691 | i = 60;
|
---|
692 | // offset 60
|
---|
693 | // number of the disk with the start of the zip64 eocd
|
---|
694 | Array.Copy(BitConverter.GetBytes(thisSegment), 0, a, i, 4);
|
---|
695 | i += 4;
|
---|
696 | i += 8;
|
---|
697 |
|
---|
698 | // offset 72
|
---|
699 | // total number of disks
|
---|
700 | Array.Copy(BitConverter.GetBytes(thisSegment), 0, a, i, 4);
|
---|
701 | }
|
---|
702 | s.Write(a, 0, a.Length);
|
---|
703 | }
|
---|
704 | else
|
---|
705 | a2 = GenCentralDirectoryFooter(Start, Finish, zip64, countOfEntries, comment, container);
|
---|
706 |
|
---|
707 |
|
---|
708 | // now, the regular footer
|
---|
709 | if (startSegment != 0)
|
---|
710 | {
|
---|
711 | // The assumption is the central directory is never split across
|
---|
712 | // segment boundaries.
|
---|
713 |
|
---|
714 | UInt16 thisSegment = (UInt16) zss.ComputeSegment(a2.Length);
|
---|
715 | int i = 4;
|
---|
716 | // number of this disk
|
---|
717 | Array.Copy(BitConverter.GetBytes(thisSegment), 0, a2, i, 2);
|
---|
718 | i += 2;
|
---|
719 | // number of the disk with the start of the central directory
|
---|
720 | //Array.Copy(BitConverter.GetBytes((UInt16)startSegment), 0, a2, i, 2);
|
---|
721 | Array.Copy(BitConverter.GetBytes(thisSegment), 0, a2, i, 2);
|
---|
722 | i += 2;
|
---|
723 | }
|
---|
724 |
|
---|
725 | s.Write(a2, 0, a2.Length);
|
---|
726 |
|
---|
727 | // reset the contiguous write property if necessary
|
---|
728 | if (zss != null)
|
---|
729 | zss.ContiguousWrite = false;
|
---|
730 |
|
---|
731 | return needZip64CentralDirectory;
|
---|
732 | }
|
---|
733 |
|
---|
734 |
|
---|
735 | private static System.Text.Encoding GetEncoding(ZipContainer container, string t)
|
---|
736 | {
|
---|
737 | switch (container.AlternateEncodingUsage)
|
---|
738 | {
|
---|
739 | case ZipOption.Always:
|
---|
740 | return container.AlternateEncoding;
|
---|
741 | case ZipOption.Never:
|
---|
742 | return container.DefaultEncoding;
|
---|
743 | }
|
---|
744 |
|
---|
745 | // AsNecessary is in force
|
---|
746 | var e = container.DefaultEncoding;
|
---|
747 | if (t == null) return e;
|
---|
748 |
|
---|
749 | var bytes = e.GetBytes(t);
|
---|
750 | var t2 = e.GetString(bytes,0,bytes.Length);
|
---|
751 | if (t2.Equals(t)) return e;
|
---|
752 | return container.AlternateEncoding;
|
---|
753 | }
|
---|
754 |
|
---|
755 |
|
---|
756 |
|
---|
757 | private static byte[] GenCentralDirectoryFooter(long StartOfCentralDirectory,
|
---|
758 | long EndOfCentralDirectory,
|
---|
759 | Zip64Option zip64,
|
---|
760 | int entryCount,
|
---|
761 | string comment,
|
---|
762 | ZipContainer container)
|
---|
763 | {
|
---|
764 | System.Text.Encoding encoding = GetEncoding(container, comment);
|
---|
765 | int j = 0;
|
---|
766 | int bufferLength = 22;
|
---|
767 | byte[] block = null;
|
---|
768 | Int16 commentLength = 0;
|
---|
769 | if ((comment != null) && (comment.Length != 0))
|
---|
770 | {
|
---|
771 | block = encoding.GetBytes(comment);
|
---|
772 | commentLength = (Int16)block.Length;
|
---|
773 | }
|
---|
774 | bufferLength += commentLength;
|
---|
775 | byte[] bytes = new byte[bufferLength];
|
---|
776 |
|
---|
777 | int i = 0;
|
---|
778 | // signature
|
---|
779 | byte[] sig = BitConverter.GetBytes(ZipConstants.EndOfCentralDirectorySignature);
|
---|
780 | Array.Copy(sig, 0, bytes, i, 4);
|
---|
781 | i+=4;
|
---|
782 |
|
---|
783 | // number of this disk
|
---|
784 | // (this number may change later)
|
---|
785 | bytes[i++] = 0;
|
---|
786 | bytes[i++] = 0;
|
---|
787 |
|
---|
788 | // number of the disk with the start of the central directory
|
---|
789 | // (this number may change later)
|
---|
790 | bytes[i++] = 0;
|
---|
791 | bytes[i++] = 0;
|
---|
792 |
|
---|
793 | // handle ZIP64 extensions for the end-of-central-directory
|
---|
794 | if (entryCount >= 0xFFFF || zip64 == Zip64Option.Always)
|
---|
795 | {
|
---|
796 | // the ZIP64 version.
|
---|
797 | for (j = 0; j < 4; j++)
|
---|
798 | bytes[i++] = 0xFF;
|
---|
799 | }
|
---|
800 | else
|
---|
801 | {
|
---|
802 | // the standard version.
|
---|
803 | // total number of entries in the central dir on this disk
|
---|
804 | bytes[i++] = (byte)(entryCount & 0x00FF);
|
---|
805 | bytes[i++] = (byte)((entryCount & 0xFF00) >> 8);
|
---|
806 |
|
---|
807 | // total number of entries in the central directory
|
---|
808 | bytes[i++] = (byte)(entryCount & 0x00FF);
|
---|
809 | bytes[i++] = (byte)((entryCount & 0xFF00) >> 8);
|
---|
810 | }
|
---|
811 |
|
---|
812 | // size of the central directory
|
---|
813 | Int64 SizeOfCentralDirectory = EndOfCentralDirectory - StartOfCentralDirectory;
|
---|
814 |
|
---|
815 | if (SizeOfCentralDirectory >= 0xFFFFFFFF || StartOfCentralDirectory >= 0xFFFFFFFF)
|
---|
816 | {
|
---|
817 | // The actual data is in the ZIP64 central directory structure
|
---|
818 | for (j = 0; j < 8; j++)
|
---|
819 | bytes[i++] = 0xFF;
|
---|
820 | }
|
---|
821 | else
|
---|
822 | {
|
---|
823 | // size of the central directory (we just get the low 4 bytes)
|
---|
824 | bytes[i++] = (byte)(SizeOfCentralDirectory & 0x000000FF);
|
---|
825 | bytes[i++] = (byte)((SizeOfCentralDirectory & 0x0000FF00) >> 8);
|
---|
826 | bytes[i++] = (byte)((SizeOfCentralDirectory & 0x00FF0000) >> 16);
|
---|
827 | bytes[i++] = (byte)((SizeOfCentralDirectory & 0xFF000000) >> 24);
|
---|
828 |
|
---|
829 | // offset of the start of the central directory (we just get the low 4 bytes)
|
---|
830 | bytes[i++] = (byte)(StartOfCentralDirectory & 0x000000FF);
|
---|
831 | bytes[i++] = (byte)((StartOfCentralDirectory & 0x0000FF00) >> 8);
|
---|
832 | bytes[i++] = (byte)((StartOfCentralDirectory & 0x00FF0000) >> 16);
|
---|
833 | bytes[i++] = (byte)((StartOfCentralDirectory & 0xFF000000) >> 24);
|
---|
834 | }
|
---|
835 |
|
---|
836 |
|
---|
837 | // zip archive comment
|
---|
838 | if ((comment == null) || (comment.Length == 0))
|
---|
839 | {
|
---|
840 | // no comment!
|
---|
841 | bytes[i++] = (byte)0;
|
---|
842 | bytes[i++] = (byte)0;
|
---|
843 | }
|
---|
844 | else
|
---|
845 | {
|
---|
846 | // the size of our buffer defines the max length of the comment we can write
|
---|
847 | if (commentLength + i + 2 > bytes.Length) commentLength = (Int16)(bytes.Length - i - 2);
|
---|
848 | bytes[i++] = (byte)(commentLength & 0x00FF);
|
---|
849 | bytes[i++] = (byte)((commentLength & 0xFF00) >> 8);
|
---|
850 |
|
---|
851 | if (commentLength != 0)
|
---|
852 | {
|
---|
853 | // now actually write the comment itself into the byte buffer
|
---|
854 | for (j = 0; (j < commentLength) && (i + j < bytes.Length); j++)
|
---|
855 | {
|
---|
856 | bytes[i + j] = block[j];
|
---|
857 | }
|
---|
858 | i += j;
|
---|
859 | }
|
---|
860 | }
|
---|
861 |
|
---|
862 | // s.Write(bytes, 0, i);
|
---|
863 | return bytes;
|
---|
864 | }
|
---|
865 |
|
---|
866 |
|
---|
867 |
|
---|
868 | private static byte[] GenZip64EndOfCentralDirectory(long StartOfCentralDirectory,
|
---|
869 | long EndOfCentralDirectory,
|
---|
870 | int entryCount,
|
---|
871 | uint numSegments)
|
---|
872 | {
|
---|
873 | const int bufferLength = 12 + 44 + 20;
|
---|
874 |
|
---|
875 | byte[] bytes = new byte[bufferLength];
|
---|
876 |
|
---|
877 | int i = 0;
|
---|
878 | // signature
|
---|
879 | byte[] sig = BitConverter.GetBytes(ZipConstants.Zip64EndOfCentralDirectoryRecordSignature);
|
---|
880 | Array.Copy(sig, 0, bytes, i, 4);
|
---|
881 | i+=4;
|
---|
882 |
|
---|
883 | // There is a possibility to include "Extensible" data in the zip64
|
---|
884 | // end-of-central-dir record. I cannot figure out what it might be used to
|
---|
885 | // store, so the size of this record is always fixed. Maybe it is used for
|
---|
886 | // strong encryption data? That is for another day.
|
---|
887 | long DataSize = 44;
|
---|
888 | Array.Copy(BitConverter.GetBytes(DataSize), 0, bytes, i, 8);
|
---|
889 | i += 8;
|
---|
890 |
|
---|
891 | // offset 12
|
---|
892 | // VersionMadeBy = 45;
|
---|
893 | bytes[i++] = 45;
|
---|
894 | bytes[i++] = 0x00;
|
---|
895 |
|
---|
896 | // VersionNeededToExtract = 45;
|
---|
897 | bytes[i++] = 45;
|
---|
898 | bytes[i++] = 0x00;
|
---|
899 |
|
---|
900 | // offset 16
|
---|
901 | // number of the disk, and the disk with the start of the central dir.
|
---|
902 | // (this may change later)
|
---|
903 | for (int j = 0; j < 8; j++)
|
---|
904 | bytes[i++] = 0x00;
|
---|
905 |
|
---|
906 | // offset 24
|
---|
907 | long numberOfEntries = entryCount;
|
---|
908 | Array.Copy(BitConverter.GetBytes(numberOfEntries), 0, bytes, i, 8);
|
---|
909 | i += 8;
|
---|
910 | Array.Copy(BitConverter.GetBytes(numberOfEntries), 0, bytes, i, 8);
|
---|
911 | i += 8;
|
---|
912 |
|
---|
913 | // offset 40
|
---|
914 | Int64 SizeofCentraldirectory = EndOfCentralDirectory - StartOfCentralDirectory;
|
---|
915 | Array.Copy(BitConverter.GetBytes(SizeofCentraldirectory), 0, bytes, i, 8);
|
---|
916 | i += 8;
|
---|
917 | Array.Copy(BitConverter.GetBytes(StartOfCentralDirectory), 0, bytes, i, 8);
|
---|
918 | i += 8;
|
---|
919 |
|
---|
920 | // offset 56
|
---|
921 | // now, the locator
|
---|
922 | // signature
|
---|
923 | sig = BitConverter.GetBytes(ZipConstants.Zip64EndOfCentralDirectoryLocatorSignature);
|
---|
924 | Array.Copy(sig, 0, bytes, i, 4);
|
---|
925 | i+=4;
|
---|
926 |
|
---|
927 | // offset 60
|
---|
928 | // number of the disk with the start of the zip64 eocd
|
---|
929 | // (this will change later) (it will?)
|
---|
930 | uint x2 = (numSegments==0)?0:(uint)(numSegments-1);
|
---|
931 | Array.Copy(BitConverter.GetBytes(x2), 0, bytes, i, 4);
|
---|
932 | i+=4;
|
---|
933 |
|
---|
934 | // offset 64
|
---|
935 | // relative offset of the zip64 eocd
|
---|
936 | Array.Copy(BitConverter.GetBytes(EndOfCentralDirectory), 0, bytes, i, 8);
|
---|
937 | i += 8;
|
---|
938 |
|
---|
939 | // offset 72
|
---|
940 | // total number of disks
|
---|
941 | // (this will change later)
|
---|
942 | Array.Copy(BitConverter.GetBytes(numSegments), 0, bytes, i, 4);
|
---|
943 | i+=4;
|
---|
944 |
|
---|
945 | return bytes;
|
---|
946 | }
|
---|
947 |
|
---|
948 |
|
---|
949 |
|
---|
950 | private static int CountEntries(ICollection<ZipEntry> _entries)
|
---|
951 | {
|
---|
952 | // Cannot just emit _entries.Count, because some of the entries
|
---|
953 | // may have been skipped.
|
---|
954 | int count = 0;
|
---|
955 | foreach (var entry in _entries)
|
---|
956 | if (entry.IncludedInMostRecentSave) count++;
|
---|
957 | return count;
|
---|
958 | }
|
---|
959 |
|
---|
960 |
|
---|
961 |
|
---|
962 |
|
---|
963 | }
|
---|
964 | }
|
---|