compress-archive and preserve relative paths

13,923

Solution 1

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,

Solution 2

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};

Solution 3

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()

Solution 4

The cumbersome technique mklement0 mentioned worked for me. Below is the script I created to support a list of various files mixed with folders.

# Compress LFS based files into a zip
# To use
#  1. place this script in the root folder
#  2. modify the contents of $lfsAssetFiles to point to files relative to this root folder
#  3. modify $zipDestination to be where you want the resultant zip to be placed
# based off of https://stackoverflow.com/a/51394271

# this should match files being .gitignored
$lfsAssetFiles = 
"\Assets\Project\Plugins\x32",
"\Assets\Project\Plugins\x64\HugePlugin.dll"

# This is where the contents of the zip file will be structured, because placing them inside of a specific folder of the zip is difficult otherwise
$zipStruct = $PSScriptRoot + "\zipStruct"

# the actual zip file that will be created
$zipDestination = "C:\Dropbox\GitLfsZip\ProjectNameLfs.zip"

# remove files from previous runs of this script
If(Test-path $zipStruct) {Remove-item $zipStruct -Recurse}
If(Test-path $zipDestination) {Remove-item $zipDestination}

Foreach ($entry in $lfsAssetFiles)
{
  # form absolute path to source each file to be included in the zip
  $sourcePath = $PSScriptRoot + $entry;

  # get the parent directories of the path. If the entry itself is a directory, we still only need the parent as the directory will be created when it is copied over.
  $entryPath = Split-Path -Parent $entry

  # form what the path will look like in the destination
  $entryPath = $zipStruct + $entryPath

  # ensure the folders to the entry path exist
  $createdPath = New-Item -Force -ItemType Directory $entryPath

  # copy the file or directory
  Copy-Item -Recurse -Force $sourcePath $createdPath
}

# create a zip file https://blogs.technet.microsoft.com/heyscriptingguy/2015/page/59/
Add-Type -AssemblyName "system.io.compression.filesystem"
[io.compression.zipfile]::CreateFromDirectory($zipStruct, $zipDestination)
# Compress-Archive doesn't work here because it includes the "zipStruct" folder: Compress-Archive -Path $zipStruct -DestinationPath $zipDestination
Share:
13,923
viplove
Author by

viplove

Maintaining and supporting in-house web apps and services with the .NET stack for a fitness company. C#, ASP.NET, MS SQL, & NetSuite dev-ops

Updated on June 06, 2022

Comments

  • viplove
    viplove almost 2 years

    I'm having a challenging time getting compress-archive to do what I want...

    I have a root project folder and I want to zip up some of the files in sub-directories and preserve the relative paths. For example: / ├── _scripts ├── ├─_module1 | | └── filex.js | └─_module2 | ├── file1.js | └── file2.txt

    So from my root directory, i'd like to create a zip file that includes module2/*, and i want to keep the folder structure. I'd like my zip to contain: scripts/module2/file1.js scripts/module2/file2.txt

    But when I run this from the root folder: Compress-Archive -Path "scripts\module2\*" -DestinationPath tmp.zip

    The contents of the zip file only contain: /file1.js /file2.txt

  • viplove
    viplove about 5 years
    Thanks. This is essentially what I ended up doing. Creating a temp folder, copying the files into it to place them int the correct path. Then zipping that temp folder up.
  • Rich Moss
    Rich Moss over 4 years
    I agree with this process - it's faster to avoid temp files and better to preserve the relative path. I've written a similar script with some error handling to report progress and skipped files, and always Dispose() the ZipFile object to make it readable. Your 'clean up' section may have some side effects if there are other IDisposable variables in the RunSpace that are not ZipFiles. .
  • Admin
    Admin over 4 years
    It would be great if someone could make a commandlet for this. I have a linux background and basically what I was looking for was something like a "tar czf". About the cleanup: yes, I'm a powershell novice user and dont know/understand it very well yet. Thanks for the advice
  • Rich Moss
    Rich Moss over 4 years
    The two functions in this answer might be what you're looking for.
  • Rich Moss
    Rich Moss about 2 years
    I would add ^ to this line $rname = $(Resolve-Path -Path $fname -Relative) -replace '^\.\\','' to prevent incorrect results for paths like this: .\test.\test.txt