如何使用 powershell 压缩超过 2 GB 的文件?

问题描述 投票:0回答:1

我正在开发一个项目来压缩从几个 mb 到几个 GB 大小的文件,我正在尝试使用 powershell 将它们压缩成 .zip。我遇到的主要问题是使用压缩存档对单个文件大小有 2 GB 上限,我想知道是否有另一种方法来压缩文件。

编辑:

因此,对于这个项目,我们希望实现一个系统,从 Outlook 获取 .pst 文件并将其压缩为 .zip 并将其上传到服务器。上传后,它们将从新设备上拉下来并再次提取到 .pst 文件中。

powershell zip system.io.compression compress-archive system.io.compression.zipfile
1个回答
7
投票

注意

此功能的进一步更新将发布到官方 GitHub 存储库 以及 PowerShell Gallery。此答案中的代码将不再维护

非常欢迎贡献,如果您想贡献,请分叉存储库并提交包含更改的拉取请求。


解释 PowerShell 文档中指定的

Compress-Archive
的限制:

Compress-Archive
cmdlet 使用 Microsoft .NET API
System.IO.Compression.ZipArchive
来压缩文件。由于底层 API 的限制,最大文件大小为 2 GB。

发生这种情况是因为此 cmdlet 使用“内存流”将字节保存在内存中,然后将它们写入文件。检查 cmdlet 生成的

InnerException,我们可以看到: System.IO.IOException: Stream was too long. at System.IO.MemoryStream.Write(Byte[] buffer, Int32 offset, Int32 count) at CallSite.Target(Closure , CallSite , Object , Object , Int32 , Object )

如果我们尝试从大于 2Gb 的文件中读取所有字节,我们也会看到类似的问题

Exception calling "ReadAllBytes" with "1" argument(s): "The file is too long. This operation is currently limited to supporting files less than 2 gigabytes in size." 巧合的是,我们也看到了同样的限制

System.Array

:

 仅限 .NET Framework:默认情况下,数组的最大大小为 2 GB。

这个问题
中还指出了另一个限制,
Compress-Archive

如果另一个进程拥有文件句柄,则无法压缩。 如何重现?

# cd to a temporary folder and
# start a Job which will write to a file
$job = Start-Job {
    0..1000 | ForEach-Object {
        "Iteration ${_}:" + ('A' * 1kb)
        Start-Sleep -Milliseconds 200
    } | Set-Content .\temp\test.txt
}

Start-Sleep -Seconds 1
# attempt to compress
Compress-Archive .\temp\test.txt -DestinationPath test.zip
# Exception:
# The process cannot access the file '..\test.txt' because it is being used by another process.
$job | Stop-Job -PassThru | Remove-Job
Remove-Item .\temp -Recurse

为了克服这个问题,并在压缩另一个进程使用的文件时模拟资源管理器的行为,下面发布的函数在打开

[FileShare] 'ReadWrite, Delete'

时将默认为

FileStream
要解决此问题,有两种解决方法:


简单的解决方法是使用

ZipFile.CreateFromDirectory
    方法
  • 。使用此静态方法有 3 个限制:
    来源
    必须是目录
    ,单个文件无法压缩。
    1. 源文件夹中的所有文件(递归地)将被压缩,我们无法选择/过滤文件进行压缩。
    2. 无法更新现有 Zip 存档的条目。
    3. 值得注意的是,如果您需要在 Windows PowerShell (.NET Framework) 中使用
  • ZipFile
Class

,则必须引用 System.IO.Compression.FileSystem

。请参阅内嵌评论。
# Only needed if using Windows PowerShell (.NET Framework): Add-Type -AssemblyName System.IO.Compression.FileSystem [IO.Compression.ZipFile]::CreateFromDirectory($sourceDirectory, $destinationArchive)

自己编写代码的解决方法是使用一个函数来完成创建
ZipArchive文件/文件夹结构不变

可以在此处

找到文档以及使用示例。

using namespace System.IO using namespace System.IO.Compression using namespace System.Collections.Generic Add-Type -AssemblyName System.IO.Compression function Compress-ZipArchive { [CmdletBinding(DefaultParameterSetName = 'Path')] [Alias('zip', 'ziparchive')] param( [Parameter(ParameterSetName = 'PathWithUpdate', Mandatory, Position = 0, ValueFromPipeline)] [Parameter(ParameterSetName = 'PathWithForce', Mandatory, Position = 0, ValueFromPipeline)] [Parameter(ParameterSetName = 'Path', Mandatory, Position = 0, ValueFromPipeline)] [string[]] $Path, [Parameter(ParameterSetName = 'LiteralPathWithUpdate', Mandatory, ValueFromPipelineByPropertyName)] [Parameter(ParameterSetName = 'LiteralPathWithForce', Mandatory, ValueFromPipelineByPropertyName)] [Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipelineByPropertyName)] [Alias('PSPath')] [string[]] $LiteralPath, [Parameter(Position = 1, Mandatory)] [string] $DestinationPath, [Parameter()] [CompressionLevel] $CompressionLevel = [CompressionLevel]::Optimal, [Parameter(ParameterSetName = 'PathWithUpdate', Mandatory)] [Parameter(ParameterSetName = 'LiteralPathWithUpdate', Mandatory)] [switch] $Update, [Parameter(ParameterSetName = 'PathWithForce', Mandatory)] [Parameter(ParameterSetName = 'LiteralPathWithForce', Mandatory)] [switch] $Force, [Parameter()] [switch] $PassThru ) begin { $DestinationPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath) if([Path]::GetExtension($DestinationPath) -ne '.zip') { $DestinationPath = $DestinationPath + '.zip' } if($Force.IsPresent) { $fsMode = [FileMode]::Create } elseif($Update.IsPresent) { $fsMode = [FileMode]::OpenOrCreate } else { $fsMode = [FileMode]::CreateNew } $ExpectingInput = $null } process { $isLiteral = $false $targetPath = $Path if($PSBoundParameters.ContainsKey('LiteralPath')) { $isLiteral = $true $targetPath = $LiteralPath } if(-not $ExpectingInput) { try { $destfs = [File]::Open($DestinationPath, $fsMode) $zip = [ZipArchive]::new($destfs, [ZipArchiveMode]::Update) $ExpectingInput = $true } catch { $zip, $destfs | ForEach-Object Dispose $PSCmdlet.ThrowTerminatingError($_) } } $queue = [Queue[FileSystemInfo]]::new() foreach($item in $ExecutionContext.InvokeProvider.Item.Get($targetPath, $true, $isLiteral)) { $queue.Enqueue($item) $here = $item.Parent.FullName if($item -is [FileInfo]) { $here = $item.Directory.FullName } while($queue.Count) { try { $current = $queue.Dequeue() if($current -is [DirectoryInfo]) { $current = $current.EnumerateFileSystemInfos() } } catch { $PSCmdlet.WriteError($_) continue } foreach($item in $current) { try { if($item.FullName -eq $DestinationPath) { continue } $relative = $item.FullName.Substring($here.Length + 1) $entry = $zip.GetEntry($relative) if($item -is [DirectoryInfo]) { $queue.Enqueue($item) if(-not $entry) { $entry = $zip.CreateEntry($relative + '\', $CompressionLevel) } continue } if(-not $entry) { $entry = $zip.CreateEntry($relative, $CompressionLevel) } $sourcefs = $item.Open([FileMode]::Open, [FileAccess]::Read, [FileShare] 'ReadWrite, Delete') $entryfs = $entry.Open() $sourcefs.CopyTo($entryfs) } catch { $PSCmdlet.WriteError($_) } finally { $entryfs, $sourcefs | ForEach-Object Dispose } } } } } end { $zip, $destfs | ForEach-Object Dispose if($PassThru.IsPresent) { $DestinationPath -as [FileInfo] } } }

© www.soinside.com 2019 - 2024. All rights reserved.