Technology Toolbox

Your technology Sherpa for the Microsoft platform

Jeremy Jameson - Founder and Principal

Search

Search

Zip a folder using PowerShell

After creating the code sample for my previous post, I realized the original zip file I provided contained quite a bit of "junk" (e.g. temporary object folders created during the build, a copy of one of the SharePoint assemblies in the "bin" folder, etc.). I've since updated the attachment on that post to reduce the size from 850 KB to a mere 134 KB.

However, there's a strong possibility this will happen again in the future with some other post, if I don't take the time to either document the process of "trimming the fat" before creating a zip file for a code sample or automate the process via PowerShell.

Since I estimated the script would take only slightly longer to create than the documentation -- and also save considerable time over the long run -- I decided to go with the PowerShell option.

I already assembled some PowerShell for a similar scenario last year (in order to quickly transfer the SharePoint solution that I was working on between environments). From the research I did back then, I recall there being a couple of approaches to creating a zip file in PowerShell. One is to use the PowerShell Community Extensions (or some other third-party solution), and the other is to combine out-of-the-box PowerShell with some scriptable COM objects from the Windows Shell (specifically the Shell and Folder objects).

Imagine you have a folder (e.g. C:\NotBackedUp\Fabrikam) that you want to compress into a zip file (e.g. C:\NotBackedUp\Fabrikam.zip).

Assuming you have installed the PowerShell Community Extensions, you could simply execute the following in Windows PowerShell:

PS C:\Users\jjameson> Import-Module Pscx
PS C:\Users\jjameson> cd C:\NotBackedUp
C:\NotBackedUp
PS C:\NotBackedUp> Write-Zip Fabrikam -OutputPath Fabrikam.zip -IncludeEmptyDirectories

Mode           LastWriteTime       Length Name
----           -------------       ------ ----
-a---     2/28/2012  5:00 AM      7698443 Fabrikam.zip

However, what if you don't have the PowerShell Community Extensions installed (and, for whatever reason, you can't or don't want to install them)? In that case, it takes a little more work.

If you Google "PowerShell zip files" you'll quickly discover a number of resources that show how to create a zip file using the Set-Content cmdlet, followed by the use of the CopyHere method on the Folder shell object. For example, David Aiken's blog post shows the following:

function Add-Zip
{
	param([string]$zipfilename)

	if(-not (test-path($zipfilename)))
	{
		set-content $zipfilename ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
		(dir $zipfilename).IsReadOnly = $false	
	}
	
	$shellApplication = new-object -com shell.application
	$zipPackage = $shellApplication.NameSpace($zipfilename)
	
	foreach($file in $input) 
	{ 
            $zipPackage.CopyHere($file.FullName)
            Start-sleep -milliseconds 500
	}
} 

However, there are a few problems with this approach:

  • If you use the function as David illustrates in his post (e.g. "dir Fabrikam\*.* -Recurse | Add-Zip Fabrikam.zip") then the folder hierarchy is not preserved within the zip file -- which is almost certainly not what you want (but seems to have been okay for David's scenario).
  • If you try to operate on the folder instead (e.g. "dir Fabrikam | Add-Zip Fabrikam.zip") then an error occurs:
    You cannot call a method on a null-valued expression.
    At line:13 char:33
    +             $zipPackage.CopyHere <<<< ($file.FullName)
    ...
  • Relying exclusively on a 500 ms delay (to wait for the asynchronous CopyHere operation to complete) seems a little "dicey" to me. In other words, how do you know the zip operation completed successfully?

A different approach is to place the call to Start-Sleep inside a loop that checks the number of items in the zip file against the expected number (as shown in another blog post). This is the approach I used last year and it seemed to work just fine -- most of the time.

As I mentioned before, the CopyHere method runs asynchronously. When the CopyHere operation is running, a dialog is displayed with a Cancel button -- and if you click this by mistake (or press Enter when the dialog box has the focus) then, well, let's just say that you aren't on the "Happy Path" anymore.

To make this process more robust, I decided to use a different approach -- specifically, counting all of the files and folders in the zip file and comparing it to the expected number. The approach shown in the other blog post I referred to before only counts the items in the "root" of the zip file (which, honestly, does seem to work reliably -- even when you click the Cancel button during the CopyHere operation). However, I wanted a higher degree of confidence that wasn't based on the assumption that cancelling the CopyHere operation is treated as a "transaction."

First, we need a function to create a zip file for a specific folder (a.k.a. directory):

function ZipFolder(
    [IO.DirectoryInfo] $directory)
{
    ...    
    [IO.DirectoryInfo] $parentDir = $directory.Parent
    
    [string] $zipFileName
    
    If ($parentDir.FullName.EndsWith("\") -eq $true)
    {
        # e.g. $parentDir = "C:\"
        $zipFileName = $parentDir.FullName + $directory.Name + ".zip"
    }
    Else
    {
        $zipFileName = $parentDir.FullName + "\" + $directory.Name + ".zip"
    }
    
    ...
    
    Set-Content $zipFileName ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
        
    $shellApp = New-Object -ComObject Shell.Application
    $zipFile = $shellApp.NameSpace($zipFileName)

    ...
    
    [int] $expectedCount = (Get-ChildItem $directory -Force -Recurse).Count
    $expectedCount += 1 # account for the top-level folder
    
    $zipFile.CopyHere($directory.FullName)

    # wait for CopyHere operation to complete
    WaitForZipOperationToFinish $zipFile $expectedCount
    
    ...}

The WaitForZipOperationToFinish function is where the "magic" happens:

function WaitForZipOperationToFinish(
    [__ComObject] $zipFile,
    [int] $expectedNumberOfItemsInZipFile)
{
    ...
    
    Write-Host -NoNewLine "Waiting for zip operation to finish..."
    Start-Sleep -Milliseconds 100 # ensure zip operation had time to start
    
    [int] $waitTime = 0
    [int] $maxWaitTime = 60 * 1000 # [milliseconds]
    while($waitTime -lt $maxWaitTime)
    {
        [int] $waitInterval = GetWaitInterval($waitTime)
                
        Write-Host -NoNewLine "."
        Start-Sleep -Milliseconds $waitInterval
        $waitTime += $waitInterval

        ...        
        [bool] $isFileLocked = IsFileLocked($zipFile.Self.Path)
        
        If ($isFileLocked -eq $true)
        {
            Write-Debug "Zip file is locked by another process."
            Continue
        }
        Else
        {
            Break
        }
    }
    
    Write-Host                           
    
    If ($waitTime -ge $maxWaitTime)
    {
        Throw "Timeout exceeded waiting for zip operation"
    }
    
    [int] $count = CountZipItems($zipFile)
    
    If ($count -eq $expectedNumberOfItemsInZipFile)
    {
        Write-Debug "The zip operation completed succesfully."
    }
    ElseIf ($count -eq 0)
    {
        Throw ("Zip file is empty. This can occur if the operation is" `
            + " cancelled by the user.")
    }
    ElseIf ($count -gt $expectedCount)
    {
        Throw "Zip file contains more than the expected number of items."
    }
}

I use a variable "wait interval" to account for scenarios ranging from very small folders to relatively large folders (but still assuming the zip operation should complete in less than 60 seconds):

function GetWaitInterval(
    [int] $waitTime)
{
    If ($waitTime -lt 1000)
    {
        return 100
    }
    ElseIf ($waitTime -lt 5000)
    {
        return 1000
    }
    Else
    {
        return 5000
    }
}

To determine if the CopyHere operation is running, I check to see if the zip file can be locked exclusively:

function IsFileLocked(
    [string] $path)
{
    ...
    
    [bool] $isFileLocked = $true

    $file = $null
    
    Try
    {
        $file = [IO.File]::Open(
            $path,
            [IO.FileMode]::Open,
            [IO.FileAccess]::Read,
            [IO.FileShare]::None)
            
        $isFileLocked = $false
    }
    Catch [IO.IOException]
    {
        If ($_.Exception.Message.EndsWith(
            "it is being used by another process.") -eq $false)
        {
            Throw $_.Exception
        }
    }
    Finally
    {
        If ($file -ne $null)
        {
            $file.Close()
        }
    }
    
    return $isFileLocked
}

Once the zip file is no longer locked by the zip operation, it is time to count the total number of files and folders in a zip file:

function CountZipItems(
    [__ComObject] $zipFile)
{
    ...    
    [int] $count = CountZipItemsRecursive($zipFile)
    ...    
    return $count
}

function CountZipItemsRecursive(
    [__ComObject] $parent)
{
    ...  
    [int] $count = 0

    $parent.Items() |
        ForEach-Object {
            $count += 1
            
            If ($_.IsFolder -eq $true)
            {
                $count += CountZipItemsRecursive($_.GetFolder)
            }
        }
    
    return $count
}

The final step is to use these functions to create the zip file:

PS C:\NotBackedUp> $directory = Get-Item "C:\NotBackedUp\Fabrikam"
PS C:\NotBackedUp> ZipFolder $directory
Creating zip file for folder (C:\NotBackedUp\Fabrikam)...

Waiting for zip operation to finish..............
Counting items in zip file (C:\NotBackedUp\Fabrikam.zip)...
840 items in zip file (C:\NotBackedUp\Fabrikam.zip).
Successfully created zip file for folder (C:\NotBackedUp\Fabrikam).

Here is the PowerShell script in its entirety.

ZipFolder.ps1

function CountZipItems(
    [__ComObject] $zipFile)
{
    If ($zipFile -eq $null)
    {
        Throw "Value cannot be null: zipFile"
    }
    
    Write-Host ("Counting items in zip file (" + $zipFile.Self.Path + ")...")
    
    [int] $count = CountZipItemsRecursive($zipFile)

    Write-Host ($count.ToString() + " items in zip file (" `
        + $zipFile.Self.Path + ").")
    
    return $count
}

function CountZipItemsRecursive(
    [__ComObject] $parent)
{
    If ($parent -eq $null)
    {
        Throw "Value cannot be null: parent"
    }
    
    [int] $count = 0

    $parent.Items() |
        ForEach-Object {
            $count += 1
            
            If ($_.IsFolder -eq $true)
            {
                $count += CountZipItemsRecursive($_.GetFolder)
            }
        }
    
    return $count
}

function IsFileLocked(
    [string] $path)
{
    If ([string]::IsNullOrEmpty($path) -eq $true)
    {
        Throw "The path must be specified."
    }
    
    [bool] $fileExists = Test-Path $path
    
    If ($fileExists -eq $false)
    {
        Throw "File does not exist (" + $path + ")"
    }
    
    [bool] $isFileLocked = $true

    $file = $null
    
    Try
    {
        $file = [IO.File]::Open(
            $path,
            [IO.FileMode]::Open,
            [IO.FileAccess]::Read,
            [IO.FileShare]::None)
            
        $isFileLocked = $false
    }
    Catch [IO.IOException]
    {
        If ($_.Exception.Message.EndsWith(
            "it is being used by another process.") -eq $false)
        {
            Throw $_.Exception
        }
    }
    Finally
    {
        If ($file -ne $null)
        {
            $file.Close()
        }
    }
    
    return $isFileLocked
}
    
function GetWaitInterval(
    [int] $waitTime)
{
    If ($waitTime -lt 1000)
    {
        return 100
    }
    ElseIf ($waitTime -lt 5000)
    {
        return 1000
    }
    Else
    {
        return 5000
    }
}

function WaitForZipOperationToFinish(
    [__ComObject] $zipFile,
    [int] $expectedNumberOfItemsInZipFile)
{
    If ($zipFile -eq $null)
    {
        Throw "Value cannot be null: zipFile"
    }
    ElseIf ($expectedNumberOfItemsInZipFile -lt 1)
    {
        Throw "The expected number of items in the zip file must be specified."
    }
    
    Write-Host -NoNewLine "Waiting for zip operation to finish..."
    Start-Sleep -Milliseconds 100 # ensure zip operation had time to start
    
    [int] $waitTime = 0
    [int] $maxWaitTime = 60 * 1000 # [milliseconds]
    while($waitTime -lt $maxWaitTime)
    {
        [int] $waitInterval = GetWaitInterval($waitTime)
                
        Write-Host -NoNewLine "."
        Start-Sleep -Milliseconds $waitInterval
        $waitTime += $waitInterval

        Write-Debug ("Wait time: " + $waitTime / 1000 + " seconds")
        
        [bool] $isFileLocked = IsFileLocked($zipFile.Self.Path)
        
        If ($isFileLocked -eq $true)
        {
            Write-Debug "Zip file is locked by another process."
            Continue
        }
        Else
        {
            Break
        }
    }
    
    Write-Host                           
    
    If ($waitTime -ge $maxWaitTime)
    {
        Throw "Timeout exceeded waiting for zip operation"
    }
    
    [int] $count = CountZipItems($zipFile)
    
    If ($count -eq $expectedNumberOfItemsInZipFile)
    {
        Write-Debug "The zip operation completed succesfully."
    }
    ElseIf ($count -eq 0)
    {
        Throw ("Zip file is empty. This can occur if the operation is" `
            + " cancelled by the user.")
    }
    ElseIf ($count -gt $expectedCount)
    {
        Throw "Zip file contains more than the expected number of items."
    }
}

function ZipFolder(
    [IO.DirectoryInfo] $directory)
{
    If ($directory -eq $null)
    {
        Throw "Value cannot be null: directory"
    }
    
    Write-Host ("Creating zip file for folder (" + $directory.FullName + ")...")
    
    [IO.DirectoryInfo] $parentDir = $directory.Parent
    
    [string] $zipFileName
    
    If ($parentDir.FullName.EndsWith("\") -eq $true)
    {
        # e.g. $parentDir = "C:\"
        $zipFileName = $parentDir.FullName + $directory.Name + ".zip"
    }
    Else
    {
        $zipFileName = $parentDir.FullName + "\" + $directory.Name + ".zip"
    }
    
    If (Test-Path $zipFileName)
    {
        Throw "Zip file already exists ($zipFileName)."
    }
    
    Set-Content $zipFileName ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
        
    $shellApp = New-Object -ComObject Shell.Application
    $zipFile = $shellApp.NameSpace($zipFileName)

    If ($zipFile -eq $null)
    {
        Throw "Failed to get zip file object."
    }
    
    [int] $expectedCount = (Get-ChildItem $directory -Force -Recurse).Count
    $expectedCount += 1 # account for the top-level folder
    
    $zipFile.CopyHere($directory.FullName)

    # wait for CopyHere operation to complete
    WaitForZipOperationToFinish $zipFile $expectedCount
    
    Write-Host -Fore Green ("Successfully created zip file for folder (" `
        + $directory.FullName + ").")
}

Remove-Item "C:\NotBackedUp\Fabrikam.zip"

[IO.DirectoryInfo] $directory = Get-Item "C:\NotBackedUp\Fabrikam"
ZipFolder $directory

Tags

Comments

  1. # re: Zip a folder using PowerShell

    June 21, 2012 8:54 AM
    gomodo
    Gravatar
    good job, but one comment :

    www.powershellmagazine.com/... :

    first good practice : Annotate your script

    ZipFolder.ps1 :
    220 lines
    3 comments
  2. # re: Zip a folder using PowerShell

    June 21, 2012 9:25 AM
    gomodo
    Gravatar
    error message if folder C:\NotBackedUp\Fabrikam contain any sub folder :

    "Zip file contains more than the expected number of items."

    adding just a useless sub folder, and it works
  3. # re: Zip a folder using PowerShell

    July 31, 2012 8:29 AM
    Ray
    I am trying to create a zip file using steps mentioned above.

    While using this line :-
    $shellApp = (new-object -com shell.application).NameSpace($zipFileName)

    I'm getting this error :-
    Exception calling "NameSpace" with "1" argument(s): "Unspecified error
    (Exception from HRESULT: 0x80004005 (E_FAIL))"

    Any idea why zip file creation is failing ?
  4. # re: Zip a folder using PowerShell

    August 1, 2012 6:19 AM
    Jeremy Jameson
    Gravatar
    Ray,

    I have not encountered that error before. My recommendation is to try it on a different PC or VM. Good luck.

  5. # re: Zip a folder using PowerShell

    August 7, 2012 9:06 AM
    Paul
    Gravatar
    Great post, and just what I needed.

    Can one call this so that it zips the contents of the folder but not the top level folder itself, basically making it one level less deep? When you unzip the top level element is the folder, then the files in that folder, so how do you make the zip one level more shallow and only have the files?
  6. # ah ha!

    August 7, 2012 1:58 PM
    Paul
    Gravatar
    change the CopyHere line to be as follows, and then remove the extra increment of the file count.

    Get-ChildItem $directory | foreach {$zipFile.CopyHere($_.fullname)}

    Done deal...though now on some occasions not all files are added to the zip. Still working on that.
  7. # re: Zip a folder using PowerShell

    September 9, 2012 2:51 PM
    Emil
    This is great but what about the situation when the script(winzip) asks for the last volume as in the old times with floppy disks?!?

    Thx,
    Emil
  8. # re: Zip a folder using PowerShell

    October 25, 2012 7:40 AM
    Pelado
    Great job.
    Only a comment... about programing style and clarity.
    The boolean expressions don't need be compared with true or false: they ARE true or false.

    Examples:
    If ($_.IsFolder -eq $true) ==> If ($_.IsFolder)
    If ($fileExists -eq $false) ==> If (-not $fileExists)

  9. # ePUB Automator unter Windows mit Notepad++

    November 3, 2012 4:30 AM
    Pingback/TrackBack
    ePUB Automator unter Windows mit Notepad++
  10. # re: Zip a folder using PowerShell

    November 6, 2012 6:49 PM
    Steph
    Great post. Easily written and understood and it worked. Nice one :-)
  11. # re: Zip a folder using PowerShell

    November 19, 2012 11:11 PM
    Rahul
    Gravatar
    Thanks for the tutorial.
    But i am new to the coding and knows only C++, can you please share the same code in C or C++.
  12. # re: Zip a folder using PowerShell

    December 18, 2012 6:36 AM
    manish
    Gravatar
    is this command/function uses some any other tools/extensions? becauseit is working on my local machine but not on batch server i am using
  13. # re: Zip a folder using PowerShell

    January 15, 2013 5:33 PM
    Meg
    I had many folders to zip. I used your routine(added more time) and substituted:

    Remove-Item "C:\NotBackedUp\Fabrikam.zip"

    [IO.DirectoryInfo] $directory = GetItem "C:\NotBackedUp\Fabrikam"
    ZipFolder $directory

    WITH

    $folder ="C:\NotBackedUp"

    dir $folder -recurse | Where-Object { $_.PSIsContainer } | ForEach-Object {
    [IO.DirectoryInfo] $directory = Get-Item $_.FullName
    ZipFolder $directory }
  14. #  Automatically backup Hyper-V VMs using PowerShell | Adam Driscoll&amp;#039;s Blog

    February 4, 2013 11:46 AM
    Pingback/TrackBack
    Automatically backup Hyper-V VMs using PowerShell | Adam Driscoll&amp;#039;s Blog
  15. # re: Zip a folder using PowerShell

    June 3, 2013 12:16 PM
    RP
    Thank you for the script. It really helped me. Just wanted to share my experience.
    I got an error for initial run cause 100 ms were not enough to start zipping process. I was getting an error as 'Zip file is empty'.

    I then increased the sleep time to 500 ms and it worked for me. Also I would like to know how I can zip the files of a folder, without zip file having the actual folder inside it. The zip file should contain only the files to be zipped and not the actual folder the files were into.
  16. # re: Zip a folder using PowerShell

    July 2, 2013 2:04 AM
    Frédéric AUDON
    Gravatar
    Zip up a Folder with PowerShell 3 the Easy Way

    viziblr.com/...


    [Reflection.Assembly]::LoadWithPartialName( "System.IO.Compression.FileSystem" )
    $src_folder = "D:\stuff"
    $destfile = "D:\stuff.zip"
    $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
    $includebasedir = $false
    [System.IO.Compression.ZipFile]::CreateFromDirectory($src_folder,$destfile,$compressionLevel, $includebasedir )
  17. # Powershell Wait For Compressing To Finish | Click &amp;amp; Find Answer !

    July 8, 2013 12:35 AM
    Pingback/TrackBack
    Powershell Wait For Compressing To Finish | Click &amp;amp; Find Answer !
  18. # re: Zip a folder using PowerShell

    September 20, 2013 4:15 AM
    DatabaseJase
    Gravatar
    Hi Jeremy,

    Thank you for this great post which has really helped me out. I've posted a link on the Red Gate Deployment Manager forum to this post but wanted to ask if it's possible to attach a copy of your code just in case this post is lost in the future?

    www.red-gate.com/.../viewtopic.php?p=64782#64782

    I'm not affiliated with Red Gate but having scratched my head over this issue I know it could help others too.

    Regards

    Jason
  19. # re: Zip a folder using PowerShell

    September 20, 2013 4:39 AM
    Jeremy Jameson
    Gravatar
    Jason,

    I have no objections to copying the code into the forum post. All I ask is that you reference the original source (which you've already done).

    However, I can tell you that I don't expect the technologytoolbox.com site to go anywhere as long as I'm around. Personally, I find it very frustrating when I find a link to what looks to be a great resource but then get a 404 when I try to access the site.

    Regards,

    Jeremy
  20. # re: Zip a folder using PowerShell

    November 1, 2013 4:36 AM
    DatabaseJase
    Hi Jeremy,

    Thank you for the reply, which I only just remembered to go looking for so apologies for that, and appreciate that the site isn't going anywhere.

    I'll leave the link only for now since it's only fair to drive some traffic to your site plus the fact you might add to the code in the future making a fixed copy obsolete.

    Thanks again.

    Jason
  21. # re: Zip a folder using PowerShell

    January 21, 2014 8:01 AM
    Shaine MaGuire
    Gravatar
    Hello,

    I am using your script in mine to zip up files that are then sent to a third-party vendor. Your script works great; however, when the process runs, the ZIP file is created, but I get an error in ISE telling me that the ZIP file doesn't exist when in fact it does exist. Any suggestions?

    Thanks!
  22. # re: Zip a folder using PowerShell

    February 25, 2014 7:25 AM
    Alexander
    Gravatar
    Hello everybody! I'm also happy use this code, but i can't understand one strange code block:

    If ($_.Exception.Message.EndsWith(
    "it is being used by another process.") -eq $false)
    {
    Throw $_.Exception
    }

    If anybody knows what this hack means, it will be perfect if you share it with me. Thank you!
  23. # Powershell &amp;#8211; modify items in ZIP archive | Search RounD

    March 5, 2014 11:30 PM
    Pingback/TrackBack
    Powershell &amp;#8211; modify items in ZIP archive | Search RounD
  24. # re: Zip a folder using PowerShell

    April 2, 2014 2:42 AM
    Stuart
    Hi,

    This is great well done.

    Back to what Paul ^ was asking about zipping the contents of a folder and not having the top level folder zipped also. Can this be done? Its crucial to my project! I tried his fix but as he said this does not zip all files for some reason...

    Thanks for any help!
  25. # re: Zip a folder using PowerShell

    April 21, 2014 3:29 AM
    Ganu
    Gravatar
    Please help me for below problem.

    I have application folder which has all the dll and required files. I want to zip the folder and want to deploy to a specific location. Before deploying it should clean the folder and should deploy the zipped folder.

  26. # re: Zip a folder using PowerShell

    September 3, 2014 4:55 PM
    Jon Bryce
    Gravatar
    OP, thanks for posting - I also found it helpful and appreciate you sharing.

    One problem I found - if zipping a folder that contains just one item, you get an expected count of "1" and an actual count of '2' - and the appropriate error from the code.

    I did some research and checking: the cause is that the 'count' method only works on an array of objects. The lines...

    [int] $expectedCount = (Get-ChildItem $directory -Force -Recurse).Count
    $expectedCount += 1 # account for the top-level folder

    will return "1" when there is a file in the top-level folder.

    Fix is to force the count into an array. Something like...

    [int] $expectedcount =@(Get-ChildItem $directory -Force -Recurse).Count

    All credit to Karl Mitchke on the Vista64 forum for diagnosing the problem.

    Adding this comment here to help others who might also get caught on this (gomodo, this is your problem, I guess, on 21/Jun/12)

Add Comment

Optional, but recommended (especially if you have a Gravatar). Note that your email address will not appear with your comment.
If URL is specified, it will be included as a link with your name.

To prevent spam from being submitted, please select the following fruit: Apple

Apple
Pear
Strawberry
Watermelon
Cherries
Grapes
 
Please add 5 and 6 and type the answer here: