Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way of manipulating zip file contents in memory with Powershell?

Tags:

powershell

zip

I'm currently trying to write a powershell function which works with the output of the Lync powershell cmdlet "Export-CsConfiguration -AsBytes". When using implicit Powershell remoting with the Lync Cmdlets, the -AsBytes flag is the only way to work the Export-CsConfiguration cmdlet, and it returns a Byte array, which, if you write it to disk with "Set-Content -Encoding Byte", results in a zip file.

I'm wondering if there's a way to expand the content of the byte array into the two files which are contained in that zip, but only do it in memory. I'm not really interested in keeping the zip file around for long, as it changes frequently, and something about writing the file contents out to disk only to read them straight back in again so I can do something with the uncompressed content seems horribly wrong to me.

So is there someway of doing something like this which avoids writes to disk:

$ZipFileBytes = Export-CsConfiguration -AsBytes
# Made up Powershell function follows:
[xml]DocItemSet = Extract-FileFromInMemoryZip -ByteArray $ZipFileBytes -FileInsideZipIWant "DocItemSet.xml"
# Do stuff with XML here

Instead of doing:

$ZipFileBytes = Export-CsConfiguration -AsBytes | Set-Content -Encoding Byte "CsConfig.zip"
[System.Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem')
[System.IO.Compression.ZipFile]::ExtractToDirectory("CsConfig.zip", "C:\Temp")
[xml]$DocItemSet = New-Object Xml.XmlDocument
$DocItemSet.Load("C:\Temp\DocItemSet.xml")
# Do stuff with XML here

Or am I SOL?

like image 522
GodEater Avatar asked Dec 20 '22 12:12

GodEater


2 Answers

Answering my own question here, in case it proves useful to others: (N.B. Requires .NET 4.5)

It looks like using System.IO.Compression.ZipArchive in combination with System.IO.Memorystream is the way forward. I've got this now:

Function Load-CsConfig{
  [System.Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null

  $ZipBytes = Export-CsConfiguration -AsBytes
  $ZipStream = New-Object System.IO.Memorystream
  $ZipStream.Write($ZipBytes,0,$ZipBytes.Length)
  $ZipArchive = New-Object System.IO.Compression.ZipArchive($ZipStream)
  $ZipEntry = $ZipArchive.GetEntry('DocItemSet.xml')
  $EntryReader = New-Object System.IO.StreamReader($ZipEntry.Open())
  $DocItemSet = $EntryReader.ReadToEnd()
  return $DocItemSet
}

Which does exactly what I need.

Thanks all :)

like image 53
GodEater Avatar answered Dec 22 '22 02:12

GodEater


Making the "Made up Powershell function" a reality:

#
#   .SYNOPSIS
#       Extract a file from a byte[] zip file
#   
#   .DESCRIPTION
#       Extracts a file from a byte[] zip file as byte[]
#   
#   .PARAMETER ByteArray
#       Byte array containing zip file
#   
#   .PARAMETER FileInsideZipIWant
#       The file inside zip I want
#   
#   .PARAMETER utf8
#       If the file is UTF-8 encoded, use this switch to get a string
#   
#   .EXAMPLE
#       PS C:\> $utf8str = Extract-FileFromInMemoryZip -ByteArray $ZipFileBytes -FileInsideZipIWant "DocItemSet.xml" -utf8
#       PS C:\> $utf8str = Extract-FileFromInMemoryZip $ZipFileBytes "DocItemSet.xml" -utf8
#       PS C:\> $bs = Extract-FileFromInMemoryZip $ZipFileBytes "DocItemSet.xml"        
#   
#   .OUTPUTS
#       string, byte[]
#   
#   .NOTES
#       Exactly as desired. You may want to change the name of the "FileInsideZipIWant" parameter.
#       Don't plan on extracting files larger than 2GB.
#
function Extract-FileFromInMemoryZip
{
    [CmdletBinding(DefaultParameterSetName = 'raw')]
    [OutputType([string], ParameterSetName = 'utf8')]
    [OutputType([byte[]], ParameterSetName = 'raw')]
    param
    (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0,
                   HelpMessage = 'Byte array containing zip file')]
        [byte[]]$ByteArray,
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1,
                   HelpMessage = 'Single file to extract')]
        [string]$FileInsideZipIWant,
        [Parameter(ParameterSetName = 'utf8')]
        [switch]$utf8
    )

    BEGIN { Add-Type -AN System.IO.Compression -ea:Stop } # Stop on error

    PROCESS {
        $entry = (
            New-Object System.IO.Compression.ZipArchive(
                New-Object System.IO.MemoryStream ( ,$ByteArray)
            )
        ).GetEntry($FileInsideZipIWant)

        # Note ZipArchiveEntry.Length returns a long (rather than int),
        # but we can't conveniently construct arrays longer than System.Int32.MaxValue
        $b = [byte[]]::new($entry.Length)

        # Avoid StreamReader to (dramatically) improve performance
        # ...but it may be useful if the extracted file has a BOM header
        $entry.Open().Read($b, 0, $b.Length)

        write $(
            if ($utf8) { [System.Text.Encoding]::UTF8.GetString($b) }
            else { $b }
        )
    }
}
like image 38
YenForYang Avatar answered Dec 22 '22 02:12

YenForYang