Saturday 7 July 2012

PowerShell–Zip

Whenever writing automation scripts you often need to zip some files, either to upload them or just for back-up purposes.

I wanted a really simple and easy way I can do this repeatedly so I thought of creating a PowerShell script that I can reuse to do this easily. I thought that PowerShell's pipeline would be a perfect tool for this. So I wanted to use the dir command with all its powerful filters etc and then just pipe this out to a zip file something like this:

PowerShell Zip Usage
1
2
3
4
5
6
Push-Location c:\temp
# create new zip file
dir -Recurse c:\temp\stufftoZip1 | ToZip -relativeBaseDirectory (Get-Location).Path  -fileName "c:\temp\test.zip"
# append to zip file
dir -Recurse c:\temp\stufftoZip2 | ToZip -relativeBaseDirectory (Get-Location).Path -appendToZip -fileName "c:\temp\test.zip"
Pop-Location

And that is all you need to make use of it. You can use all the query operators like select, where, select, first, etc.

This also introduces the concept of Pipeline functions.

Pipeline

Each pipeline function has a begin, process and end. In the begin block is where we initialize our function. In the process block this is where you handle each item from the pipeline. The end block is where we clean-up like close files etc.

PowerShell Pipeline function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ToZip($fileName, $relativeBaseDirectory=$null, [switch] $appendToZip=$false, $verbose=$true)
{
    begin
    {      
        # initialization
        $zipFile = [System.IO.Packaging.Package]::Open($fileName, $mode)
    }
    process
    {      
        # $_.FullName is the current item (in this case file) in the pipeline.
    }
    end
    {      
        # finalization
        $zipFile.Close();
    }
}

Implementation

In order to use the System.IO.Packaging.Package namespace and create Zip files you have to import the assembly WindowsBase as follows:

1
[Reflection.Assembly]::LoadWithPartialName("WindowsBase") | Out-Null

Save the following to a file and call it Zip.psm1

Zip.psm1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Set-StrictMode -V 1.0
[Reflection.Assembly]::LoadWithPartialName("WindowsBase") | Out-Null
function ToZip($fileName, $relativeBaseDirectory=$null, [switch] $appendToZip=$false, $verbose=$true)
{
    begin
    {
        $zipCreated = { (Get-Variable -ErrorAction SilentlyContinue -Name zipFile) -ne $null }
        $logMessage = {
            param ($message)
            if ($verbose)
            {
                Write-Host  $message
            }
        }
        $mode = [System.IO.FileMode]::Create
        if ($appendToZip)
        {
            $mode = [System.IO.FileMode]::Open
        }
        $zipFile = [System.IO.Packaging.Package]::Open($fileName, $mode)
    }
    process
    {
        if  ((&$zipCreated) -and ([System.IO.File]::Exists($_.FullName) -eq $true))
        {
             
            $zipFileName = $_.FullName
            if ($relativeBaseDirectory -ne $null)          
            {
                #$directoryName = [System.IO.Path]::GetDirectoryName($_.FullName)
                $zipFileName = $_.FullName.SubString($relativeBaseDirectory.Length, $_.FullName.Length-$relativeBaseDirectory.Length)              
            }
             
             
            $destFilename = [System.IO.Path]::Combine(".\\", $zipFileName)
            #$destFilename = $destFilename.Replace(" ", "_")
            $uri = New-Object Uri -ArgumentList ($destFilename, [UriKind]::Relative)
            $uri = [System.IO.Packaging.PackUriHelper]::CreatePartUri($uri)
             
            &$logMessage ("Adding: {0}" -f $destFileName)
             
            if ($zipFile.PartExists($uri))
            {
                $zipFile.DeletePart($uri);
            }
             
            $part = $zipFile.CreatePart($uri, [string]::Empty, [System.IO.Packaging.CompressionOption]::Normal)
            $dest = $part.GetStream()
             
            $srcStream = New-Object System.IO.FileStream -ArgumentList ($_.FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
            try
            {
                $srcStream.CopyTo($dest)
            }
            finally
            {
                $srcStream.Close()
            }          
        }
    }
    end
    {
        if  (&$zipCreated)
        {
            $zipFile.Close()
        }
        &$logMessage "Done"
    }
}

Import & Usage

You can easily reuse the above module anywhere in your PowerShell scripts by importing it as follows:

PowerShell Zip Usage
1
2
3
4
5
6
7
8
9
10
Import-Module c:\Development\PowerShell\Zip.psm1
 
# Example 1
dir | ToZip c:\temp\test.zip
 
# Example 2
dir | ToZip -fileName c:\temp\test.zip -relativeBaseDirectory (Get-Location).Path
 
# Example 3
dir | ToZip -fileName c:\temp\test.zip -relativeBaseDirectory (Get-Location).Path -appendToZip

Example 1

Zip all the content in the current directory to c:\temp\test.zip

Example 2

Zip all the content in the current directory to c:\temp\test.zip. Using the folder structure starting at the current directory as relative path.

Example 3

Zip all the content in the current directory to c:\temp\test.zip. Using the folder structure starting at the current directory as relative path, and appending to the Zip.

Caveats

Actually there are none from PowerShell side, however the build in .net packaging library places a [Content_Types].xml in the zip file. You can't get rid of this. Also files with spaces will be escaped. This obviously can affect anything you zip and unzip where the file names are required. But again this is not PowerShell but the packaging library.

I will try and make a future post on a version that uses SharpZipLib so stay tuned...

4 comments:

  1. Im tryng it. looks pronising. thanks

    ReplyDelete
  2. Is it possible to password protect the zip file using a user prompted password?

    ReplyDelete
    Replies
    1. Unfortunately this doesn't seem to be possible with the packaging library.

      Delete
  3. I'm getting an "Access to the path C:\users\*****\Desktop is denied" error on line 20 of Zip.psm1. I'm the local admin on my computer, I have my execution policy set to unrestricted and I've launched PowerShell as an administrator. Not sure what else to try.

    ReplyDelete