| // ZipSegmentedStream.cs | |
| // ------------------------------------------------------------------ | |
| // | |
| // Copyright (c) 2009-2011 Dino Chiesa. | |
| // All rights reserved. | |
| // | |
| // This code module is part of DotNetZip, a zipfile class library. | |
| // | |
| // ------------------------------------------------------------------ | |
| // | |
| // This code is licensed under the Microsoft Public License. | |
| // See the file License.txt for the license details. | |
| // More info on: http://dotnetzip.codeplex.com | |
| // | |
| // ------------------------------------------------------------------ | |
| // | |
| // last saved (in emacs): | |
| // Time-stamp: <2011-July-13 22:25:45> | |
| // | |
| // ------------------------------------------------------------------ | |
| // | |
| // This module defines logic for zip streams that span disk files. | |
| // | |
| // ------------------------------------------------------------------ | |
| using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| namespace OfficeOpenXml.Packaging.Ionic.Zip | |
| { | |
| internal class ZipSegmentedStream : System.IO.Stream | |
| { | |
| enum RwMode | |
| { | |
| None = 0, | |
| ReadOnly = 1, | |
| Write = 2, | |
| //Update = 3 | |
| } | |
| private RwMode rwMode; | |
| private bool _exceptionPending; // **see note below | |
| private string _baseName; | |
| private string _baseDir; | |
| //private bool _isDisposed; | |
| private string _currentName; | |
| private string _currentTempName; | |
| private uint _currentDiskNumber; | |
| private uint _maxDiskNumber; | |
| private int _maxSegmentSize; | |
| private System.IO.Stream _innerStream; | |
| // **Note regarding exceptions: | |
| // | |
| // When ZipSegmentedStream is employed within a using clause, | |
| // which is the typical scenario, and an exception is thrown | |
| // within the scope of the using, Dispose() is invoked | |
| // implicitly before processing the initial exception. If that | |
| // happens, this class sets _exceptionPending to true, and then | |
| // within the Dispose(bool), takes special action as | |
| // appropriate. Need to be careful: any additional exceptions | |
| // will mask the original one. | |
| private ZipSegmentedStream() : base() | |
| { | |
| _exceptionPending = false; | |
| } | |
| public static ZipSegmentedStream ForReading(string name, | |
| uint initialDiskNumber, | |
| uint maxDiskNumber) | |
| { | |
| ZipSegmentedStream zss = new ZipSegmentedStream() | |
| { | |
| rwMode = RwMode.ReadOnly, | |
| CurrentSegment = initialDiskNumber, | |
| _maxDiskNumber = maxDiskNumber, | |
| _baseName = name, | |
| }; | |
| // Console.WriteLine("ZSS: ForReading ({0})", | |
| // Path.GetFileName(zss.CurrentName)); | |
| zss._SetReadStream(); | |
| return zss; | |
| } | |
| public static ZipSegmentedStream ForWriting(string name, int maxSegmentSize) | |
| { | |
| ZipSegmentedStream zss = new ZipSegmentedStream() | |
| { | |
| rwMode = RwMode.Write, | |
| CurrentSegment = 0, | |
| _baseName = name, | |
| _maxSegmentSize = maxSegmentSize, | |
| _baseDir = Path.GetDirectoryName(name) | |
| }; | |
| // workitem 9522 | |
| if (zss._baseDir=="") zss._baseDir="."; | |
| zss._SetWriteStream(0); | |
| // Console.WriteLine("ZSS: ForWriting ({0})", | |
| // Path.GetFileName(zss.CurrentName)); | |
| return zss; | |
| } | |
| /// <summary> | |
| /// Sort-of like a factory method, ForUpdate is used only when | |
| /// the application needs to update the zip entry metadata for | |
| /// a segmented zip file, when the starting segment is earlier | |
| /// than the ending segment, for a particular entry. | |
| /// </summary> | |
| /// <remarks> | |
| /// <para> | |
| /// The update is always contiguous, never rolls over. As a | |
| /// result, this method doesn't need to return a ZSS; it can | |
| /// simply return a FileStream. That's why it's "sort of" | |
| /// like a Factory method. | |
| /// </para> | |
| /// <para> | |
| /// Caller must Close/Dispose the stream object returned by | |
| /// this method. | |
| /// </para> | |
| /// </remarks> | |
| public static Stream ForUpdate(string name, uint diskNumber) | |
| { | |
| if (diskNumber >= 99) | |
| throw new ArgumentOutOfRangeException("diskNumber"); | |
| string fname = | |
| String.Format("{0}.z{1:D2}", | |
| Path.Combine(Path.GetDirectoryName(name), | |
| Path.GetFileNameWithoutExtension(name)), | |
| diskNumber + 1); | |
| // Console.WriteLine("ZSS: ForUpdate ({0})", | |
| // Path.GetFileName(fname)); | |
| // This class assumes that the update will not expand the | |
| // size of the segment. Update is used only for an in-place | |
| // update of zip metadata. It never will try to write beyond | |
| // the end of a segment. | |
| return File.Open(fname, | |
| FileMode.Open, | |
| FileAccess.ReadWrite, | |
| FileShare.None); | |
| } | |
| public bool ContiguousWrite | |
| { | |
| get; | |
| set; | |
| } | |
| public UInt32 CurrentSegment | |
| { | |
| get | |
| { | |
| return _currentDiskNumber; | |
| } | |
| private set | |
| { | |
| _currentDiskNumber = value; | |
| _currentName = null; // it will get updated next time referenced | |
| } | |
| } | |
| /// <summary> | |
| /// Name of the filesystem file corresponding to the current segment. | |
| /// </summary> | |
| /// <remarks> | |
| /// <para> | |
| /// The name is not always the name currently being used in the | |
| /// filesystem. When rwMode is RwMode.Write, the filesystem file has a | |
| /// temporary name until the stream is closed or until the next segment is | |
| /// started. | |
| /// </para> | |
| /// </remarks> | |
| public String CurrentName | |
| { | |
| get | |
| { | |
| if (_currentName==null) | |
| _currentName = _NameForSegment(CurrentSegment); | |
| return _currentName; | |
| } | |
| } | |
| public String CurrentTempName | |
| { | |
| get | |
| { | |
| return _currentTempName; | |
| } | |
| } | |
| private string _NameForSegment(uint diskNumber) | |
| { | |
| if (diskNumber >= 99) | |
| { | |
| _exceptionPending = true; | |
| throw new OverflowException("The number of zip segments would exceed 99."); | |
| } | |
| return String.Format("{0}.z{1:D2}", | |
| Path.Combine(Path.GetDirectoryName(_baseName), | |
| Path.GetFileNameWithoutExtension(_baseName)), | |
| diskNumber + 1); | |
| } | |
| // Returns the segment that WILL be current if writing | |
| // a block of the given length. | |
| // This isn't exactly true. It could roll over beyond | |
| // this number. | |
| public UInt32 ComputeSegment(int length) | |
| { | |
| if (_innerStream.Position + length > _maxSegmentSize) | |
| // the block will go AT LEAST into the next segment | |
| return CurrentSegment + 1; | |
| // it will fit in the current segment | |
| return CurrentSegment; | |
| } | |
| public override String ToString() | |
| { | |
| return String.Format("{0}[{1}][{2}], pos=0x{3:X})", | |
| "ZipSegmentedStream", CurrentName, | |
| rwMode.ToString(), | |
| this.Position); | |
| } | |
| private void _SetReadStream() | |
| { | |
| if (_innerStream != null) | |
| { | |
| #if NETCF | |
| _innerStream.Close(); | |
| #else | |
| _innerStream.Dispose(); | |
| #endif | |
| } | |
| if (CurrentSegment + 1 == _maxDiskNumber) | |
| _currentName = _baseName; | |
| // Console.WriteLine("ZSS: SRS ({0})", | |
| // Path.GetFileName(CurrentName)); | |
| _innerStream = File.OpenRead(CurrentName); | |
| } | |
| /// <summary> | |
| /// Read from the stream | |
| /// </summary> | |
| /// <param name="buffer">the buffer to read</param> | |
| /// <param name="offset">the offset at which to start</param> | |
| /// <param name="count">the number of bytes to read</param> | |
| /// <returns>the number of bytes actually read</returns> | |
| public override int Read(byte[] buffer, int offset, int count) | |
| { | |
| if (rwMode != RwMode.ReadOnly) | |
| { | |
| _exceptionPending = true; | |
| throw new InvalidOperationException("Stream Error: Cannot Read."); | |
| } | |
| int r = _innerStream.Read(buffer, offset, count); | |
| int r1 = r; | |
| while (r1 != count) | |
| { | |
| if (_innerStream.Position != _innerStream.Length) | |
| { | |
| _exceptionPending = true; | |
| throw new ZipException(String.Format("Read error in file {0}", CurrentName)); | |
| } | |
| if (CurrentSegment + 1 == _maxDiskNumber) | |
| return r; // no more to read | |
| CurrentSegment++; | |
| _SetReadStream(); | |
| offset += r1; | |
| count -= r1; | |
| r1 = _innerStream.Read(buffer, offset, count); | |
| r += r1; | |
| } | |
| return r; | |
| } | |
| private void _SetWriteStream(uint increment) | |
| { | |
| if (_innerStream != null) | |
| { | |
| #if NETCF | |
| _innerStream.Close(); | |
| #else | |
| _innerStream.Dispose(); | |
| #endif | |
| if (File.Exists(CurrentName)) | |
| File.Delete(CurrentName); | |
| File.Move(_currentTempName, CurrentName); | |
| // Console.WriteLine("ZSS: SWS close ({0})", | |
| // Path.GetFileName(CurrentName)); | |
| } | |
| if (increment > 0) | |
| CurrentSegment += increment; | |
| SharedUtilities.CreateAndOpenUniqueTempFile(_baseDir, | |
| out _innerStream, | |
| out _currentTempName); | |
| // Console.WriteLine("ZSS: SWS open ({0})", | |
| // Path.GetFileName(_currentTempName)); | |
| if (CurrentSegment == 0) | |
| _innerStream.Write(BitConverter.GetBytes(ZipConstants.SplitArchiveSignature), 0, 4); | |
| } | |
| /// <summary> | |
| /// Write to the stream. | |
| /// </summary> | |
| /// <param name="buffer">the buffer from which to write</param> | |
| /// <param name="offset">the offset at which to start writing</param> | |
| /// <param name="count">the number of bytes to write</param> | |
| public override void Write(byte[] buffer, int offset, int count) | |
| { | |
| if (rwMode != RwMode.Write) | |
| { | |
| _exceptionPending = true; | |
| throw new InvalidOperationException("Stream Error: Cannot Write."); | |
| } | |
| if (ContiguousWrite) | |
| { | |
| // enough space for a contiguous write? | |
| if (_innerStream.Position + count > _maxSegmentSize) | |
| _SetWriteStream(1); | |
| } | |
| else | |
| { | |
| while (_innerStream.Position + count > _maxSegmentSize) | |
| { | |
| int c = unchecked(_maxSegmentSize - (int)_innerStream.Position); | |
| _innerStream.Write(buffer, offset, c); | |
| _SetWriteStream(1); | |
| count -= c; | |
| offset += c; | |
| } | |
| } | |
| _innerStream.Write(buffer, offset, count); | |
| } | |
| public long TruncateBackward(uint diskNumber, long offset) | |
| { | |
| // Console.WriteLine("***ZSS.Trunc to disk {0}", diskNumber); | |
| // Console.WriteLine("***ZSS.Trunc: current disk {0}", CurrentSegment); | |
| if (diskNumber >= 99) | |
| throw new ArgumentOutOfRangeException("diskNumber"); | |
| if (rwMode != RwMode.Write) | |
| { | |
| _exceptionPending = true; | |
| throw new ZipException("bad state."); | |
| } | |
| // Seek back in the segmented stream to a (maybe) prior segment. | |
| // Check if it is the same segment. If it is, very simple. | |
| if (diskNumber == CurrentSegment) | |
| { | |
| var x =_innerStream.Seek(offset, SeekOrigin.Begin); | |
| // workitem 10178 | |
| Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream); | |
| return x; | |
| } | |
| // Seeking back to a prior segment. | |
| // The current segment and any intervening segments must be removed. | |
| // First, close the current segment, and then remove it. | |
| if (_innerStream != null) | |
| { | |
| #if NETCF | |
| _innerStream.Close(); | |
| #else | |
| _innerStream.Dispose(); | |
| #endif | |
| if (File.Exists(_currentTempName)) | |
| File.Delete(_currentTempName); | |
| } | |
| // Now, remove intervening segments. | |
| for (uint j= CurrentSegment-1; j > diskNumber; j--) | |
| { | |
| string s = _NameForSegment(j); | |
| // Console.WriteLine("***ZSS.Trunc: removing file {0}", s); | |
| if (File.Exists(s)) | |
| File.Delete(s); | |
| } | |
| // now, open the desired segment. It must exist. | |
| CurrentSegment = diskNumber; | |
| // get a new temp file, try 3 times: | |
| for (int i = 0; i < 3; i++) | |
| { | |
| try | |
| { | |
| _currentTempName = SharedUtilities.InternalGetTempFileName(); | |
| // move the .z0x file back to a temp name | |
| File.Move(CurrentName, _currentTempName); | |
| break; // workitem 12403 | |
| } | |
| catch(IOException) | |
| { | |
| if (i == 2) throw; | |
| } | |
| } | |
| // open it | |
| _innerStream = new FileStream(_currentTempName, FileMode.Open); | |
| var r = _innerStream.Seek(offset, SeekOrigin.Begin); | |
| // workitem 10178 | |
| Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream); | |
| return r; | |
| } | |
| public override bool CanRead | |
| { | |
| get | |
| { | |
| return (rwMode == RwMode.ReadOnly && | |
| (_innerStream != null) && | |
| _innerStream.CanRead); | |
| } | |
| } | |
| public override bool CanSeek | |
| { | |
| get | |
| { | |
| return (_innerStream != null) && | |
| _innerStream.CanSeek; | |
| } | |
| } | |
| public override bool CanWrite | |
| { | |
| get | |
| { | |
| return (rwMode == RwMode.Write) && | |
| (_innerStream != null) && | |
| _innerStream.CanWrite; | |
| } | |
| } | |
| public override void Flush() | |
| { | |
| _innerStream.Flush(); | |
| } | |
| public override long Length | |
| { | |
| get | |
| { | |
| return _innerStream.Length; | |
| } | |
| } | |
| public override long Position | |
| { | |
| get { return _innerStream.Position; } | |
| set { _innerStream.Position = value; } | |
| } | |
| public override long Seek(long offset, System.IO.SeekOrigin origin) | |
| { | |
| var x = _innerStream.Seek(offset, origin); | |
| // workitem 10178 | |
| Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(_innerStream); | |
| return x; | |
| } | |
| public override void SetLength(long value) | |
| { | |
| if (rwMode != RwMode.Write) | |
| { | |
| _exceptionPending = true; | |
| throw new InvalidOperationException(); | |
| } | |
| _innerStream.SetLength(value); | |
| } | |
| protected override void Dispose(bool disposing) | |
| { | |
| // this gets called by Stream.Close() | |
| // if (_isDisposed) return; | |
| // _isDisposed = true; | |
| //Console.WriteLine("Dispose (mode={0})\n", rwMode.ToString()); | |
| try | |
| { | |
| if (_innerStream != null) | |
| { | |
| #if NETCF | |
| _innerStream.Close(); | |
| #else | |
| _innerStream.Dispose(); | |
| #endif | |
| //_innerStream = null; | |
| if (rwMode == RwMode.Write) | |
| { | |
| if (_exceptionPending) | |
| { | |
| // possibly could try to clean up all the | |
| // temp files created so far... | |
| } | |
| else | |
| { | |
| // // move the final temp file to the .zNN name | |
| // if (File.Exists(CurrentName)) | |
| // File.Delete(CurrentName); | |
| // if (File.Exists(_currentTempName)) | |
| // File.Move(_currentTempName, CurrentName); | |
| } | |
| } | |
| } | |
| } | |
| finally | |
| { | |
| base.Dispose(disposing); | |
| } | |
| } | |
| } | |
| } |