3

I need to archive a folder without some subfolders and files using PowerShell. My file/folder exclusions can occur on any level of hierarchy. To explain, here is a simple example for a WinForms VS project. If we open it in VS and build, VS creates the bin/obj subfolders with executable contents, the hidden .vs folder with user settings, and maybe *.user files for the projects included into the solution. I want to archive such a VS solution folder without all those file and folder items that can be recreated the next time when we build the solution.

It is done very easily with 7-Zip using its -x! command line switch:

"C:\Program Files\7-Zip\7z.exe" a -tzip "D:\Temp\WindowsFormsApp1.zip" "D:\Temp\WindowsFormsApp1\"  -r -x!bin -x!obj -x!.vs -x!*.suo -x!*.user

However, I couldn't build an equivalent PowerShell script. The best thing I got was something like this:

$exclude = "bin", "obj", ".vs", "*.suo", "*.user"
$files = Get-ChildItem -Path $path -Exclude $exclude -Force
Compress-Archive -Path $files -DestinationPath $dest -Force

If I execute this script, the exclusion list works only for the subfolders of the first hierarchy level. If I add the -Recurse switch to the Get-ChildItem cmdlet in my script or try to filter the files/folders using Where-Object, I lose the folder hierarchy in the archive.

Is there a solution to my problem? I need to solve the problem using solely PowerShell without any external tools.

6
  • The first I can think of, though, highly inefficient would be to Copy-Item first and then compress to maintain the hierarchy. Commented Nov 30, 2021 at 15:31
  • 2
    I tend to avoid -Exclude because it is very unintuitive to use, see this answer. Using Where-Object is more straightforward. I'm quite sure it could be used without loosing folder hierarchy. If all else fails, use the advise given by @SantiagoSquarzon . Commented Nov 30, 2021 at 15:42
  • 1
    With -Recurse the hierarchy is globally preserved but files are duplicated. I would use .NET ZipArchive.CreateEntry and a recursive function to achieve it in PowerShell as you expect. Commented Nov 30, 2021 at 15:50
  • This answer might be a good place to start. stackoverflow.com/a/46448068/447901 Commented Nov 30, 2021 at 15:55
  • @zett42, Where-Object does its work, but only for the folders of the first hierarchy level. If I add the -Recurse switch, I lose the hierarchy. Maybe, I do something wrong. Any example from you? Commented Nov 30, 2021 at 15:57

1 Answer 1

2

This is a similar problem to how-to-compress-log-files-older-than-30-days-in-windows.

The ArchiveOldLogs.ps1 script will preserve folder structure without the need for intermediate copying.

You can change the -Filter parameter to exclude certain files by name rather than date:

$filter = {($_.Name -notlike '*.vs') -and ($_.Name -notlike '*.suo') -and ($_.Name -notlike '*.user') -and ($_.FullName -notlike '*bin\*') -and ($_.FullName -notlike '*obj\*')}
.\ArchiveOldLogs.ps1 -FileSpecs @('*.*') -Filter $filter -DeleteAfterArchiving:$false

Here's a minimal example that doesn't include the fancy progress bar, doesn't prevent duplicates within the archive, and doesn't delete archived files:

$ParentFolder = 'C:\projects\Code\' #files will be stored with a path relative to this folder
$ZipPath = 'c:\temp\projects.zip' #the zip file should not be under $ParentFolder or an exception will be raised
$filter = {($_.Name -notlike '*.vs') -and ($_.Name -notlike '*.suo') -and ($_.Name -notlike '*.user') -and ($_.FullName -notlike '*bin\*') -and ($_.FullName -notlike '*obj\*')}
@( 'System.IO.Compression','System.IO.Compression.FileSystem') | % { [void][Reflection.Assembly]::LoadWithPartialName($_) }
Push-Location $ParentFolder #change to the parent folder so we can get $RelativePath
$FileList = (Get-ChildItem '*.*' -File -Recurse | Where-Object $Filter) #use the -File argument because empty folders can't be stored
Try{
    $WriteArchive = [IO.Compression.ZipFile]::Open( $ZipPath,'Update')
    ForEach ($File in $FileList){
        $RelativePath = (Resolve-Path -LiteralPath "$($File.FullName)" -Relative) -replace '^.\\' #trim leading .\ from path 
        Try{    
            [IO.Compression.ZipFileExtensions]::CreateEntryFromFile($WriteArchive, $File.FullName, $RelativePath, 'Optimal').FullName
        }Catch{ #Single file failed - usually inaccessible or in use
            Write-Warning  "$($File.FullName) could not be archived. `n $($_.Exception.Message)"  
        }
    }
}Catch [Exception]{ #failure to open the zip file
    Write-Error $_.Exception
}Finally{
    $WriteArchive.Dispose() #always close the zip file so it can be read later 
}
Pop-Location
Sign up to request clarification or add additional context in comments.

3 Comments

It works almost flawlessly. The only problem is that the beginning dots in filenames are removed. For example, .gitignore and .gitattributes turn into gitignore and gitattributes in the archive. Do we really need .TrimStart(".\") in the assignment to $RelativePath in the code above?
Good catch! I'd never noticed that behavior in prior testing of ArchiveOldLogs.ps1. I remove the leading .\ to make a zip file with relative paths identical to one where you dragged a folder from Windows Explorer into a zip. If you remove the .TrimStart(".\") it produces a zip file that some readers (Beyond Compare for example) will treat as having all folders nested one level deeper, under a folder named .
Thanks for catching that bug - I've been using this script to zip logs for years and never encountered the .gitignore behavior. The updated script in this answer should address that problem. Now I'm off to fix it everywhere else :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.