#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; /// /// The writer instance used when creating archives /// private IWriter m_writer; /// /// Default constructor, used to read file extension and supported commands /// public FileArchiveZip() { } /// /// 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_stream = new System.IO.FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read); m_archive = ArchiveFactory.Open(m_stream); } else { CompressionInfo cfi = new CompressionInfo(); cfi.Type = DEFAULT_COMPRESSION_METHOD; cfi.DeflateCompressionLevel = DEFAULT_COMPRESSION_LEVEL; string cpmethod; CompressionType tmptype; if (options.TryGetValue(COMPRESSION_METHOD_OPTION, out cpmethod) && Enum.TryParse(cpmethod, true, out tmptype)) cfi.Type = tmptype; string cplvl; int tmplvl; if (options.TryGetValue(COMPRESSION_LEVEL_OPTION, out cplvl) && int.TryParse(cplvl, out tmplvl)) cfi.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)) cfi.DeflateCompressionLevel = (SharpCompress.Compressor.Deflate.CompressionLevel)Math.Max(Math.Min(9, tmplvl), 0); m_isWriting = true; m_stream = new System.IO.FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read); m_writer = WriterFactory.Open(m_stream, ArchiveType.Zip, cfi); //Size of endheader, taken from SharpCompress ZipWriter m_flushBufferSize = 8 + 2 + 2 + 4 + 4 + 2 + 0; } } /// /// Converts a zip file path to the format used by the local filesystem. /// Internally all files are stored in the archive use / as directory separator. /// /// The path to convert in internal format /// The path in filesystem format private static string PathToOsFilesystem(string path) { return Path.DirectorySeparatorChar != '/' ? path.Replace('/', Path.DirectorySeparatorChar) : path; } /// /// Converts a file system path to the internal format. /// Internally all files are stored in the archive use / as directory separator. /// /// The path to convert in filesystem format /// The path in the internal format private static string PathToZipFilesystem(string path) { return Path.DirectorySeparatorChar != '/' ? path.Replace(Path.DirectorySeparatorChar, '/') : path; } #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))) }); } } /// /// Filters entries based on a prefix /// /// An optional prefix /// A list of filenames private IEnumerable FilterEntries(string prefix) { prefix = PathToZipFilesystem(prefix); List results = new List(); foreach(IArchiveEntry e in m_archive.Entries) { if (prefix == null || e.FilePath.StartsWith(prefix, Duplicati.Library.Utility.Utility.ClientFilenameStringComparision)) results.Add(PathToOsFilesystem(e.FilePath)); } return results; } /// /// 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) { return FilterEntries(prefix).ToArray(); } /// /// Returns a list of entries matching the given prefix /// /// The prefix to match /// A list of entries matching the prefix public string[] ListEntries(string prefix) { return FilterEntries(prefix).ToArray(); } /// /// Reads all bytes from a file and returns it as an array /// /// The name of the file to read /// The contents of the file as a byte array public byte[] ReadAllBytes(string file) { using (var ms = new MemoryStream()) using (var s = OpenRead(file)) { Utility.Utility.CopyStream(s, ms); return ms.ToArray(); } } /// /// Reads all lines from a text file, and returns them /// /// The name of the file to read /// The lines read from the file public string[] ReadAllLines(string file) { var lines = new List(); using (var sr = new StreamReader(OpenRead(file), System.Text.Encoding.UTF8, true)) while (!sr.EndOfStream) lines.Add(sr.ReadLine()); return lines.ToArray(); } /// /// 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(); } /// /// Opens a file for writing /// /// The name of the file to write /// A stream that can be updated with file contents public Stream OpenWrite(string file) { return CreateFile(file); } /// /// Writes all bytes from the array into the file /// /// The name of the file /// The data contents of the file public void WriteAllBytes(string file, byte[] data) { using (var s = CreateFile(file)) s.Write(data, 0, data.Length); } /// /// Writes the given lines into the file /// /// The name of the file /// The lines to write public void WriteAllLines(string file, string[] data) { using (var sw = new StreamWriter(CreateFile(file), System.Text.Encoding.UTF8)) foreach (var s in data) sw.WriteLine(s); } /// /// 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"); string path = PathToZipFilesystem(file); foreach(IArchiveEntry e in m_archive.Entries) { if (string.Equals(e.FilePath, path, Duplicati.Library.Utility.Utility.ClientFilenameStringComparision)) return e; } string path2 = PathToOsFilesystem(file); if (path != path2) { path = path2; foreach (IArchiveEntry e in m_archive.Entries) { if (string.Equals(e.FilePath, path, Duplicati.Library.Utility.Utility.ClientFilenameStringComparision)) return e; } } return null; } /// /// Deletes a file from the archive /// /// The name of the file to delete public virtual void DeleteFile(string file) { throw new NotImplementedException(); } /// /// Creates a file in the archive and returns a writeable stream /// /// The name of the file to create /// A writeable stream for the file contents public Stream CreateFile(string file) { return CreateFile(file, DateTime.Now); } /// /// Creates a file in the archive and returns a writeable stream /// /// The name of the file to create /// The time the file was last written /// A writeable stream for the file contents public virtual Stream CreateFile(string file, DateTime lastWrite) { if (!m_isWriting) throw new InvalidOperationException("Cannot write while reading"); m_flushBufferSize += CENTRAL_HEADER_ENTRY_SIZE + System.Text.Encoding.UTF8.GetByteCount(file); 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_archive == null ? m_stream.Length : m_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 } }