Reputation: 308
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
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
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
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
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
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