使用PowerShell存档文件夹时排除某些子文件夹和文件

3
我需要使用PowerShell归档一个文件夹,但要排除某些子文件夹和文件。我的文件/文件夹排除可以出现在任何层次结构中。为了解释这一点,以WinForms VS项目为例进行简单说明。如果我们在VS中打开并构建它,VS会创建带有可执行内容的bin/obj子文件夹,带有用户设置的隐藏.vs文件夹,以及可能是解决方案包含的*.user文件。我想对这样一个VS解决方案文件夹进行归档,而不包括那些可以在下次构建解决方案时重新创建的文件和文件夹项目。
使用7-Zip很容易完成此操作,只需使用其-x!命令行开关即可。
"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

然而,我无法编写出等效的PowerShell脚本。我最好得到的东西是下面这样的:

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

执行此脚本后,排除列表仅适用于第一层级别的子文件夹。如果在我的脚本中添加“-Recurse”开关到Get-ChildItem cmdlet或尝试使用Where-Object过滤文件/文件夹,则会在归档中丢失文件夹层次结构。
是否有解决方案? 我需要仅使用PowerShell解决问题,不使用任何外部工具。

我能想到的第一种方法虽然效率非常低下,但是可以先使用Copy-Item复制文件,然后再压缩以保持层次结构。 - Santiago Squarzon
2
我倾向于避免使用-Exclude,因为它非常不直观,可以参考这个答案。使用Where-Object更加简单明了。我相信它可以在不丢失文件夹层次结构的情况下使用。如果一切都失败了,请使用@SantiagoSquarzon提供的建议。 - zett42
1
使用-Recurse参数可以全局保留层次结构,但会复制文件。我建议在PowerShell中使用.NET ZipArchive.CreateEntry和递归函数来实现您的期望。 - Hazrelle
这个答案可能是一个很好的起点。https://stackoverflow.com/a/46448068/447901 - lit
@zett42,“Where-Object”可以工作,但仅适用于第一层次级别的文件夹。如果我添加“-Recurse”开关,我会失去层次结构。也许我做错了什么。你有任何示例吗? - TecMan
实际上你是对的。当将文件名传递给 Compress-Archive 时(就像 Get-ChildItem -recurse 一样),你会丢失目录层次结构。这在文档的 示例部分 中有提到。我会按照 @Hazrelle 的建议使用 ZipArchive - zett42
1个回答

2
这是一个类似于如何在Windows中压缩30天之前的日志文件的问题。 ArchiveOldLogs.ps1脚本将保留文件夹结构,无需进行中间复制。
您可以更改-Filter参数以按名称而非日期排除某些文件:
$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

这是一个简单的示例,不包含花哨的进度条,不会在存档中防止重复,并且不会删除已存档的文件:
$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

1
它几乎完美地工作。唯一的问题是文件名中的起始点被删除了。例如,.gitignore.gitattributes 在存档中变成了 gitignoregitattributes。我们真的需要在上面的代码中对 $RelativePath 进行 .TrimStart(".\") 分配吗? - TecMan
好的发现!我以前在测试ArchiveOldLogs.ps1时从未注意到这种行为。 我删除了前导的.\,以便使用相对路径创建一个与从Windows资源管理器中将文件夹拖入zip文件时相同的zip文件。 如果您删除.TrimStart(".\"),它会生成一个zip文件,一些读者(例如Beyond Compare)将把所有文件夹嵌套在名为“.”的文件夹下面。 - Rich Moss
谢谢你发现了那个 bug - 我用这个脚本压缩日志已经好几年了,从来没有遇到过 .gitignore 的问题。这个回答中更新的脚本应该解决了这个问题。现在我要去其他地方修复它 :) - Rich Moss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接