Search code examples
powershellzip

Expand-Archive without Importing and Exporting files


How do I stop exporting then importing files in and out of Powershell when working with .zip files (Expand-Archive)?

I am currently using a temporary folder to extract the .zip file. Is there a variable or something I missed that would work better than the solution below?

$filename = 'foobar'
$Zip_in_Bytes | Set-Content -Encoding Byte -Path "C:\temp\filename.zip"
Expand-Archive -Path "C:\temp\filename.zip" -DestinationPath "C:\temp\" -Force
[xml]$xml = Get-Content -Path "C:\temp\filename.xml"

Remove-Item "C:\temp\filename.zip"
Remove-Item "C:\temp\filename.xml"

Expand-Archive only support paths parameters, not objects Is there a better way to handle .zip files?


Solution

  • Using System.IO.Compression you can work with byte arrays and streams rather than temporary files, but it's a bit more work than Expand-Archive.

    EDIT: Added Get-ZipEntryContent and Add-ZipEntry sample calls, and tweaked parameters making $ZipFilePath optional.

    @( 'System.IO.Compression','System.IO.Compression.FileSystem') | % { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
    
    function Get-ZipEntryContent(#returns the bytes of the first matching entry 
      [string] $ZipFilePath, #optional - specify a ZipStream or path 
      [IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::Open)),
      [string] $EntryPath){
        $ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Read)
        $buf = New-Object byte[] (0) #return an empty byte array if not found
        $ZipArchive.GetEntry($EntryPath) | ?{$_} | %{ #GetEntry returns first matching entry or null if there is no match
            $buf = New-Object byte[] ($_.Length)
            Write-Verbose "     reading: $($_.Name)"
            $_.Open().Read($buf,0,$buf.Length)
        }
        $ZipArchive.Dispose()
        $ZipStream.Close()
        $ZipStream.Dispose()
        return $buf 
    }
    
    
    function Add-ZipEntry(#Adds an entry to the $ZipStream. Sample call: Add-ZipEntry -ZipFilePath "$PSScriptRoot\temp.zip" -EntryPath Test.xml -Content ([text.encoding]::UTF8.GetBytes("Testing"))
      [string] $ZipFilePath, #optional - specify a ZipStream or path 
      [IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::OpenOrCreate)),
      [string] $EntryPath, 
      [byte[]] $Content, 
      [switch] $OverWrite, #if specified, will not create a second copy of an existing entry 
      [switch] $PassThru ){#return a copy of $ZipStream
        $ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Update, $true)
        $ExistingEntry = $ZipArchive.GetEntry($EntryPath) | ?{$_} 
        If($OverWrite -and $ExistingEntry){
            Write-Verbose "    deleting existing $($ExistingEntry.FullName)"
            $ExistingEntry.Delete()
        }
        $Entry = $ZipArchive.CreateEntry($EntryPath)
        $WriteStream = New-Object System.IO.StreamWriter($Entry.Open())
        $WriteStream.Write($Content,0,$Content.Length)
        $WriteStream.Flush()
        $WriteStream.Dispose()
        $ZipArchive.Dispose()
        If($PassThru){
            $OutStream = New-Object System.IO.MemoryStream
            $ZipStream.Seek(0, 'Begin') | Out-Null
            $ZipStream.CopyTo($OutStream)
        }
        $ZipStream.Close()
        $ZipStream.Dispose()
        If($PassThru){$OutStream}
    }
    

    Here's an example of how you would call Add-ZipEntry and Get-ZipEntryContent functions completely in memory:

    $NewZipStream = Add-ZipEntry -ZipStream (New-Object IO.MemoryStream) -EntryPath Test.xml -Content ([text.encoding]::UTF8.GetBytes("<xml><test>1</test>")) -PassThru
    $bytes = Get-ZipEntryContent -ZipStream $NewZipStream -EntryPath 'Test.xml'
    [text.encoding]::UTF8.GetString($bytes)