randomraccoon
randomraccoon

Reputation: 308

How to zip a directory's contents except one subdirectory?

I'm attempting to create a zip file to serve as a backup of a directory, and eventually save that backup inside a "backups" folder in that directory. For illustration, "folder" includes "subFolder", "file1.txt", "file2.txt", and "backups". The "backups" folder would contain various earlier backup files. I want to create an archive of "folder" and all its contents, including subfolder, but exclude "backups".

Here is what I originally wanted to use:

ZipFile.CreateFromDirectory(folderToZip, backupFileName);

I realize there are problems with saving a zip file inside the folder being zipped, so I intended to save it somewhere else and then transfer it. But I don't know how to easily create an archive without the backups folder. My only thought is to write my own method recursively traversing the directory and excluding that one folder. It seems like there must be a simpler way.

Any help would be greatly appreciated!

Upvotes: 13

Views: 11193

Answers (5)

Kurt Van den Branden
Kurt Van den Branden

Reputation: 12973

Remove them afterwards:

var folderToZip = "folderName";
var file = "zipfileName";

using (ZipFile zip = new ZipFile())
{
    zip.AddDirectory(folderToZip);
    MemoryStream output = new MemoryStream();
            
    zip.RemoveSelectedEntries("FolderToExclude/*"); //Will remove all files and folder
    zip.RemoveSelectedEntries("OtherFolderToExclude/*");

    zip.Save(output);
    
    File(output.ToArray(), "application/zip", file);
}
 

Upvotes: 2

Justin
Justin

Reputation: 1503

In my case i needed to do this in PowerShell, here is the script I created based on the suggestions above:

Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
    

function Get-RelativePath
{
    <#
    .SYNOPSIS
    Returns the relative path of an object compared to a Base path
    #>
    param (
        [Parameter(Mandatory=$true)][string]$Path,
        [Parameter(Mandatory=$true)][string]$BasePath,
        [Parameter(Mandatory=$true,ParameterSetName="file")][switch]$File,
        [Parameter(Mandatory=$true,ParameterSetName="dir")][switch]$Directory)

    $Base = [System.IO.Path]::GetFullPath($BasePath)
    if ($File) {
        $fi = new-object System.IO.FileInfo ($Path)
        $Full = $fi.Directory
    } else {
        $fi = $null
        $Full = new-object System.IO.DirectoryInfo ($Path)
    }

    $CurLoc = $Full
    $Relative = @()
    while ($CurLoc.FullName -ne $Base -and !([string]::IsNullOrEmpty($CurLoc.Name)))
    {
        #Write-Host $CurLoc.Name
        $Relative += $CurLoc.Name
        $CurLoc = $CurLoc.Parent
    }
    if ($Relative.Count -eq 0) {return ""}
    [void][System.Array]::Reverse($Relative)
    if ($null -ne $fi) {
        $Relative += $fi.Name
    }
    return [string]::Join([System.IO.Path]::DirectorySeparatorChar,$Relative)
}

function Get-DirectoriesWithExclusion
{
    <#
    .SYNOPSIS
    Gets a list ofdirectories with optional include/exclude lists
    .PARAMETER SourceDirectory
    Folder to compress
    .PARAMETER SearchPattern
    Wildcard for OS search
    .PARAMETER Recurse
    When enabled, will evaluate child directories too
    .PARAMETER excludeDirRegex

    List of regex to use in excluding Dirs by name
    .PARAMETER excludeDirList
    List of regex strings to use in excluding Dirs by name, only used if excludeDirRegex is not supplied
    .PARAMETER includeDirRegex
    List of regex to use in including Dirs by name
    .PARAMETER includeDirList
    List of regex strings to use in including Dirs by name, only used if includeDirRegex is not supplied
    #>
    param(
        [Parameter(Mandatory=$true)][string]$SourceDirectory, 
        [string]$SearchPattern = "*",
        [switch]$Recurse,
        [System.Text.RegularExpressions.Regex[]]$excludeDirRegex,
        [string[]]$excludeDirList,
        [System.Text.RegularExpressions.Regex[]]$includeDirRegex,
        [string[]]$includeDirList
    )
    if (![System.IO.Directory]::Exists($SourceDirectory)) { throw [System.IO.FileNotFoundException] "source directory $SourceDirectory for zipping does NOT exist";}
    $di = new-object System.IO.DirectoryInfo ($SourceDirectory)
    
    #build regex
    if ($null -eq $excludeDirRegex) {
        $excludeDirRegex = @()
        $excludeDirRegex += $excludeDirList | Where-Object {$_}| ForEach-Object{ new-object System.Text.RegularExpressions.Regex $_,([System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Compiled) }
    }
    if ($null -eq $includeDirRegex) {
        $includeDirRegex = @()
        $includeDirRegex += $includeDirList | Where-Object {$_}| ForEach-Object{ new-object System.Text.RegularExpressions.Regex $_,([System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Compiled) }
    }

    #find the dirs recursively
    foreach ($srcDir in $di.EnumerateDirectories($SearchPattern,[System.IO.SearchOption]::TopDirectoryOnly))
    {

        #if directory is not excluded
        if (($includeDirRegex.Count -eq 0 -or ($includeDirRegex | Where-Object {$_.IsMatch($srcDir.Name)})) -and!($excludeDirRegex | Where-Object {$_.IsMatch($srcDir.Name)}))
        {
            Write-Output $srcDir
            if ($Recurse) 
            {
                Get-DirectoriesWithExclusion -SourceDirectory $srcDir.FullName -SearchPattern $SearchPattern -includeDirRegex $includeDirRegex -excludeDirRegex $excludeDirRegex -Recurse
            }
        }
    }
}

function Compress-Directory 
{
    <#
    .SYNOPSIS
    Creates a zip of a directory
    .DESCRIPTION 
    Creates a zip of a directory with optional include/exclude lists
    .PARAMETER SourceDirectory
    Folder to compress
    .PARAMETER ZipFile
    Zipfile to create
    .PARAMETER SearchPattern
    Wildcard for OS search
    .PARAMETER Recurse
    When enabled, will evaluate child directories too
    .PARAMETER excludeFileRegex
    List of regex to use in excluding files by name
    .PARAMETER excludeFileList
    List of regex strings to use in excluding files by name, only used if excludeFileRegex is not supplied
    .PARAMETER excludeDirRegex

    List of regex to use in excluding Dirs by name
    .PARAMETER excludeDirList
    List of regex strings to use in excluding Dirs by name, only used if excludeDirRegex is not supplied
    .PARAMETER includeFileRegex
    List of regex to use in including files by name
    .PARAMETER includeFileList
    List of regex strings to use in including files by name, only used if includeFileRegex is not supplied
    .PARAMETER includeDirRegex
    List of regex to use in including Dirs by name
    .PARAMETER includeDirList
    List of regex strings to use in including Dirs by name, only used if includeDirRegex is not supplied
    .PARAMETER rootDirInZip
    When specified will create a folder in the zip to place all files
    .PARAMETER IgnoreRootFiles
    When specified, files in the root of the source directory will be ignored
    
    #>
    param (
        [Parameter(Mandatory=$true)][string]$SourceDirectory, 
        [Parameter(Mandatory=$true)][string]$ZipFile, 
        [string]$SearchPattern = "*",
        [switch]$Recurse,
        [System.Text.RegularExpressions.Regex[]]$excludeFileRegex,
        [System.Text.RegularExpressions.Regex[]]$excludeDirRegex,
        [string[]]$excludeFileList,
        [string[]]$excludeDirList,
        [System.Text.RegularExpressions.Regex[]]$includeFileRegex,
        [System.Text.RegularExpressions.Regex[]]$includeDirRegex,
        [string[]]$includeFileList,
        [string[]]$includeDirList,
        [string]$RootDirInZip,
        [switch]$WhatIf,
        [switch]$IgnoreRootFiles,
        [System.IO.Compression.CompressionLevel]$CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal

        )
    #init/validate parms
    if (![System.IO.Directory]::Exists($SourceDirectory)) { throw [System.IO.FileNotFoundException] "source directory $SourceDirectory for zipping does NOT exist";}
    $fi = new-object System.IO.FileInfo $ZipFile
    if (!$fi.Directory.Exists) { throw [System.IO.FileNotFoundException] "ZipFile path $($fi.Directory.FullName) does NOT exist"; }
    $di = new-object System.IO.DirectoryInfo ($SourceDirectory)
    
    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    #build regex
    if ($null -eq $excludeFileRegex) {
        $excludeFileRegex = @()
        $excludeFileRegex += $excludeFileList | Where-Object {$_}| ForEach-Object{ new-object System.Text.RegularExpressions.Regex $_,([System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Compiled) }
    }

    if ($null -eq $includeFileRegex) {
        $includeFileRegex = @()
        $includeFileRegex += $includeFileList | Where-Object {$_}| ForEach-Object{ new-object System.Text.RegularExpressions.Regex $_,([System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Compiled) }
    }

    $FilesZipped = 0
    #build the zip   
    if (!($WhatIf)) { 
        $FileMode = [System.IO.FileMode]::OpenOrCreate
        $ZipMode = ([System.IO.Compression.ZipArchiveMode]::Create)
        try {
            $fs = New-Object System.IO.FileStream $zipFile, $FileMode
            try
            {
                $archive = New-Object System.IO.Compression.ZipArchive $fs,$ZipMode
            } catch {
                $fs.Dispose()
                throw;
            }
        } catch {
            throw;  
        }
        
    }
    try
    {
        #handle root dir
        if (!($IgnoreRootFiles))
        {
            $RelativeRoot=""
            if ($RootDirInZip) {
                $RelativeRoot="$RootDirInZip\"
            }
            foreach ($srcFile in $di.EnumerateFiles($SearchPattern, [System.IO.SearchOption]::TopDirectoryOnly))
            {
                if (($includeFileRegex.Count -eq 0 -or ($includeFileRegex | Where-Object {$_.IsMatch($srcFile.Name)})) -and !($excludeFileRegex | Where-Object {$_.IsMatch($srcFile.Name)}))
                {
                    if ($WhatIf) {
                        Write-Host "Would add $($srcFile.FullName) -> $RelativeRoot$($srcFile.Name)"   
                    } else {
                        [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive,$srcFile.FullName,"$RelativeRoot$($srcFile.Name)",$compressionLevel)
                    }
                    $FilesZipped++
                }                    
            }
        }

        if ($Recurse)
        {

            $DirList = Get-DirectoriesWithExclusion -SourceDirectory $di.FullName -SearchPattern $SearchPattern -Recurse -excludeDirList $excludeDirList -excludeDirRegex $excludeDirRegex -includeDirList $includeDirList -includeDirRegex $includeDirRegex 
            #handle child dirs
            foreach ($srcDir in $DirList)
            {
                Write-Verbose "Evaluating $($srcDir.FullName) for files"
                $RelativeRoot = Get-RelativePath -Directory -Path $srcDir.FullName -BasePath $SourceDirectory
                if ($RootDirInZip) {
                    $RelativeRoot = "$RootDirInZip\$RelativeRoot\"
                }

                foreach ($srcFile in $srcDir.EnumerateFiles($SearchPattern,[System.IO.SearchOption]::TopDirectoryOnly))
                {
                    if (($includeFileRegex.Count -eq 0 -or ($includeFileRegex | Where-Object {$_.IsMatch($srcFile.Name)})) -and !($excludeFileRegex | Where-Object {$_.IsMatch($srcFile.Name)}))
                    {
                        if ($WhatIf) {
                            Write-Host "Would add $($srcFile.FullName) -> $RelativeRoot\$($srcFile.Name)"   
                        } else {
                            [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive,$srcFile.FullName,"$RelativeRoot\$($srcFile.Name)",$compressionLevel)
                        }
                        
                    }   
                    $FilesZipped++                 
                }
                
            }
        }
        if ($WhatIf) {
            Write-Host "Found $FilesZipped files to add to $ZipFile"
        } else {
            $fs.Flush()
            $sw.Stop()
            if ($FilesZipped -eq 0) {
                Write-Warning "No files found to include!"
            } else {
                Write-Verbose "Added $FilesZipped files to $ZipFile, taking $($sw.Elapsed)"
            }
        }
    } finally {
        if (!($WhatIf)) { 
            $archive.Dispose()
            $fs.Dispose()
        }
    }
}


Compress-Directory -SourceDirectory "E:\PowershellModules" -ZipFile E:\PowershellModules\test.zip -IgnoreRootFiles -RootDirInZip "Modules" -excludeFileList "log$" -includeDirList 'common' -excludeDirList "badmodule" -includeFileList "psm1$"  -Verbose

Upvotes: 0

uril
uril

Reputation: 193

You can add an extension to ZipArchive:

    public static void ZipDirectory(this ZipArchive zipArchive, string srcDir, IEnumerable<Regex>  excludeFileList = null, IEnumerable<Regex> excludeDirList = null , string rootDir = "")
    {
        if (!Directory.Exists(srcDir)) throw new Exception("source directory for zipping doesn't exit");
        var dir = new DirectoryInfo(srcDir);

        dir.GetFiles().ToList().ForEach((file) => {
            if (excludeFileList == null || excludeFileList.Where(rule => rule.IsMatch(file.Name)).Count() == 0)
            {
                zipArchive.CreateEntryFromFile(file.FullName, string.IsNullOrEmpty(rootDir) ? file.Name : $@"{rootDir}\{file.Name}");
            }});

        dir.GetDirectories().ToList().ForEach((directory) => {
            if (excludeDirList == null || excludeDirList.Where(rule => rule.IsMatch(directory.Name)).Count() == 0)
            {
                zipArchive.ZipDirectory(directory.FullName, excludeFileList, excludeDirList, string.IsNullOrEmpty(rootDir) ? $@"{directory.Name}" : $@"{rootDir}\{directory.Name}");
            }});
    }

and use it like that:

var excludeFileList = new List<Regex>() { new Regex(".pdb$") };

using (var zipFileStream = new FileStream(zipFile, FileMode.Create))
{
     using (var zipArch = new ZipArchive(zipFileStream, ZipArchiveMode.Create))
     {
          zipArch.ZipDirectory(src, excludeFileList );
     }
}

Upvotes: 1

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 727067

Unfortunately, ZipFile does not offer a method that lets you filter entries. Fortunately, you can easily create a method like this based on this implementation:

public static class ZipHelper {
    public static void CreateFromDirectory(
        string sourceDirectoryName
    ,   string destinationArchiveFileName
    ,   CompressionLevel compressionLevel
    ,   bool includeBaseDirectory
    ,   Encoding entryNameEncoding
    ,   Predicate<string> filter // Add this parameter
    ) {
        if (string.IsNullOrEmpty(sourceDirectoryName)) {
            throw new ArgumentNullException("sourceDirectoryName");
        }
        if (string.IsNullOrEmpty(destinationArchiveFileName)) {
            throw new ArgumentNullException("destinationArchiveFileName");
        }
        var filesToAdd = Directory.GetFiles(sourceDirectoryName, "*", SearchOption.AllDirectories);
        var entryNames = GetEntryNames(filesToAdd, sourceDirectoryName, includeBaseDirectory);
        using(var zipFileStream = new FileStream(destinationArchiveFileName, FileMode.Create)) {
            using (var archive = new ZipArchive(zipFileStream, ZipArchiveMode.Create)) {
                for (int i = 0; i < filesToAdd.Length; i++) {
                    // Add the following condition to do filtering:
                    if (!filter(filesToAdd[i])) {
                        continue;
                    }
                    archive.CreateEntryFromFile(filesToAdd[i], entryNames[i], compressionLevel);
                }
            }
        }
    }
}

This implementation lets you pass a filter that rejects all entries from the "backup/" directory:

ZipHelper.CreateFromDirectory(
    myDir, destFile, CompressionLevel.Fastest, true, Encoding.UTF8,
    fileName => !fileName.Contains(@"\backup\")
);

Upvotes: 18

Ignacio
Ignacio

Reputation: 838

In my opinion, you can take every file inside the folder excluding the "backups" folder and make an temporary folder with them to make the zip as you have said.

With this LINQ query you can do it in a really simple way:

List<FileInfo> files = di.GetFiles("*", SearchOption.AllDirectories).Where(file => !file.DirectoryName.Contains("backups")).ToList();

Once you have this list, you can copy all this files, make the zip and erase the content. I can not see a simpler way.

Upvotes: 1

Related Questions