A new way to zip and unzip in PowerShell 3 and .Net 4.5

I’ve got an article coming about WPF 4.5 and what’s new there for ShowUI, but in the meantime I thought I’d share this little nugget I noticed today: System.IO.Compression.FileSystem.ZipFile. You can use that class and it’s buddy ZipArchive to do all sorts of zipping and unzipping tasks.

In order to use these new classes, you have to use Add-Type to import the System.IO.Compression.FileSystem assembly. If you don’t understand that, don’t worry about it, just copy the first line of the examples below. You only have to do it once, so it might be worth sticking it in your profile (or in a module file with a couple of scripts) if you think you’ll do a lot of this sort of thing.

Add-Type -As System.IO.Compression.FileSystem
[IO.Compression.ZipFile]::CreateFromDirectory( (Split-Path $Profile), "WindowsPowerShell.zip", "Optimal", $true )
 

That’s basically a one-liner to zip up a folder. Of course, you should have a one liner to unzip it too:

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

$ZipFile = Get-Item ~\Downloads\Wasp.zip
$ModulePath = $Env:PSModulePath -split ";" -like "$Home*" | Select -first 1

[IO.Compression.ZipFile]::ExtractToDirectory( $ZipFile, $ModulePath )
 

In order for the second example to work the way you want it to, you need a zip file where all the files are in a single folder — which is why in the first example I used the overload for CreateFromDirectory which takes a boolean for whether or not to include the root directory in the zip as opposed to just it’s contents. Otherwise, you would need to create the “WASP” folder, first, and then extract to that directory. Of course, if you do that when there is a directory in the zip (as there was, in this case), you’ll end up with a Modules\WASP\WASP ... which in PS3 will work (although it shouldn’t), but is rather frustrating.

So, to avoid ending up with folders inside folders, we can use the ZipArchive class.

The easiest way to get an actual ZipArchive is to use the “Open” method on the ZipFile class. Once you’ve done that you can easily check all the files in it: $archive.Entries | Format-Table and you can extract a single entry using ExtractToFile or the whole archive using ExtractToDirectory

So for a final example, I’ll use ZipArchive to create a script that will always unzip to a new folder (unless there’s just one single file in the zip), and will create an “archive name” folder if there isn’t already a single folder root inside the archive. In fact, as you’ll see below, I went further and forced the resulting folder to always end up named after the archive.


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

function Expand-ZipFile {
  #.Synopsis
  #  Expand a zip file, ensuring it's contents go to a single folder ...
  [CmdletBinding()]
  param(
    # The path of the zip file that needs to be extracted
    [Parameter(ValueFromPipelineByPropertyName=$true, Position=0, Mandatory=$true)]
    [Alias("PSPath")]
    $FilePath,

    # The path where we want the output folder to end up
    $FolderPath = $Pwd,

    # Make sure the resulting folder is always named the same as the archive
    $Force
  )
  process {
    $ZipFile = Get-Item $FilePath
    $Archive = [System.IO.Compression.ZipFile]::Open( $ZipFile, "Read" )
    # The place where we expect it to end up
    $Destination = join-path $FolderPath $ZipFile.BaseName
    # The root folder of the first entry ...
    $ArchiveRoot = $Archive.Entries[0].FullName.split("/",2)[0]

    # If any of the files are not in the same root folder ...
    if($Archive.Entries.FullName | Where-Object { $_.split("/",2)[0] -ne $ArchiveRoot }) {
      # extract it into a new folder:
      New-Item $Destination -Type Directory -Force
      [System.IO.Compression.ZipFileExtensions]::ExtractToDirectory( $Archive, $Destination )
    } else {
      # otherwise, extract it to the FolderPath
      [System.IO.Compression.ZipFileExtensions]::ExtractToDirectory( $Archive, $FolderPath )

      # If there was only a single file in the archive, then we'll just output that file...
      if($Archive.Entries.Count -eq 1) {
        Get-Item (Join-Path $FolderPath $Archive.Entries[0].FullName)
      } elseif($Force) {
        # Otherwise let's make sure that we move it to where we expect it to go, in case the zip's been renamed
        if($ArchiveRoot -ne $ZipFile.BaseName) {
          Move-Item (join-path $FolderPath $ArchiveRoot) $Destination
          Get-Item $Destination
        }
      } else {
        Get-Item (Join-Path $FolderPath $ArchiveRoot)
      }
    }

    $Archive.Dispose()
  }
}
 

Update and continuation

I just posted a new version of this to PoshCode including a New-ZipFile function, just because I can’t stop myself, and I’ve always wanted a zip function that behaved exactly the way I wanted it to. If anyone want’s to add to that ZipFile module, that would be the place to do it, for now. Here’s the current version:

Similar Posts:

3 thoughts on “A new way to zip and unzip in PowerShell 3 and .Net 4.5”

  1. Really great article! I stumbled through making Zip files and folders using this class but it’s nice to have a readable explanation.

    1 question, why use add-type instead of loadwithpartialname()? I like Add-Type myself but I was wondering why you picked it.

  2. 3 reasons, in no particular order:

    • It’s less typing. If there was a built-in alias for Add-Type it would be even more awesome.
    • LoadWithPartialName is deprecated, and we’ve really got to stop using it at some point.
    • Add-Type resolves the partial name using an official list of full names.

    That last point is important. The Add-Type cmdlet actually has a hard-coded list of the FullNames of the assemblies they ship, with the PublicKeyToken and everything. When you specify a partial name, they resolve it to the full name … and then they call Load with the full name. That means that you can’t get the wrong assembly. Even if someone’s intentionally tried to hijack your system and registered another “System.IO.Compression.FileSystem” assembly.

    For what it’s worth, the full name is “System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089” ... and you could put that in the call to Add-Type if you wanted to.

Comments are closed.