compress-archive and preserve relative paths

It is a bit old thread, but I think this will help folks to create zip files through PowerShell 5.1, which is standard with Windows 10 installations these days. Script allows you to keep original subdirectory structure as well as to exclude some unnecessary subtrees / files. This is what I use to archive source code of my Visual Studio solutions:

Write-Output "Zipping Visual Studio solution..."

# top level from where to start and location of the zip file
$path = "C:\TheSolution"
# top path that we want to keep in the source code zip file
$subdir = "source\TheSolution"
# location of the zip file
$ZipFile = "${path}\TheSolution.zip"

# change current directory
Set-Location "$path"

# collecting list of files that we want to archive excluding those that we don't want to preserve
$Files  = @(Get-ChildItem "${subdir}" -Recurse -File | Where-Object {$_.PSParentPath -inotmatch "x64|packages|.vs|Win32"})
$Files += @(Get-ChildItem "${subdir}\packages" -Recurse -File)
$Files += @(Get-ChildItem "${subdir}\.git" -Recurse -File)
$FullFilenames = $files | ForEach-Object -Process {Write-Output -InputObject $_.FullName}

# remove old zip file
if (Test-Path $ZipFile) { Remove-Item $ZipFile -ErrorAction Stop }

#create zip file
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::Open(($ZipFile), [System.IO.Compression.ZipArchiveMode]::Create)

# write entries with relative paths as names
foreach ($fname in $FullFilenames) {
    $rname = $(Resolve-Path -Path $fname -Relative) -replace '\.\\',''
    Write-Output $rname
    $zentry = $zip.CreateEntry($rname)
    $zentryWriter = New-Object -TypeName System.IO.BinaryWriter $zentry.Open()
    $zentryWriter.Write([System.IO.File]::ReadAllBytes($fname))
    $zentryWriter.Flush()
    $zentryWriter.Close()
}

# release zip file
$zip.Dispose()

It appears that Compress-Archive (as of Windows PowerShell v5.1) doesn't support what you want:

Targeting a folder recursively adds that folder's subtree to the archive, but only by the target folder's name (which becomes a child folder inside the archive), not its path.

Specifically,

Compress-Archive -Path scripts\module2 -DestinationPath tmp.zip

will (recursively) store the contents of scripts\module2 in tmp.zip, but not with archive-internal path .\scripts\module2, just with .\module2 - the target folder's name (the last input path component).

The implication is that you'd have to pass folder scripts instead to get the desired archive-internal path, but that would invariably include the entire subtree of scripts, given that Compress-Archive offers no inclusion/exclusion mechanism.


One - cumbersome - option is to recreate the desired hierarchy in, say, the $env:TEMP folder, copy the target folder there, run Compress-Archive against the root of the recreated hierarchy, and then clean up:

New-Item -Force -ItemType Directory $env:TEMP/scripts
Copy-Item -Recurse -Force scripts/module2 $env:TEMP/scripts
Compress-Archive -LiteralPath $env:TEMP/scripts -DestinationPath tmp.zip
Remove-Item $env:TEMP/Scripts -Recurse -Whatif

Otherwise, you may be able to find a solution:

  • by using the .NET v4.5+ [System.IO.Compression.ZipFile] class directly; you can load it into your session with Add-Type -Assembly System.IO.Compression.FileSystem (not necessary in PowerShell Core).

  • by using external programs such as 7-Zip,


I wanted to do this without having to copy the full structure to a temp directory.

#build list of files to compress
$files = @(Get-ChildItem -Path .\procedimentos -Recurse | Where-Object -Property Name -EQ procedimentos.xlsx);
$files += @(Get-ChildItem -Path .\procedimentos -Recurse | Where-Object -Property Name -CLike procedimento_*_fs_*_d_*.xml);
$files += @(Get-ChildItem -Path .\procedimentos -Recurse | Where-Object -Property FullName -CLike *\documentos_*_fs_*_d_*);

# exclude directory entries and generate fullpath list
$filesFullPath = $files | Where-Object -Property Attributes -CContains Archive | ForEach-Object -Process {Write-Output -InputObject $_.FullName}

#create zip file
$zipFileName = 'procedimentos.zip'
$zip = [System.IO.Compression.ZipFile]::Open((Join-Path -Path $(Resolve-Path -Path ".") -ChildPath $zipFileName), [System.IO.Compression.ZipArchiveMode]::Create)

#write entries with relative paths as names
foreach ($fname in $filesFullPath) {
    $rname = $(Resolve-Path -Path $fname -Relative) -replace '\.\\',''
    echo $rname
    $zentry = $zip.CreateEntry($rname)
    $zentryWriter = New-Object -TypeName System.IO.BinaryWriter $zentry.Open()
    $zentryWriter.Write([System.IO.File]::ReadAllBytes($fname))
    $zentryWriter.Flush()
    $zentryWriter.Close()
}

# clean up
Get-Variable -exclude Runspace | Where-Object {$_.Value -is [System.IDisposable]} | Foreach-Object {$_.Value.Dispose(); Remove-Variable $_.Name};

Tags:

Powershell

Zip