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

td1*_*126 8 powershell zip system.io.compression compress-archive system.io.compression.zipfile

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

编辑:

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

San*_*zon 8

笔记

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

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


解释PowerShell 文档Compress-Archive中指定的限制:

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

发生这种情况的原因可能是(因为 5.1 版本是闭源的),此 cmdlet在将 zip 存档写入文件之前使用内存流所有 zip 存档条目保存在内存中。检查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 )
Run Code Online (Sandbox Code Playgroud)

如果我们尝试从大于 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."
Run Code Online (Sandbox Code Playgroud)

巧合的是,我们看到了同样的限制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
Run Code Online (Sandbox Code Playgroud)

为了克服这个问题,并在压缩另一个进程使用的文件时模拟资源管理器的行为,下面发布的函数将默认在[FileShare] 'ReadWrite, Delete'打开FileStream.


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

  • 简单的解决方法是使用ZipFile.CreateFromDirectoryMethod。使用此静态方法有 3 个限制:
    1. 必须是目录,不能压缩单个文件。
    2. 源文件夹中的所有文件(递归地)都将被压缩,我们无法选择/过滤文件进行压缩。
    3. 无法更新现有 Zip 存档的条目。

值得注意的是,如果您需要在 Windows PowerShell (.NET Framework) 中使用该类ZipFile则必须引用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)
Run Code Online (Sandbox Code Playgroud)

此函数应该能够像ZipFile.CreateFromDirectory方法一样处理压缩,但也允许过滤要压缩的文件和文件夹,同时保持文件/文件夹结构不变

文档以及使用示例可以在此处找到。

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]
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 仅供参考:MS 刚刚更新了他们的代码(测试版)以提供不再有 2GB 限制的 x64 实现:https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/19 (2认同)