2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2013-02-12 21:43:14 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using Duplicati.Library.Interface ;
using SharpCompress.Common ;
2017-02-27 22:21:01 +01:00
using SharpCompress.Archives ;
using SharpCompress.Writers ;
using SharpCompress.Writers.Zip ;
2017-03-07 23:12:01 +01:00
using SharpCompress.Readers ;
2017-03-09 09:21:03 +01:00
using System.Linq ;
2013-02-12 21:43:14 +00:00
namespace Duplicati.Library.Compression
{
/// <summary>
/// An abstraction of a zip archive as a FileArchive, based on SharpCompress.
2018-03-17 13:56:19 -07:00
/// Please note, duplicati does not require both Read & Write access at the same time so this has not been implemented.
2013-02-12 21:43:14 +00:00
/// </summary>
public class FileArchiveZip : ICompression
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < FileArchiveZip >();
2017-12-25 04:12:19 +07:00
private const string CannotReadWhileWriting = "Cannot read while writing" ;
private const string CannotWriteWhileReading = "Cannot write while reading" ;
2013-02-12 21:43:14 +00:00
/// <summary>
/// The commandline option for toggling the compression level
/// </summary>
private const string COMPRESSION_LEVEL_OPTION = "zip-compression-level" ;
/// <summary>
/// The old commandline option for toggling the compression level
/// </summary>
private const string COMPRESSION_LEVEL_OPTION_ALIAS = "compression-level" ;
/// <summary>
/// The commandline option for toggling the compression method
/// </summary>
2017-12-25 04:12:19 +07:00
private const string COMPRESSION_METHOD_OPTION = "zip-compression-method" ;
/// <summary>
/// The commandline option for toggling the zip64 support
/// </summary>
private const string COMPRESSION_ZIP64_OPTION = "zip-compression-zip64" ;
/// <summary>
/// The default compression level
/// </summary>
private const SharpCompress . Compressors . Deflate . CompressionLevel DEFAULT_COMPRESSION_LEVEL = SharpCompress . Compressors . Deflate . CompressionLevel . Level9 ;
2013-02-12 21:43:14 +00:00
/// <summary>
2017-06-29 14:50:48 +02:00
/// The default compression method
2013-02-12 21:43:14 +00:00
/// </summary>
2017-06-29 14:50:48 +02:00
private const CompressionType DEFAULT_COMPRESSION_METHOD = CompressionType . Deflate ;
2013-02-12 21:43:14 +00:00
/// <summary>
2017-06-29 14:50:48 +02:00
/// The default setting for the zip64 support
2013-02-12 21:43:14 +00:00
/// </summary>
2017-06-29 14:50:48 +02:00
private const bool DEFAULT_ZIP64 = false ;
2013-02-12 21:43:14 +00:00
/// <summary>
/// Taken from SharpCompress ZipCentralDirectorEntry.cs
/// </summary>
private const int CENTRAL_HEADER_ENTRY_SIZE = 8 + 2 + 2 + 4 + 4 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 4 ;
2017-06-29 14:50:48 +02:00
/// <summary>
/// The size of the extended zip64 header
/// </summary>
private const int CENTRAL_HEADER_ENTRY_SIZE_ZIP64_EXTRA = 2 + 2 + 8 + 8 + 8 + 4 ;
2013-02-12 21:43:14 +00:00
/// <summary>
2017-12-25 04:12:19 +07:00
/// This property indicates reading or writing access mode of the file archive.
2013-02-12 21:43:14 +00:00
/// </summary>
2018-05-23 21:18:01 -07:00
readonly ArchiveMode m_mode ;
2013-02-12 21:43:14 +00:00
/// <summary>
/// Gets the number of bytes expected to be written after the stream is disposed
/// </summary>
private long m_flushBufferSize = 0 ;
/// <summary>
/// The ZipArchive instance used when reading archives
/// </summary>
private IArchive m_archive ;
2019-09-09 17:34:58 -04:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// The stream used to either read or write
/// </summary>
private Stream m_stream ;
2017-12-25 04:12:19 +07:00
2013-04-08 22:20:21 +02:00
/// <summary>
/// Lookup table for faster access to entries based on their name.
/// </summary>
2017-03-07 23:12:01 +01:00
private Dictionary < string , IEntry > m_entryDict ;
2017-12-25 04:12:19 +07:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// The writer instance used when creating archives
/// </summary>
private IWriter m_writer ;
2017-03-07 23:12:01 +01:00
/// <summary>
/// A flag indicating if we are using the fail-over reader interface
/// </summary>
public bool m_using_reader = false ;
2013-02-27 20:57:58 +00:00
/// <summary>
2017-02-27 22:21:01 +01:00
/// The compression level applied when the hint does not indicate incompressible
2013-02-27 20:57:58 +00:00
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly SharpCompress . Compressors . Deflate . CompressionLevel m_defaultCompressionLevel ;
2013-02-27 20:57:58 +00:00
2017-12-25 04:12:19 +07:00
/// <summary>
2013-02-27 20:57:58 +00:00
/// The compression level applied when the hint does not indicate incompressible
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly CompressionType m_compressionType ;
2013-02-27 20:57:58 +00:00
2017-06-29 14:50:48 +02:00
/// <summary>
/// A flag indicating if zip64 is in use
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly bool m_usingZip64 ;
2017-06-29 14:50:48 +02:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// Default constructor, used to read file extension and supported commands
/// </summary>
public FileArchiveZip () { }
2017-12-25 04:12:19 +07:00
private IArchive Archive
2013-02-12 21:43:14 +00:00
{
get
{
if ( m_archive == null )
2017-12-25 04:12:19 +07:00
{
m_stream . Position = 0 ;
2013-02-12 21:43:14 +00:00
m_archive = ArchiveFactory . Open ( m_stream );
2017-12-25 04:12:19 +07:00
}
2013-02-12 21:43:14 +00:00
return m_archive ;
}
}
2017-03-07 23:12:01 +01:00
public void SwitchToReader ()
{
if (! m_using_reader )
{
// Close what we have
using ( m_stream )
using ( m_archive )
{ }
m_using_reader = true ;
}
}
public Stream GetStreamFromReader ( IEntry entry )
{
SharpCompress . Readers . Zip . ZipReader rd = null ;
try
{
2017-12-25 04:12:19 +07:00
rd = SharpCompress . Readers . Zip . ZipReader . Open ( m_stream );
2017-03-07 23:12:01 +01:00
while ( rd . MoveToNextEntry ())
if ( entry . Key == rd . Entry . Key )
2017-03-10 11:09:18 +01:00
return new StreamWrapper ( rd . OpenEntryStream (), stream => {
2017-03-07 23:12:01 +01:00
rd . Dispose ();
2017-03-10 11:09:18 +01:00
});
2017-03-07 23:12:01 +01:00
throw new Exception ( string . Format ( "Stream not found: {0}" , entry . Key ));
}
catch
{
if ( rd != null )
rd . Dispose ();
2017-12-25 04:12:19 +07:00
2017-03-07 23:12:01 +01:00
throw ;
}
}
2013-02-12 21:43:14 +00:00
/// <summary>
/// Constructs a new zip instance.
2017-12-25 04:12:19 +07:00
/// Access mode is specified by mode parameter.
/// Note that stream would not be disposed by FileArchiveZip instance so
/// you may reuse it and have to dispose it yourself.
2013-02-12 21:43:14 +00:00
/// </summary>
2017-12-25 04:12:19 +07:00
/// <param name="stream">The stream to read or write depending access mode</param>
2019-09-09 17:34:58 -04:00
/// <param name="mode">The archive access mode</param>
2013-02-12 21:43:14 +00:00
/// <param name="options">The options passed on the commandline</param>
2017-12-25 04:12:19 +07:00
public FileArchiveZip ( Stream stream , ArchiveMode mode , IDictionary < string , string > options )
2013-02-12 21:43:14 +00:00
{
2017-12-25 04:12:19 +07:00
m_stream = stream ;
m_mode = mode ;
if ( mode == ArchiveMode . Write )
2013-02-12 21:43:14 +00:00
{
2017-02-27 22:21:01 +01:00
var compression = new ZipWriterOptions ( CompressionType . Deflate );
compression . CompressionType = DEFAULT_COMPRESSION_METHOD ;
compression . DeflateCompressionLevel = DEFAULT_COMPRESSION_LEVEL ;
2013-02-12 21:43:14 +00:00
2017-06-29 14:50:48 +02:00
m_usingZip64 = compression . UseZip64 =
options . ContainsKey ( COMPRESSION_ZIP64_OPTION )
? Duplicati . Library . Utility . Utility . ParseBoolOption ( options , COMPRESSION_ZIP64_OPTION )
: DEFAULT_ZIP64 ;
2013-02-12 21:43:14 +00:00
string cpmethod ;
CompressionType tmptype ;
if ( options . TryGetValue ( COMPRESSION_METHOD_OPTION , out cpmethod ) && Enum . TryParse < SharpCompress . Common . CompressionType >( cpmethod , true , out tmptype ))
2017-02-27 22:21:01 +01:00
compression . CompressionType = tmptype ;
2013-02-12 21:43:14 +00:00
string cplvl ;
int tmplvl ;
if ( options . TryGetValue ( COMPRESSION_LEVEL_OPTION , out cplvl ) && int . TryParse ( cplvl , out tmplvl ))
2017-02-27 22:21:01 +01:00
compression . DeflateCompressionLevel = ( SharpCompress . Compressors . Deflate . CompressionLevel ) Math . Max ( Math . Min ( 9 , tmplvl ), 0 );
2013-02-12 21:43:14 +00:00
else if ( options . TryGetValue ( COMPRESSION_LEVEL_OPTION_ALIAS , out cplvl ) && int . TryParse ( cplvl , out tmplvl ))
2017-02-27 22:21:01 +01:00
compression . DeflateCompressionLevel = ( SharpCompress . Compressors . Deflate . CompressionLevel ) Math . Max ( Math . Min ( 9 , tmplvl ), 0 );
2013-02-27 20:57:58 +00:00
2017-02-27 22:21:01 +01:00
m_defaultCompressionLevel = compression . DeflateCompressionLevel ;
m_compressionType = compression . CompressionType ;
2013-02-12 21:43:14 +00:00
2017-02-27 22:21:01 +01:00
m_writer = WriterFactory . Open ( m_stream , ArchiveType . Zip , compression );
2013-02-12 21:43:14 +00:00
//Size of endheader, taken from SharpCompress ZipWriter
m_flushBufferSize = 8 + 2 + 2 + 4 + 4 + 2 + 0 ;
}
}
#region IFileArchive Members
/// <summary>
/// Gets the filename extension used by the compression module
/// </summary>
public string FilenameExtension { get { return "zip" ; } }
/// <summary>
/// Gets a friendly name for the compression module
/// </summary>
public string DisplayName { get { return Strings . FileArchiveZip . DisplayName ; } }
/// <summary>
/// Gets a description of the compression module
/// </summary>
public string Description { get { return Strings . FileArchiveZip . Description ; } }
/// <summary>
/// Gets a list of commands supported by the compression module
/// </summary>
public IList < ICommandLineArgument > SupportedCommands
{
get
2017-12-25 04:12:19 +07:00
{
// This is the cross between these two:
// https://github.com/adamhathcock/sharpcompress/blob/master/src/SharpCompress/Common/Zip/ZipCompressionMethod.cs
// https://github.com/adamhathcock/sharpcompress/blob/master/src/SharpCompress/Common/CompressionType.cs
var methods = new []
2017-09-26 14:06:11 +02:00
{
2017-12-25 04:12:19 +07:00
CompressionType . None . ToString (),
CompressionType . Deflate . ToString (),
CompressionType . BZip2 . ToString (),
CompressionType . LZMA . ToString (),
CompressionType . PPMd . ToString (),
2017-09-26 14:06:11 +02:00
};
2013-02-12 21:43:14 +00:00
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
new CommandLineArgument ( COMPRESSION_LEVEL_OPTION , CommandLineArgument . ArgumentType . Enumeration , Strings . FileArchiveZip . CompressionlevelShort , Strings . FileArchiveZip . CompressionlevelLong , DEFAULT_COMPRESSION_LEVEL . ToString (), null , new string [] { "0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" }),
2015-01-20 21:07:24 +01:00
new CommandLineArgument ( COMPRESSION_LEVEL_OPTION_ALIAS , CommandLineArgument . ArgumentType . Enumeration , Strings . FileArchiveZip . CompressionlevelShort , Strings . FileArchiveZip . CompressionlevelLong , DEFAULT_COMPRESSION_LEVEL . ToString (), null , new string [] { "0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" }, Strings . FileArchiveZip . CompressionlevelDeprecated ( COMPRESSION_LEVEL_OPTION )),
2017-09-26 14:06:11 +02:00
new CommandLineArgument ( COMPRESSION_METHOD_OPTION , CommandLineArgument . ArgumentType . Enumeration , Strings . FileArchiveZip . CompressionmethodShort , Strings . FileArchiveZip . CompressionmethodLong ( COMPRESSION_LEVEL_OPTION ), DEFAULT_COMPRESSION_METHOD . ToString (), null , methods ),
2017-12-25 04:12:19 +07:00
new CommandLineArgument ( COMPRESSION_ZIP64_OPTION , CommandLineArgument . ArgumentType . Boolean , Strings . FileArchiveZip . Compressionzip64Short , Strings . FileArchiveZip . Compressionzip64Long , DEFAULT_ZIP64 . ToString ())
});
2013-02-12 21:43:14 +00:00
}
}
/// <summary>
/// Returns a list of files matching the given prefix
/// </summary>
/// <param name="prefix">The prefix to match</param>
/// <returns>A list of files matching the prefix</returns>
public string [] ListFiles ( string prefix )
{
2017-03-09 09:21:03 +01:00
return ListFilesWithSize ( prefix ). Select ( x => x . Key ). ToArray ();
2013-02-12 21:43:14 +00:00
}
/// <summary>
/// Returns a list of files matching the given prefix
/// </summary>
/// <param name="prefix">The prefix to match</param>
/// <returns>A list of files matching the prefix</returns>
2013-03-08 22:24:54 +01:00
public IEnumerable < KeyValuePair < string , long >> ListFilesWithSize ( string prefix )
2013-02-12 21:43:14 +00:00
{
2017-03-09 09:21:03 +01:00
LoadEntryTable ();
var q = m_entryDict . Values . AsEnumerable ();
if (! string . IsNullOrEmpty ( prefix ))
q = q . Where ( x =>
2018-05-16 19:40:51 +02:00
x . Key . StartsWith ( prefix , Duplicati . Library . Utility . Utility . ClientFilenameStringComparison )
2017-03-09 09:21:03 +01:00
||
2018-05-16 19:40:51 +02:00
x . Key . Replace ( '\\' , '/' ). StartsWith ( prefix , Duplicati . Library . Utility . Utility . ClientFilenameStringComparison )
2017-03-09 09:21:03 +01:00
);
return q . Select ( x => new KeyValuePair < string , long >( x . Key , x . Size )). ToArray ();
2013-02-12 21:43:14 +00:00
}
2017-03-09 09:21:03 +01:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// Opens an file for reading
/// </summary>
/// <param name="file">The name of the file to open</param>
/// <returns>A stream with the file contents</returns>
public Stream OpenRead ( string file )
{
2017-12-25 04:12:19 +07:00
if ( m_mode != ArchiveMode . Read )
throw new InvalidOperationException ( CannotReadWhileWriting );
2013-02-12 21:43:14 +00:00
2017-03-07 23:12:01 +01:00
var ze = GetEntry ( file );
if ( ze == null )
return null ;
2019-09-29 20:16:28 -07:00
if ( ze is IArchiveEntry entry )
return entry . OpenEntryStream ();
2017-03-07 23:12:01 +01:00
else if ( ze is SharpCompress . Common . Zip . ZipEntry )
return GetStreamFromReader ( ze );
throw new Exception ( string . Format ( "Unexpected result: {0}" , ze . GetType (). FullName ));
2013-02-12 21:43:14 +00:00
}
/// <summary>
2017-03-09 09:21:03 +01:00
/// Helper method to load the entry table
2013-02-12 21:43:14 +00:00
/// </summary>
2017-03-09 09:21:03 +01:00
private void LoadEntryTable ()
2013-02-12 21:43:14 +00:00
{
2016-09-15 11:39:27 +02:00
if ( m_entryDict == null )
{
2017-03-07 23:12:01 +01:00
try
{
var d = new Dictionary < string , IEntry >( Duplicati . Library . Utility . Utility . ClientFilenameStringComparer );
2017-03-09 09:21:03 +01:00
foreach ( var en in Archive . Entries )
2017-03-07 23:12:01 +01:00
d [ en . Key ] = en ;
m_entryDict = d ;
}
2017-03-10 11:09:18 +01:00
catch ( Exception ex )
2017-03-07 23:12:01 +01:00
{
// If we get an exception here, it may be caused by the Central Header
// being defect, so we switch to the less efficient reader interface
if ( m_using_reader )
throw ;
2018-03-12 14:07:11 +01:00
Logging . Log . WriteWarningMessage ( LOGTAG , "BrokenCentralHeaderFallback" , ex , "Zip archive appears to have a broken Central Record Header, switching to stream mode" );
2017-03-07 23:12:01 +01:00
SwitchToReader ();
2017-03-09 09:21:03 +01:00
var d = new Dictionary < string , IEntry >( Duplicati . Library . Utility . Utility . ClientFilenameStringComparer );
2017-03-10 11:09:18 +01:00
try
{
2017-12-25 04:12:19 +07:00
using ( var rd = SharpCompress . Readers . Zip . ZipReader . Open ( m_stream , new ReaderOptions () { LookForHeader = false }))
2017-03-10 11:09:18 +01:00
while ( rd . MoveToNextEntry ())
2017-03-12 12:36:24 +01:00
{
2017-03-10 11:09:18 +01:00
d [ rd . Entry . Key ] = rd . Entry ;
2017-03-12 12:36:24 +01:00
// Some streams require this
// to correctly find the next entry
using ( rd . OpenEntryStream ())
{ }
}
2017-03-10 11:09:18 +01:00
}
catch ( Exception ex2 )
{
// If we have zero files, or just a manifest, don't bother
if ( d . Count < 2 )
throw ;
2017-12-25 04:12:19 +07:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteWarningMessage ( LOGTAG , "BrokenCentralHeader" , ex2 , "Zip archive appears to have broken records, returning the {0} records that could be recovered" , d . Count );
2017-03-10 11:09:18 +01:00
}
2017-12-25 04:12:19 +07:00
2017-03-09 09:21:03 +01:00
m_entryDict = d ;
2017-03-07 23:12:01 +01:00
}
2016-09-15 11:39:27 +02:00
}
2017-03-09 09:21:03 +01:00
}
/// <summary>
/// Internal function that returns a ZipEntry for a filename, or null if no such file exists
/// </summary>
/// <param name="file">The name of the file to find</param>
/// <returns>The ZipEntry for the file or null if no such file was found</returns>
private IEntry GetEntry ( string file )
{
2017-12-25 04:12:19 +07:00
if ( m_mode != ArchiveMode . Read )
throw new InvalidOperationException ( CannotReadWhileWriting );
2017-03-09 09:21:03 +01:00
LoadEntryTable ();
2016-09-15 11:39:27 +02:00
2017-03-07 23:12:01 +01:00
IEntry e ;
2016-09-15 11:39:27 +02:00
if ( m_entryDict . TryGetValue ( file , out e ))
return e ;
if ( m_entryDict . TryGetValue ( file . Replace ( '/' , '\\' ), out e ))
return e ;
return null ;
2013-02-12 21:43:14 +00:00
}
2019-09-09 17:34:58 -04:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// Creates a file in the archive and returns a writeable stream
/// </summary>
/// <param name="file">The name of the file to create</param>
2013-02-27 20:57:58 +00:00
/// <param name="hint">A hint to the compressor as to how compressible the file data is</param>
2013-02-12 21:43:14 +00:00
/// <param name="lastWrite">The time the file was last written</param>
/// <returns>A writeable stream for the file contents</returns>
2013-02-27 20:57:58 +00:00
public virtual Stream CreateFile ( string file , CompressionHint hint , DateTime lastWrite )
2013-02-12 21:43:14 +00:00
{
2017-12-25 04:12:19 +07:00
if ( m_mode != ArchiveMode . Write )
throw new InvalidOperationException ( CannotWriteWhileReading );
2013-02-12 21:43:14 +00:00
m_flushBufferSize += CENTRAL_HEADER_ENTRY_SIZE + System . Text . Encoding . UTF8 . GetByteCount ( file );
2017-06-29 14:50:48 +02:00
if ( m_usingZip64 )
m_flushBufferSize += CENTRAL_HEADER_ENTRY_SIZE_ZIP64_EXTRA ;
2017-12-25 04:12:19 +07:00
2017-02-27 22:21:01 +01:00
return (( ZipWriter ) m_writer ). WriteToStream ( file , new ZipWriterEntryOptions ()
{
DeflateCompressionLevel = hint == CompressionHint . Noncompressible ? SharpCompress . Compressors . Deflate . CompressionLevel . None : m_defaultCompressionLevel ,
ModificationDateTime = lastWrite ,
CompressionType = m_compressionType
});
2015-03-05 21:06:56 +01:00
}
2019-09-09 17:34:58 -04:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// Returns a value that indicates if the file exists
/// </summary>
/// <param name="file">The name of the file to test existence for</param>
/// <returns>True if the file exists, false otherwise</returns>
public bool FileExists ( string file )
{
2017-12-25 04:12:19 +07:00
if ( m_mode != ArchiveMode . Read )
throw new InvalidOperationException ( CannotReadWhileWriting );
2013-02-12 21:43:14 +00:00
return GetEntry ( file ) != null ;
}
/// <summary>
/// Gets the current size of the archive
/// </summary>
public long Size
{
get
{
2017-12-25 04:12:19 +07:00
return m_mode == ArchiveMode . Write ? m_stream . Length : Archive . TotalSize ;
2013-02-12 21:43:14 +00:00
}
}
/// <summary>
/// The size of the current unflushed buffer
/// </summary>
public long FlushBufferSize
2017-12-25 04:12:19 +07:00
{
2013-02-12 21:43:14 +00:00
get
{
return m_flushBufferSize ;
2017-12-25 04:12:19 +07:00
}
2013-02-12 21:43:14 +00:00
}
/// <summary>
/// Gets the last write time for a file
/// </summary>
/// <param name="file">The name of the file to query</param>
/// <returns>The last write time for the file</returns>
public DateTime GetLastWriteTime ( string file )
{
IEntry entry = GetEntry ( file );
if ( entry != null )
{
if ( entry . LastModifiedTime . HasValue )
return entry . LastModifiedTime . Value ;
else
return DateTime . MinValue ;
}
2015-01-20 21:07:24 +01:00
throw new FileNotFoundException ( Strings . FileArchiveZip . FileNotFoundError ( file ));
2013-02-12 21:43:14 +00:00
}
#endregion
#region IDisposable Members
public void Dispose ()
{
if ( m_archive != null )
m_archive . Dispose ();
m_archive = null ;
if ( m_writer != null )
m_writer . Dispose ();
m_writer = null ;
m_stream = null ;
}
#endregion
}
}