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.

TecMan
  • 2,743
  • 2
  • 30
  • 64
  • The first I can think of, though, highly inefficient would be to `Copy-Item` first and then compress to maintain the hierarchy. – Santiago Squarzon Nov 30 '21 at 15:31
  • 2
    I tend to avoid `-Exclude` because it is very unintuitive to use, see [this answer](https://stackoverflow.com/a/38308796/7571258). 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 . – zett42 Nov 30 '21 at 15:42
  • 1
    With `-Recurse` the hierarchy is globally preserved but files are duplicated. I would use .NET [ZipArchive.CreateEntry](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.ziparchive.createentry?view=net-6.0) and a recursive function to achieve it in PowerShell as you expect. – Hazrelle Nov 30 '21 at 15:50
  • This answer might be a good place to start. https://stackoverflow.com/a/46448068/447901 – lit Nov 30 '21 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? – TecMan Nov 30 '21 at 15:57
  • Actually you are right. When passing file names to `Compress-Archive` (as with `Get-ChildItem -recurse`), you loose directory hierarchy. This is mentioned in the [examples section](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.archive/compress-archive?view=powershell-7.2#examples) of the docs. I would go with `ZipArchive` as recommended by @Hazrelle. – zett42 Nov 30 '21 at 16:01

1 Answers1

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
Rich Moss
  • 2,195
  • 1
  • 13
  • 18
  • 1
    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? – TecMan Dec 02 '21 at 16:26
  • 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 `.` – Rich Moss Dec 02 '21 at 18:36
  • 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 :) – Rich Moss Dec 02 '21 at 19:00