Wednesday, February 19, 2014

Используем SharpZipLib для работы с ZIP-архивами

В последнее время я много времени уделяю написанию мотивационных статей, изучению нового языка F#, а также к подготовке к сертификации. Это компенсировало время, уделенное написанию статей по C#. Закрадываются сомнения: странный программист, который все время твердит о мотивации и ничего для этого не делает. Поэтому сегодня поговорим о том аспекте в разработке на языке C#, с которым приходится сталкиваться разработчикам, а именно: о работе с архивами, а также с библиотеками, позволяющими работать с архивами. Речь пойдет о библиотеке SharpZipLib, о популярности которой говорит количество скачиваний, которых на момент написания статьи было около 170 тысяч.
Для удобства работы с этой библиотекой мы создадим класс-обертку, которая будет выполнять за нас всю "грязную работу", а мы для этого ей передадим лишь несколько параметров. Часть методов для тестирования этой библиотеки взяты с github. Приступим к реализации. Класс-обертка, которую я назвал SharpZipLibHelper, имеет следующий вид:
public class SharpZipLibHelper
    {
        #region [ public methods ]
        public void CompressionSharpZipLibLibrary(string fileName, string sourceDirectory)
        {
            using (var zip = ZipFile.Create(fileName))
            {
                zip.BeginUpdate();
                GetFilesToZip(new FileSystemInfo[]
                                  {
                                        new DirectoryInfo(sourceDirectory)
                                  }, zip);
                zip.CommitUpdate();
                zip.Close();
            }
        }

        public void FastZipCompression(string fileName, string sourceDirectory)
        {
            var zip = new FastZip {CreateEmptyDirectories = true};
            zip.CreateZip(fileName, sourceDirectory, true, "");
        }

        public void ExtractZipFile(string archiveFilenameIn, string password, string outFolder)
        {
            using(var fs = File.OpenRead(archiveFilenameIn))
            using (var zf = new ZipFile(fs))
            {
                if (!String.IsNullOrEmpty(password))
                {
                    zf.Password = password;     // AES encrypted entries are handled automatically
                }
                foreach (ZipEntry zipEntry in zf)
                {
                    if (!zipEntry.IsFile)
                    {
                        continue;           // Ignore directories
                    }
                    var entryFileName = zipEntry.Name;
                    // to remove the folder from the entry:- entryFileName = Path.GetFileName(entryFileName);
                    // Optionally match entrynames against a selection list here to skip as desired.
                    // The unpacked length is available in the zipEntry.Size property.

                    var buffer = new byte[4096];     // 4K is optimum
                    var zipStream = zf.GetInputStream(zipEntry);

                    // Manipulate the output filename here as desired.
                    var fullZipToPath = Path.Combine(outFolder, entryFileName);
                    var directoryName = Path.GetDirectoryName(fullZipToPath);
                    if (!string.IsNullOrEmpty(directoryName))
                        Directory.CreateDirectory(directoryName);

                    // Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size
                    // of the file, but does not waste memory.
                    // The "using" will close the stream even if an exception occurs.
                    using (var streamWriter = File.Create(fullZipToPath))
                    {
                        StreamUtils.Copy(zipStream, streamWriter, buffer);
                    }
                }

                zf.IsStreamOwner = true; // Makes close also shut the underlying stream
                zf.Close(); // Ensure we release resources
            }
        }

        public MemoryStream CreateToMemoryStream(MemoryStream memStreamIn, string zipEntryName)
        {
            var outputMemStream = new MemoryStream();
            var zipStream = new ZipOutputStream(outputMemStream);

            zipStream.SetLevel(3); //0-9, 9 being the highest level of compression

            var newEntry = new ZipEntry(zipEntryName) {DateTime = DateTime.Now};

            zipStream.PutNextEntry(newEntry);

            StreamUtils.Copy(memStreamIn, zipStream, new byte[4096]);
            zipStream.CloseEntry();

            zipStream.IsStreamOwner = false;    // False stops the Close also Closing the underlying stream.
            zipStream.Close();          // Must finish the ZipOutputStream before using outputMemStream.

            outputMemStream.Position = 0;
            return outputMemStream;

            // Alternative outputs:
            // ToArray is the cleaner and easiest to use correctly with the penalty of duplicating allocated memory.
            //byte[] byteArrayOut = outputMemStream.ToArray();

            // GetBuffer returns a raw buffer raw and so you need to account for the true length yourself.
            //byte[] byteArrayOut = outputMemStream.GetBuffer();
            //long len = outputMemStream.Length;
        }

        public void UnzipFromStream(Stream zipStream, string outFolder)
        {
            using (var zipInputStream = new ZipInputStream(zipStream))
            {
                var zipEntry = zipInputStream.GetNextEntry();
                while (zipEntry != null)
                {
                    var entryFileName = zipEntry.Name;
                    // to remove the folder from the entry:- entryFileName = Path.GetFileName(entryFileName);
                    // Optionally match entrynames against a selection list here to skip as desired.
                    // The unpacked length is available in the zipEntry.Size property.

                    var buffer = new byte[4096]; // 4K is optimum

                    // Manipulate the output filename here as desired.
                    var fullZipToPath = Path.Combine(outFolder, entryFileName).Replace("/","\\");
                    string directoryName = Path.GetDirectoryName(fullZipToPath);
                    if (!string.IsNullOrEmpty(directoryName))
                        Directory.CreateDirectory(directoryName);

                    if (zipEntry.IsFile)
                    {

                        // Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size
                        // of the file, but does not waste memory.
                        // The "using" will close the stream even if an exception occurs.
                        using (var streamWriter = File.Create(fullZipToPath))
                        {
                            StreamUtils.Copy(zipInputStream, streamWriter, buffer);
                        }
                    }
                    zipEntry = zipInputStream.GetNextEntry();
                }
            }
        }
       
        public void CompressionSharpZipLibOutputStream(string fileName, string sourceDirectory,
            int level = 9)
        {
            using (var s = new ZipOutputStream(File.Create(fileName)))
            {
                s.SetLevel(level); // 0-9, 9 being the highest compression

                var buffer = new byte[4096];

                var files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);
                foreach (string file in files)
                {

                    var entry = new ZipEntry(Path.GetFileName(file)) {DateTime = DateTime.Now};

                    s.PutNextEntry(entry);

                    using (var fs = File.OpenRead(file))
                    {
                        int sourceBytes;
                        do
                        {
                            sourceBytes = fs.Read(buffer, 0,
                            buffer.Length);

                            s.Write(buffer, 0, sourceBytes);

                        } while (sourceBytes > 0);
                    }
                }
                s.Finish();
                s.Close();
            }

        }
        #endregion

        #region [ private methods ]
        /// <summary>
        /// Iterate thru all the filesysteminfo objects and add it to our zip file
        /// </summary>
        /// <param name="fileSystemInfosToZip">a collection of files/directores</param>
        /// <param name="z">our existing ZipFile object</param>
        private void GetFilesToZip(IEnumerable<FileSystemInfo> fileSystemInfosToZip, ZipFile z)
        {
            //check whether the objects are null
            if (fileSystemInfosToZip != null && z != null)
            {
                //iterate thru all the filesystem info objects
                foreach (var fi in fileSystemInfosToZip)
                {
                    //check if it is a directory
                    var info = fi as DirectoryInfo;
                    if (info != null)
                    {
                        var di = info;
                        z.AddDirectory(di.FullName);
                        GetFilesToZip(di.GetFileSystemInfos(), z);
                    }
                    else
                    {
                        z.Add(fi.FullName);
                    }
                }
            }
        }
        #endregion
    }
Данный врапер Вы можете использовать для своих целей "безвозмездно, то есть даром". Для тестирования были взяты две папки размером 38087531 байт, что равно 38Мб, и папка размером 463441844 байт, или 460Мб. Исходный код для тестов приведен ниже.
static void Main(string[] args)
{
    //Compress directory
    var directory = CreateEmptyDirectory();
    var zipHelper = new SharpZipLibHelper();
    var resultFile = Path.Combine(Directory.GetCurrentDirectory(), "result.txt");
    if(File.Exists(resultFile))
        File.Delete(resultFile);

    var sourceDirecory = @"D:\books\Introduction to F#";
    var size = GetDirectorySize(sourceDirecory);
    File.AppendAllText(resultFile, string.Format("Directory size {0} bytes {1}", size, Environment.NewLine));

    var sharpZipLibPacking1 = "SharpZipLibPacking1.zip";
    var sharpZipLibPacking2 = "SharpZipLibPacking2.zip";

    string fileName = Path.Combine(directory, sharpZipLibPacking1);
    var result1 = Profiler.MeasureAction(() => zipHelper.CompressionSharpZipLibLibrary(fileName,
                                                                                        sourceDirecory));
    string fileName2 = Path.Combine(directory, sharpZipLibPacking2);
    var result2 = Profiler.MeasureAction(() => zipHelper.CompressionSharpZipLibOutputStream(fileName2,
                                                                                    sourceDirecory,
                                                                                    5));
    File.AppendAllText(resultFile, string.Format("Zip directory with ZipFile {0} msec, Size: {1}{2}",
        result1,
        new FileInfo(fileName).Length,
        Environment.NewLine));
    File.AppendAllText(resultFile, string.Format("Zip directory with ZipOutputStream {0} msec, Size: {1}{2}",
        result2,
        new FileInfo(fileName2).Length,
        Environment.NewLine));
    File.AppendAllText(resultFile, Environment.NewLine);
    //Compress zip level
    var fileNames = new List<string>();
    for (var i = 1; i <= 9; i++)
    {
        fileNames.Add(Path.Combine(directory, string.Format("SharpZipLibTest{0}.zip", i)));
    }
    for (var i = 1; i <= fileNames.Count; i++)
    {
        var temp = fileNames[i - 1];
        var tempCounter = i;
        var res = Profiler.MeasureAction(() => zipHelper.CompressionSharpZipLibOutputStream(temp,
            sourceDirecory,
            tempCounter));
        File.AppendAllText(resultFile, string.Format("Zip directory with ZipOutputStream {0} msec, Size: {1}{2}",
            res,
            new FileInfo(temp).Length,
            Environment.NewLine));
    }

    //Unzip file
    var outputDirectory = Path.Combine(directory, "12345");
    if (!Directory.Exists(outputDirectory))
        Directory.CreateDirectory(outputDirectory);
    var outputResult = Profiler.MeasureAction(() => zipHelper.ExtractZipFile(fileName, string.Empty, outputDirectory));
    File.AppendAllText(resultFile, string.Format("Unzip file to directory {0} msec, FileName: {1}{2}",
        outputResult,
        fileName,
        Environment.NewLine));
    Directory.Delete(outputDirectory, true);
    if (!Directory.Exists(outputDirectory))
        Directory.CreateDirectory(outputDirectory);
    using (var stream = new FileStream(fileName, FileMode.Open))
    {
        var temp = stream;
        var outputResult1 = Profiler.MeasureAction(() => zipHelper.UnzipFromStream(temp, outputDirectory));
        File.AppendAllText(resultFile, string.Format("Unzip file from stream  {0} msec, FileName: {1}{2}",
            outputResult1,
            fileName,
            Environment.NewLine));
    }

    DeleteTempDirectory();
}

private static void DeleteTempDirectory()
{
    var directory = Path.Combine(Directory.GetCurrentDirectory(), "TestArchives");
    if (Directory.Exists(directory))
        Directory.Delete(directory, true);
}

private static string CreateEmptyDirectory()
{
    var directory = Path.Combine(Directory.GetCurrentDirectory(), "TestArchives");
    if (!Directory.Exists(directory))
        Directory.CreateDirectory(directory);
    else
    {
        var files = Directory.GetFiles(directory);
        foreach (var file in files)
        {
            File.Delete(file);
        }
    }
    return directory;
}

static long GetDirectorySize(string path)
{
    var a = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);

    return a.Select(name => new FileInfo(name)).Select(info => info.Length).Sum();
}

public static void CompressionSevenZipLibrary(string fileName,
    string sourceDirectory, OutArchiveFormat outArchiveFormat = OutArchiveFormat.Zip)
{
    var tmp = new SevenZipCompressor { ArchiveFormat = outArchiveFormat };
    tmp.CompressDirectory(sourceDirectory, fileName, true);
}

public static void CompressionIonicZipLibrary(string fileName,
    string sourceDirectory,
    Ionic.Zlib.CompressionLevel compressionLevel = Ionic.Zlib.CompressionLevel.Default)
{
    using (var zipFile = new Ionic.Zip.ZipFile(fileName))
    {
        zipFile.CompressionLevel = compressionLevel;
        zipFile.AddDirectory(sourceDirectory, "\\");
        zipFile.Save();
    }
}
Упаковка проводилась с помощью трех способов:
  1. Использование класса ZipFile с использованием методов Add и AddDirectory.
  2. Использование класса ZipOutputStream.
  3. Использование класса FastZip.
Вторым этапом выполнена проверка всех возможных уровней сжатия, доступных для данной библиотеки. Их у нее девять. И последним этапом мы распаковали сжатый архив двумя разными способами:
  1. С использованием класса ZipFile
  2. С использованием класса ZipInputStream
Посмотрим, что у нас из этого получилось.
Примечание: все тесты проводились в релиз-режиме, после трех-четырех тестовых запуска. Единственное, что я поленился сделать, – это выставить явно выполнение на одном процессоре. Имеем данные, взятые в релиз-режиме с использованием 4-ядерного процессора и 4-Гб оперативной памяти на ОС Windows 7.
Поскольку в исходном коде для теста я забыл добавить упаковку с помощью FastZip, то решил привести ее ниже отдельно.
var directory = CreateEmptyDirectory();
var zipHelper = new SharpZipLibHelper();
var resultFile = Path.Combine(Directory.GetCurrentDirectory(), "result.txt");
if(File.Exists(resultFile))
    File.Delete(resultFile);

var sourceDirecory = @"D:\CSharp\IocContainersDemo";
var size = GetDirectorySize(sourceDirecory);
File.AppendAllText(resultFile, string.Format("Directory size {0} bytes {1}", size, Environment.NewLine));

var sharpZipLibPacking1 = "SharpZipLibPacking1.zip";
var sharpZipLibPacking2 = "SharpZipLibPacking2.zip";

string fileName = Path.Combine(directory, sharpZipLibPacking1);
var result1 = Profiler.MeasureAction(() => zipHelper.FastZipCompression(fileName,
                                                                        sourceDirecory));

File.AppendAllText(resultFile, string.Format("FastZipCompression {0} msec, Size: {1}{2}",
    result1,
    new FileInfo(fileName).Length,
    Environment.NewLine));

var sourceDirecory1 = @"D:\books\Introduction to F#";
string fileName1 = Path.Combine(directory, sharpZipLibPacking2);
var result2 = Profiler.MeasureAction(() => zipHelper.FastZipCompression(fileName1,
                                                                        sourceDirecory1));

File.AppendAllText(resultFile, string.Format("FastZipCompression {0} msec, Size: {1}{2}",
    result2,
    new FileInfo(fileName1).Length,
    Environment.NewLine));
Также добавлю класс, используемый для измерения производительности, а затем перейду к показателям.
public static class Profiler
{
    public static double MeasureAction(Action action)
    {
        var st = new Stopwatch();
           
        st.Start();

        action();

        st.Stop();

        return st.Elapsed.TotalMilliseconds;
    }
}
Как видим, класс очень простой и, по сути, является оберткой над классом Stopwatch.

Ниже приведен результат второго теста. Тест создан не искусственно, а за основу взяты реальные данные. В приведенном выше тесте взят файл проекта с библиотеками. Тест, который приведен ниже, архивирует видеоуроки по языку F#.

Итоги

Анализируемая библиотека достаточно популярна и часто используется в коммерческих приложениях. SharpZipLib поддерживает работу с паролями, работу с потоками. Правда, есть и негативные моменты, например, мне эта библиотека не подошла из-за того, что она не поддерживает возможность дописывать в уже существующий архив. IonicZip с этим заданием справляется намного проще. Удобна эта библиотека также тем, что позволяет писать данные сразу в архив, не сохраняя для этого промежуточных результатов. В принципе, если Вы решите использовать данное решение для своих целей, то намного упростите свою работу, так как библиотека небольшая и занимает всего 200 КБ, что не может не радовать. Такой размер с таким функционалом – это огромный плюс в пользу библиотеки. Надеюсь, если Вы не знакомы с данной библиотекой, то подумаете об ее использовании в реальных проектах. 

No comments:

Post a Comment