public/Compress-ZipArchive.ps1

using namespace System.IO
using namespace System.IO.Compression
using namespace System.Collections.Generic

function Compress-ZipArchive {
    <#
    .SYNOPSIS
    PowerShell function that overcomes the limitations of `Compress-Archive` while keeping similar pipeline capabilities.
 
    .DESCRIPTION
    `Compress-ZipArchive` intends to solve the present limitations of the built-in cmdlet `Compress-Archive`:
        1. Compression is limited to files below 2Gb
        2. It cannot compress files in use by another process, even though explorer can.
 
    .Parameter Path
    Specifies the path or paths to the files that you want to add to the archive zipped file.
    To specify multiple paths, and include files in multiple locations, use commas to separate the paths.
    This Parameter accepts wildcard characters. Wildcard characters allow you to add all files in a directory to your archive file.
 
    .Parameter LiteralPath
    Specifies the path or paths to the files that you want to add to the archive zipped file.
    Unlike the Path Parameter, the value of LiteralPath is used exactly as it's typed.
    No characters are interpreted as wildcards
 
    .Parameter DestinationPath
    The destination path to the Zip file.
    If the file name in DestinationPath doesn't have a `.zip` file name extension, the function appends the `.zip` file name extension.
 
    .Parameter CompressionLevel
    Define the compression level that should be used.
    See https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.compressionlevel for details.
 
    .Parameter Update
    Updates Zip entries and adds new entries to an existing Zip file.
 
    .Parameter Force
    Replaces an existing Zip file with a new one. All Zip contents will be lost.
 
    .Parameter PassThru
    Outputs the object representing the compressed file. The function produces no output by default.
 
    .EXAMPLE
    Get-ChildItem .\path -Recurse -Filter *.ext |
        Compress-ZipArchive -DestinationPath dest.zip
 
    Compress all `.ext` files from a specific folder.
 
    .EXAMPLE
    Compress-ZipArchive .\*\*.txt -DestinationPath dest.zip
 
    Compress all `.txt` files contained in all folders in the Current Directory.
 
    .EXAMPLE
    Compress-ZipArchive .\*.ext, .\*.ext2 -DestinationPath dest.zip
 
    Compress all `.ext` and `.ext2` from a specific folder.
 
    .EXAMPLE
    Compress-ZipArchive .\path -Destination myPath.zip -CompressionLevel Fastest
 
    Compress a folder using "Fastest" Compression Level.
 
    .EXAMPLE
    Get-ChildItem .\path -Recurse -Directory |
        Compress-ZipArchive -DestinationPath dest.zip
 
    Compressing all directories in ".\Path".
 
    .EXAMPLE
    Compress-ZipArchive -Path .\path -DestinationPath dest.zip -Force
 
    Replacing an existing Zip Archive.
 
    .EXAMPLE
    Get-ChildItem .\path -Recurse -Directory |
        Compress-ZipArchive -DestinationPath dest.zip -Update
 
    Adding and updating new entries to an existing Zip Archive.
 
    .LINK
    https://github.com/santisq/PSCompression
    #>


    [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]
        }
    }
}