Создание каталогов в ZipArchive C# .Net 4.5

ZipArchive — это коллекция ZipArchiveEntries, и добавление/удаление «Entries» работает хорошо. Но, похоже, нет понятия каталогов/вложенных «Архивов». Теоретически класс отделен от файловой системы, так что вы можете полностью создать архив в потоке памяти. Но если вы хотите добавить структуру каталогов в архив, вы должны поставить перед именем записи путь.

Вопрос. Как бы вы расширили ZipArchive, чтобы создать лучший интерфейс для создания каталогов и управления ими?

Например, текущий метод добавления файла в каталог заключается в создании записи с путем к каталогу:

var entry = _archive.CreateEntry("directory/entryname");

тогда как что-то в этом роде кажется мне более приятным:

var directory = _archive.CreateDirectoryEntry("directory");
var entry = _directory.CreateEntry("entryname");

person Meirion Hughes    schedule 28.02.2013    source источник
comment
Вы имеете в виду структуру папок внутри одного почтового индекса или иерархию почтовых индексов?   -  person Davin Tryon    schedule 28.02.2013
comment
Структура папок внутри одного почтового индекса.   -  person Meirion Hughes    schedule 28.02.2013


Ответы (10)


Вы можете использовать что-то вроде следующего, другими словами, создать структуру каталогов вручную:

using (var fs = new FileStream("1.zip", FileMode.Create))
using (var zip = new ZipArchive(fs, ZipArchiveMode.Create))
{
    zip.CreateEntry("12/3/"); // just end with "/"
}
person Aimeast    schedule 25.09.2013

Я знаю, что опаздываю на вечеринку (25.07.2018),

это работает безупречно для меня, даже с рекурсивными каталогами.

Во-первых, не забудьте установить пакет NuGet:

Install-Package System.IO.Compression

И затем файл расширения для ZipArchive:

 public static class ZipArchiveExtension {

     public static void CreateEntryFromAny(this ZipArchive archive, string sourceName, string entryName = "") {
         var fileName = Path.GetFileName(sourceName);
         if (File.GetAttributes(sourceName).HasFlag(FileAttributes.Directory)) {
             archive.CreateEntryFromDirectory(sourceName, Path.Combine(entryName, fileName));
         } else {
             archive.CreateEntryFromFile(sourceName, Path.Combine(entryName, fileName), CompressionLevel.Fastest);
         }
     }

     public static void CreateEntryFromDirectory(this ZipArchive archive, string sourceDirName, string entryName = "") {
         string[] files = Directory.GetFiles(sourceDirName).Concat(Directory.GetDirectories(sourceDirName)).ToArray();
         archive.CreateEntry(Path.Combine(entryName, Path.GetFileName(sourceDirName)));
         foreach (var file in files) {
             archive.CreateEntryFromAny(file, entryName);
         }
     }
 }

И тогда вы можете запаковать что угодно, будь то файл или каталог:

using (var memoryStream = new MemoryStream()) {
    using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) {
        archive.CreateEntryFromAny(sourcePath);
    }
}
person Val    schedule 25.07.2018
comment
Спасибо, что поделились этим, сэкономили мне время - person agrath; 29.10.2018
comment
Это спасительный ответ! - person gvdm; 09.05.2019
comment
Я проголосовал за алгоритм, несмотря на то, что он не компилируется. foreach определяет file и вместо этого использует fileName. - person kpull1; 21.05.2019
comment
@kpull1 Спасибо! это была ошибка, вызванная предложением редактирования. Фиксированный - person Val; 22.05.2019
comment
Это сработало для меня, когда я удалил строку: archive.CreateEntry(Path.Combine(entryName, Path.GetFileName(sourceDirName))); //Эта строка, кажется, добавляет дополнительный пустой файл с тем же именем, что и имя папки. - person Mikhael Loo; 16.09.2019

Если вы работаете над проектом, который может использовать полную версию .NET, вы можете попробовать использовать метод ZipFile.CreateFromDirectory, как объяснено здесь:

using System;
using System.IO;
using System.IO.Compression;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            string startPath = @"c:\example\start";
            string zipPath = @"c:\example\result.zip";
            string extractPath = @"c:\example\extract";

            ZipFile.CreateFromDirectory(startPath, zipPath, CompressionLevel.Fastest, true);

            ZipFile.ExtractToDirectory(zipPath, extractPath);
        }
    }
}

Конечно, это будет работать только в том случае, если вы создаете новые Zip-файлы на основе заданного каталога.

Согласно комментарию, предыдущее решение не сохраняет структуру каталогов. Если это необходимо, следующий код может решить эту проблему:

    var InputDirectory = @"c:\example\start";
    var OutputFilename = @"c:\example\result.zip";
    using (Stream zipStream = new FileStream(Path.GetFullPath(OutputFilename), FileMode.Create, FileAccess.Write))
    using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
    {
        foreach(var filePath in System.IO.Directory.GetFiles(InputDirectory,"*.*",System.IO.SearchOption.AllDirectories))
        {
            var relativePath = filePath.Replace(InputDirectory,string.Empty);
            using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            using (Stream fileStreamInZip = archive.CreateEntry(relativePath).Open())
                fileStream.CopyTo(fileStreamInZip);
        }
    }
person hmadrigal    schedule 21.10.2014
comment
Просто педантичная заметка по этому поводу, которую я нашел после некоторой возни. Он работает точно не так, как стандартный файл Sent To-›Compressed на рабочем столе Windows. Windows SendTo создаст записи для каталогов в структуре явно, тогда как этот метод поддерживает структуру каталогов неявно, т. е. он не создает записи для каталогов, но каталоги перечислены в полном пути к каждому файлу. Это не меняет способ работы функции winzip (с ней тоже приятно работать), но это просто вопрос того, ожидаете ли вы определенную файловую структуру. - person Andy Bullivent; 08.03.2017
comment
Спасибо. Сэкономил мне много времени. Я только что немного отредактировал код, надеюсь, вы его примете. Я добавил Substring(1) для получения относительного пути, чтобы сделать zip-файл доступным для просмотра. - person A Khudairy; 31.10.2017

Вот одно из возможных решений:

public static class ZipArchiveExtension
{
    public static ZipArchiveDirectory CreateDirectory(this ZipArchive @this, string directoryPath)
    {
        return new ZipArchiveDirectory(@this, directoryPath);
    }
}

public class ZipArchiveDirectory
{
    private readonly string _directory;
    private ZipArchive _archive;

    internal ZipArchiveDirectory(ZipArchive archive, string directory)
    {
        _archive = archive;
        _directory = directory;
    }

    public ZipArchive Archive { get{return _archive;}}

    public ZipArchiveEntry CreateEntry(string entry)
    {
        return _archive.CreateEntry(_directory + "/" + entry);
    }

    public ZipArchiveEntry CreateEntry(string entry, CompressionLevel compressionLevel)
    {
        return _archive.CreateEntry(_directory + "/" + entry, compressionLevel);
    }
}

и используется:

var directory = _archive.CreateDirectory(context);
var entry = directory.CreateEntry(context);
var stream = entry.Open();

но я предвижу проблемы с вложенностью, возможно.

person Meirion Hughes    schedule 28.02.2013

Я также искал похожее решение и нашел решение @Val & @sDima более многообещающим для меня. Но я обнаружил некоторые проблемы с кодом и исправил их для использования с моим кодом.

Как и @sDima, я также решил использовать расширение, чтобы добавить больше функциональности в ZipArchive.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Compression;
using System.IO;

namespace ZipTest
{
   public static class ZipArchiveExtensions
   {
      public static void CreateEntryFromAny(this ZipArchive archive, string sourceName, string entryName, CompressionLevel compressionLevel = CompressionLevel.Optimal)
    {
        try
        {
            if (File.GetAttributes(sourceName).HasFlag(FileAttributes.Directory))
            {
                archive.CreateEntryFromDirectory(sourceName, entryName, compressionLevel);
            }
            else
            {
                archive.CreateEntryFromFile(sourceName, entryName, compressionLevel);
            }
        }
        catch
        {
            throw;
        }
    }

    public static void CreateEntryFromDirectory(this ZipArchive archive, string sourceDirName, string entryName, CompressionLevel compressionLevel)
    {
        try
        {
            var files = Directory.EnumerateFileSystemEntries(sourceDirName);
            if (files.Any())
            {
                foreach (var file in files)
                {
                    var fileName = Path.GetFileName(file);
                    archive.CreateEntryFromAny(file, Path.Combine(entryName, fileName), compressionLevel);
                }
            }
            else
            {
                //Do a folder entry check.
                if (!string.IsNullOrEmpty(entryName) && entryName[entryName.Length - 1] != '/')
                {
                    entryName += "/";
                }

                archive.CreateEntry(entryName, compressionLevel);
            }
        }
        catch
        {
            throw;
        }
      }
    }
  }

Вы можете попробовать расширение, используя простой приведенный ниже,

 class Program
 {
    static void Main(string[] args)
    {
        string filePath = @"C:\Users\WinUser\Downloads\Test.zip";
        string dirName = Path.GetDirectoryName(filePath);
       
        if (File.Exists(filePath))
            File.Delete(filePath);

        using (ZipArchive archive = ZipFile.Open(filePath, ZipArchiveMode.Create))
        {
            archive.CreateEntryFromFile( @"C:\Users\WinUser\Downloads\file1.jpg", "SomeFolder/file1.jpg", CompressionLevel.Optimal);
            archive.CreateEntryFromDirectory(@"C:\Users\WinUser\Downloads\MyDocs", "OfficeDocs", CompressionLevel.Optimal);
            archive.CreateEntryFromAny(@"C:\Users\WinUser\Downloads\EmptyFolder", "EmptyFolder", CompressionLevel.Optimal);
        };

        using (ZipArchive zip = ZipFile.OpenRead(filePath))
        {
            string dirExtract = @"C:\Users\WinUser\Downloads\Temp";
            if (Directory.Exists(dirExtract))
            {
                Directory.Delete(dirExtract, true);
            }

            zip.ExtractToDirectory(dirExtract);
        }
     }
   }

Я попытался сохранить точное поведение стандартного CreateEntryFromFile из .Net Framework для методов расширения.

Чтобы использовать приведенный пример кода, добавьте ссылку на System.IO.Compression.dll и System.IO.Compression.FileSystem.dll.

Ниже приведены преимущества этого класса расширения.

  1. Рекурсивное добавление содержимого папки.
  2. Поддержка пустой папки.
person Nitheesh George    schedule 30.04.2020

Мой ответ основан на ответе Вала, но немного улучшен для производительности и без создания пустых файлов в ZIP.

public static class ZipArchiveExtensions
{
    public static void CreateEntryFromAny(this ZipArchive archive, string sourceName, string entryName = "")
    {
        var fileName = Path.GetFileName(sourceName);
        if (File.GetAttributes(sourceName).HasFlag(FileAttributes.Directory))
        {
            archive.CreateEntryFromDirectory(sourceName, Path.Combine(entryName, fileName));
        }
        else
        {
            archive.CreateEntryFromFile(sourceName, Path.Combine(entryName, fileName), CompressionLevel.Optimal);
        }
    }

    public static void CreateEntryFromDirectory(this ZipArchive archive, string sourceDirName, string entryName = "")
    {
        var files = Directory.EnumerateFileSystemEntries(sourceDirName);
        foreach (var file in files)
        {
            archive.CreateEntryFromAny(file, entryName);
        }
    }
}

Пример использования:

// Create and open a new ZIP file
                using (var zip = ZipFile.Open(ZipPath, ZipArchiveMode.Create))
                {
                    foreach (string file in FILES_LIST)
                    {
                        // Add the entry for each file
                        zip.CreateEntryFromAny(file);
                    }
                }
person sDima    schedule 13.04.2020

Еще одно настроенное расширение ZipArchive, добавляющее структуру папок, включая все подпапки и файлы в zip. Решено IOException (Процесс не может получить доступ к файлу...), который выдается, если файлы используются в момент архивирования, например, каким-либо регистратором

public static class ZipArchiveExtensions
{
    public static void AddDirectory(this ZipArchive @this, string path)
    {
        @this.AddDirectory(path, string.Empty);
    }

    private static void AddDirectory(this ZipArchive @this, string path, string relativePath)
    {
        var fileSystemEntries = Directory.EnumerateFileSystemEntries(path);

        foreach (var fileSystemEntry in fileSystemEntries)
        {
            if (File.GetAttributes(fileSystemEntry).HasFlag(FileAttributes.Directory))
            {
                @this.AddDirectory(fileSystemEntry, Path.Combine(relativePath, Path.GetFileName(fileSystemEntry)));
                continue;
            }

            var fileEntry = @this.CreateEntry(Path.Combine(relativePath, Path.GetFileName(fileSystemEntry)));

            using (var zipStream = fileEntry.Open())
            using (var fileStream = new FileStream(fileSystemEntry, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (var memoryStream = new MemoryStream())
            {
                fileStream.CopyTo(memoryStream);

                var bytes = memoryStream.ToArray();
                zipStream.Write(bytes, 0, bytes.Length);
            }
        }
    }
}
person Piwnik    schedule 12.01.2021
comment
Я изменил второй метод на общедоступный в своем коде. Я также удалил поток памяти, так как не хочу читать весь файл в ОЗУ: fileStream.CopyTo(zipStream); - person andasa; 05.02.2021

Небольшое изменение в очень хорошем подходе от @Andrey

public static void CreateEntryFromDirectory2(this ZipArchive archive, string sourceDirName, CompressionLevel compressionLevel = CompressionLevel.Fastest)
    {
        var folders = new Stack<string>();

        folders.Push(sourceDirName);

        do
        {
            var currentFolder = folders.Pop();

            foreach (var item in Directory.GetFiles(currentFolder))
            {
                archive.CreateEntryFromFile(item, item.Substring(sourceDirName.Length + 1), compressionLevel);
            }

            foreach (var item in Directory.GetDirectories(currentFolder))
            {
                folders.Push(item);
            }
        } 
        while (folders.Count > 0);
    }
person Murilo Maciel Curti    schedule 13.02.2021

Используйте рекурсивный подход к Zip-папкам с подпапками.

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;

public static async Task<bool> ZipFileHelper(IFolder folderForZipping, IFolder folderForZipFile, string zipFileName)
{
    if (folderForZipping == null || folderForZipFile == null
        || string.IsNullOrEmpty(zipFileName))
    {
        throw new ArgumentException("Invalid argument...");
    }

    IFile zipFile = await folderForZipFile.CreateFileAsync(zipFileName, CreationCollisionOption.ReplaceExisting);

    // Create zip archive to access compressed files in memory stream
    using (MemoryStream zipStream = new MemoryStream())
    {
        using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
        {
            await ZipSubFolders(folderForZipping, zip, "");
        }

        zipStream.Position = 0;
        using (Stream s = await zipFile.OpenAsync(FileAccess.ReadAndWrite))
        {
            zipStream.CopyTo(s);
        }
    }
    return true;
}

//Create zip file entry for folder and subfolders("sub/1.txt")
private static async Task ZipSubFolders(IFolder folder, ZipArchive zip, string dir)
{
    if (folder == null || zip == null)
        return;

    var files = await folder.GetFilesAsync();
    var en = files.GetEnumerator();
    while (en.MoveNext())
    {
        var file = en.Current;
        var entry = zip.CreateEntryFromFile(file.Path, dir + file.Name);                
    }

    var folders = await folder.GetFoldersAsync();
    var fEn = folders.GetEnumerator();
    while (fEn.MoveNext())
    {
        await ZipSubFolders(fEn.Current, zip, dir + fEn.Current.Name + "/");
    }
}
person Hack ok    schedule 25.01.2018

Мне не нравится рекурсия, предложенная @Val, @sDima, @Nitheesh. Потенциально это приводит к StackOverflowException. потому что стек имеет ограниченный размер. Итак, вот мои два цента с обходом дерева.

public static class ZipArchiveExtensions
{
    public static void CreateEntryFromDirectory(this ZipArchive archive, string sourceDirName, CompressionLevel compressionLevel = CompressionLevel.Fastest)
    {
        var folders = new Stack<string>();
        folders.Push(sourceDirName);
        
        do
        {
            var currentFolder = folders.Pop();
            Directory.GetFiles(currentFolder).ForEach(f => archive.CreateEntryFromFile(f, f.Substring(sourceDirName.Length+1), compressionLevel));
            Directory.GetDirectories(currentFolder).ForEach(d => folders.Push(d));
        } while (folders.Count > 0);
    }
}
person Andrey Burykin    schedule 08.07.2020
comment
Очень хорошее решение! Я изменил Array.ForEach на обычный foreach - person Murilo Maciel Curti; 14.02.2021