Create ZIP archives with PowerShell and the Shell.Application COM object

If you search the web, then you’ll find no shortage of PowerShell scripts for creating ZIP archives. Most of these solutions use the .NET compression classes, where some scripts simply call command-line versions of third-party archiving commands like WinRar, 7Zip, or even the venerable WinZip. That’s great if it works for you. However, all of these should be unnecessary by the time PowerShell 5.0 rolls around, but we’ll need to roll out our own zip tools for now.

This is especially true when you’re working with Desired State Configuration (DSC) and a pull server. If you want to deploy custom resources to managed nodes, then the resources need to be zipped up and copied to the pull server. Although there’s a checksum step that needs to be performed, but that’s not what I’ll cover in this article today. There are reports that zip files created with the .NET compression classes don’t work properly in a pull server situation. The suggestion is to use Windows Explorer to create the zip file. Alternatively, you can use the Shell.Application COM object, which I’ll show you how to use in this article.
Here’s the function.

#requires -version 3.0
Function New-ZipArchive {
<#
.Synopsis
Create a zip archive from a folder.
.Description
This command will create a zip file from the specified path. The path will be a top level folder in the archive.
.Parameter Path
The top level folder to be archived. This parameter has aliases of PSPath and Source.
.Parameter OutputPath
The filename for the zip file to be created. If it already exists, the command will not run, unless you use -Force. This parameter has aliases of Zip and Target.
.Parameter Force
Delete the existing zip file and create a new one.
.Example
PS C:\> New-ZipArchive -path c:\work -outputpath e:\workback.zip
Create a new zip file called WorkBack.zip. The top level folder in the archive will be Work.
.Example
PS C:\> $dscres = Get-DSCResource | Select -expandproperty Module -unique | where {$_.path -notmatch "windows\\system32"}
PS C:\> $dscres | foreach {
 $out = "{0}_{1}.zip" -f $_.Name,$_.Version
 $zip = Join-Path -path "E:\DSC\ZipResource" -ChildPath $out
 New-ZipArchive -path $_.ModuleBase -OutputPath $zip -Passthru -force
 }
 The first command gets a unique list of modules for all DSC resources filtering out anything under System32. The second command creates a zip file for each module using the naming format modulename_version.zip.
.Notes
Version      : 1.0
Last Updated : February 2, 2015
Learn more about PowerShell:
Essential PowerShell Learning Resources
**************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** #> [cmdletbinding(SupportsShouldProcess)] param( [Parameter(Position=0,Mandatory, HelpMessage="Enter the folder path to be archived.")] [ValidateScript({Test-Path $_})] [Alias("PSPath","Source")] [String]$Path, [Parameter(Position=1,Mandatory, HelpMessage="Enter the path and filename for the zip file")] [Alias("zip","Target")] [ValidateNotNullorEmpty()] [String]$OutputPath, [Switch]$Force, [switch]$Passthru ) Write-Verbose "Starting $($MyInvocation.Mycommand)" Write-Verbose "Using bound parameters:" Write-verbose ($MyInvocation.BoundParameters| Out-String).Trim() if ($Force -AND (Test-Path -path $OutputPath)) { Write-Verbose "Testing for existing file and deleting it" Remove-Item -Path $OutputPath } if(-NOT (Test-Path $OutputPath)) { Write-Verbose "Creating $OutputPath" Try { #create an empty zip file Set-Content -path $OutputPath -value ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18)) -ErrorAction Stop #get the zip file object $zipfile = $OutputPath | Get-Item -ErrorAction Stop #make sure it is not set to ReadOnly write-verbose "Setting isReadOnly to False" $zipfile.IsReadOnly = $false } Catch { Write-Warning "Failed to create $outputpath" write-Warning $_.exception.message #bail out Return } } #if not test zip file path else { Write-Warning "The file $OutputPath already exists. Please delete or use -Force and try again." #bail out Return } if ($PSCmdlet.ShouldProcess($Path)) { Write-Verbose "Creating Shell.Application" $shellApp = New-Object -com shell.application Write-Verbose "Using namespace $($zipfile.fullname)" $zipPackage = $shellApp.NameSpace($zipfile.fullname) write-verbose ($zipfile | Out-String) $target = Get-Item -Path $Path $zipPackage.CopyHere($target.FullName) If ($passthru) { #Pause enough to give the zip file a chance to update Start-Sleep -Milliseconds 200 Get-Item -Path $Outputpath } } #should process Write-Verbose "Ending $($MyInvocation.Mycommand)" } #close New-ZipFile function

Although I wrote the function with DSC in mind, you can use it to create a zip archive for any folder. It’s very important to note that the specified folder that you’re archiving will be a top-level folder in the zip archive. To use, all you need to do is specify the target folder and the name of the zip file to create. The zip file must not already exist, otherwise you’ll receive a warning. I’ve included a –Force parameter to delete the zip file if it already exists so that you can create a new one. I’ve also included simple support for –WhatIf.

Here’s how you might use my solution to backup a folder.

new-ziparchive c:\work e:\work.zip -verb -Passthru -force

Creating a zip archive with PowerShell. (Image Credit: Jeff Hicks)
Creating a zip archive with PowerShell. (Image Credit: Jeff Hicks) 

With –Passthru, you may see the zip file that shows the initial size. Although there’s supposed to be flags that you can use to suppress the copy dialog box, I can’t get any of them to work on Windows 8.1. In my research, it appears that these flags may or may not work by design. However, the dialog may be helpful for larger folders.
You can use this function for any module that’s installed on your system. This is very handy if you’re developing a new module and want to package it.

$m = Get-Module ISEScriptingGeek –ListAvailable
New-ZipArchive $m.ModuleBase "e:\$($m.name).zip"

The ModuleBase property is the top-level folder for the module. The example above will create a zip file for the module using the module name on the E:\ drive.

Finally, here’s an example where I use the function to create zip files for all the custom DSC resources installed on my computer.

Get-DSCResource | Select -expandproperty Module -unique |
where {$_.path -notmatch "windows\\system32"} | foreach {
 $out = "{0}_{1}.zip" -f $_.Name,$_.Version
 $zip = Join-Path -path "E:\DSC\ZipResource" -ChildPath $out
 New-ZipArchive -path $_.ModuleBase -OutputPath $zip -Passthru -force
}

My sample skips anything under System32, which should be the out-of-the-box resource modules like PSDesiredStateConfiguration. Note that I am using the .NET format operator, -f, to create the output file name.

$out = "{0}_{1}.zip" -f $_.Name,$_.Version

This is often an easier approach instead of relying on variable expansion, or God forbid, concatenation.
If you’re getting started with DSC, then you might want to take a look at my fundamentals course at Pluralsight. If you’re looking to create PowerShell tools and modules, then be sure to take a look at Learn PowerShell Toolmaking in a Month of Lunches. In any event, I hope this function helps until PowerShell 5.0 hits your desktop.