В последнее время я
много времени уделяю написанию мотивационных статей, изучению нового языка 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Мб. Исходный код для тестов приведен ниже.
{
//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();
}
}
Упаковка
проводилась с помощью трех способов:
- Использование класса ZipFile с использованием методов Add и AddDirectory.
- Использование класса ZipOutputStream.
- Использование класса FastZip.
Вторым этапом выполнена
проверка всех возможных уровней сжатия, доступных для данной библиотеки. Их у
нее девять. И последним этапом мы распаковали сжатый архив двумя
разными способами:
- С использованием класса ZipFile
- С использованием класса 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