#region Disclaimer / License // Copyright (C) 2011, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion using System; using System.Collections.Generic; using System.IO; using Duplicati.Library.Interface; using System.Linq; using SharpCompress.Common; using SharpCompress.Archive; using SharpCompress.Archive.Zip; using SharpCompress.Writer; using SharpCompress.Writer.Zip; namespace Duplicati.Library.Compression { /// /// An abstraction of a zip archive as a FileArchive, based on SharpCompress. /// Please note, duplicati does not require both Read & Write access at the same time so this has not been implemented /// public class FileArchiveZip : ICompression { /// /// The commandline option for toggling the compression level /// private const string COMPRESSION_LEVEL_OPTION = "zip-compression-level"; /// /// The old commandline option for toggling the compression level /// private const string COMPRESSION_LEVEL_OPTION_ALIAS = "compression-level"; /// /// The commandline option for toggling the compression method /// private const string COMPRESSION_METHOD_OPTION = "zip-compression-method"; /// /// The default compression level /// private const SharpCompress.Compressor.Deflate.CompressionLevel DEFAULT_COMPRESSION_LEVEL = SharpCompress.Compressor.Deflate.CompressionLevel.Level9; /// /// The default compression method /// private const CompressionType DEFAULT_COMPRESSION_METHOD = CompressionType.Deflate; /// /// Taken from SharpCompress ZipCentralDirectorEntry.cs /// private const int CENTRAL_HEADER_ENTRY_SIZE = 8 + 2 + 2 + 4 + 4 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 4; /// /// This property indicates that this current instance should write to a file /// private bool m_isWriting; /// /// Gets the number of bytes expected to be written after the stream is disposed /// private long m_flushBufferSize = 0; /// /// The ZipArchive instance used when reading archives /// private IArchive m_archive; /// /// The stream used to either read or write /// private Stream m_stream; /// /// Lookup table for faster access to entries based on their name. /// private Dictionary m_entryDict; /// /// The writer instance used when creating archives /// private IWriter m_writer; /// /// Instance of the CompresisonInfo class, used to hack in compression hints /// private CompressionInfo m_compressionInfo; /// /// The compression level applied when the hint does not indicate incompressible /// private SharpCompress.Compressor.Deflate.CompressionLevel m_defaultCompressionLevel; /// /// The name of the file being read /// private string m_filename; /// /// Default constructor, used to read file extension and supported commands /// public FileArchiveZip() { } public IArchive Archive { get { if (m_stream == null) m_stream = new System.IO.FileStream(m_filename, FileMode.Open, FileAccess.Read, FileShare.Read); if (m_archive == null) m_archive = ArchiveFactory.Open(m_stream); return m_archive; } } /// /// Constructs a new zip instance. /// If the file exists and has a non-zero length we read it, /// otherwise we create a new archive. /// /// The name of the file to read or write /// The options passed on the commandline public FileArchiveZip(string filename, Dictionary options) { if (string.IsNullOrEmpty(filename) && filename.Trim().Length == 0) throw new ArgumentException("filename"); if (File.Exists(filename) && new FileInfo(filename).Length > 0) { m_isWriting = false; m_filename = filename; } else { m_compressionInfo = new CompressionInfo(); m_compressionInfo.Type = DEFAULT_COMPRESSION_METHOD; m_compressionInfo.DeflateCompressionLevel = DEFAULT_COMPRESSION_LEVEL; string cpmethod; CompressionType tmptype; if (options.TryGetValue(COMPRESSION_METHOD_OPTION, out cpmethod) && Enum.TryParse(cpmethod, true, out tmptype)) m_compressionInfo.Type = tmptype; string cplvl; int tmplvl; if (options.TryGetValue(COMPRESSION_LEVEL_OPTION, out cplvl) && int.TryParse(cplvl, out tmplvl)) m_compressionInfo.DeflateCompressionLevel = (SharpCompress.Compressor.Deflate.CompressionLevel)Math.Max(Math.Min(9, tmplvl), 0); else if (options.TryGetValue(COMPRESSION_LEVEL_OPTION_ALIAS, out cplvl) && int.TryParse(cplvl, out tmplvl)) m_compressionInfo.DeflateCompressionLevel = (SharpCompress.Compressor.Deflate.CompressionLevel)Math.Max(Math.Min(9, tmplvl), 0); m_defaultCompressionLevel = m_compressionInfo.DeflateCompressionLevel; m_isWriting = true; m_stream = new System.IO.FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read); m_writer = WriterFactory.Open(m_stream, ArchiveType.Zip, m_compressionInfo); //Size of endheader, taken from SharpCompress ZipWriter m_flushBufferSize = 8 + 2 + 2 + 4 + 4 + 2 + 0; } } #region IFileArchive Members /// /// Gets the filename extension used by the compression module /// public string FilenameExtension { get { return "zip"; } } /// /// Gets a friendly name for the compression module /// public string DisplayName { get { return Strings.FileArchiveZip.DisplayName; } } /// /// Gets a description of the compression module /// public string Description { get { return Strings.FileArchiveZip.Description; } } /// /// Gets a list of commands supported by the compression module /// public IList SupportedCommands { get { return new List(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"}), 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"}, string.Format(Strings.FileArchiveZip.CompressionlevelDeprecated, COMPRESSION_LEVEL_OPTION)), new CommandLineArgument(COMPRESSION_METHOD_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.FileArchiveZip.CompressionmethodShort, string.Format(Strings.FileArchiveZip.CompressionmethodLong, COMPRESSION_LEVEL_OPTION), DEFAULT_COMPRESSION_METHOD.ToString(), null, Enum.GetNames(typeof(CompressionType))) }); } } /// /// Returns a list of files matching the given prefix /// /// The prefix to match /// A list of files matching the prefix public string[] ListFiles(string prefix) { List results = new List(); foreach (IArchiveEntry e in Archive.Entries) { if (prefix == null) { results.Add(e.FilePath); } else { if (e.FilePath.StartsWith(prefix, Duplicati.Library.Utility.Utility.ClientFilenameStringComparision)) results.Add(e.FilePath); //Some old archives may have been created with windows style paths else if (e.FilePath.Replace('\\', '/').StartsWith(prefix, Duplicati.Library.Utility.Utility.ClientFilenameStringComparision)) results.Add(e.FilePath); } } return results.ToArray(); } /// /// Returns a list of files matching the given prefix /// /// The prefix to match /// A list of files matching the prefix public IEnumerable> ListFilesWithSize(string prefix) { List> results = new List>(); foreach (IArchiveEntry e in Archive.Entries) { if (prefix == null) { results.Add(new KeyValuePair(e.FilePath, e.Size)); } else { if (e.FilePath.StartsWith(prefix, Duplicati.Library.Utility.Utility.ClientFilenameStringComparision)) results.Add(new KeyValuePair(e.FilePath, e.Size)); //Some old archives may have been created with windows style paths else if (e.FilePath.Replace('\\', '/').StartsWith(prefix, Duplicati.Library.Utility.Utility.ClientFilenameStringComparision)) results.Add(new KeyValuePair(e.FilePath, e.Size)); } } return results; } /// /// Opens an file for reading /// /// The name of the file to open /// A stream with the file contents public Stream OpenRead(string file) { if (m_isWriting) throw new InvalidOperationException("Cannot read while writing"); IArchiveEntry ze = GetEntry(file); return ze == null ? null : ze.OpenEntryStream(); } /// /// Internal function that returns a ZipEntry for a filename, or null if no such file exists /// /// The name of the file to find /// The ZipEntry for the file or null if no such file was found private IArchiveEntry GetEntry(string file) { if (m_isWriting) throw new InvalidOperationException("Cannot read while writing"); if (m_entryDict == null) { m_entryDict = new Dictionary(Duplicati.Library.Utility.Utility.ClientFilenameStringComparer); foreach(IArchiveEntry en in Archive.Entries) m_entryDict[en.FilePath] = en; } IArchiveEntry e; if (m_entryDict.TryGetValue(file, out e)) return e; if (m_entryDict.TryGetValue(file.Replace('/', '\\'), out e)) return e; return null; } /// /// Creates a file in the archive and returns a writeable stream /// /// The name of the file to create /// A hint to the compressor as to how compressible the file data is /// The time the file was last written /// A writeable stream for the file contents public virtual Stream CreateFile(string file, CompressionHint hint, DateTime lastWrite) { if (!m_isWriting) throw new InvalidOperationException("Cannot write while reading"); m_flushBufferSize += CENTRAL_HEADER_ENTRY_SIZE + System.Text.Encoding.UTF8.GetByteCount(file); m_compressionInfo.DeflateCompressionLevel = hint == CompressionHint.Noncompressible ? SharpCompress.Compressor.Deflate.CompressionLevel.None : m_defaultCompressionLevel; return ((ZipWriter)m_writer).WriteToStream(file, lastWrite, null); } /// /// Returns a value that indicates if the file exists /// /// The name of the file to test existence for /// True if the file exists, false otherwise public bool FileExists(string file) { if (m_isWriting) throw new InvalidOperationException("Cannot read while writing"); return GetEntry(file) != null; } /// /// Gets the current size of the archive /// public long Size { get { return m_isWriting ? m_stream.Length : Archive.TotalSize; } } /// /// The size of the current unflushed buffer /// public long FlushBufferSize { get { return m_flushBufferSize; } } /// /// Gets the last write time for a file /// /// The name of the file to query /// The last write time for the file public DateTime GetLastWriteTime(string file) { IEntry entry = GetEntry(file); if (entry != null) { if (entry.LastModifiedTime.HasValue) return entry.LastModifiedTime.Value; else return DateTime.MinValue; } throw new FileNotFoundException(string.Format(Strings.FileArchiveZip.FileNotFoundError, file)); } #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; if (m_stream != null) m_stream.Dispose(); m_stream = null; } #endregion } }