0

I am trying to use .NET classes instead of native compress-archive to zip multiple directories (each containing sub-directories and files), as compress-archive is giving me occasional OutOfMemory Exception.

Some articles tell me .NET classes, makes for a more optimal approach.

My tools directory $toolsDir = 'C:\Users\Public\LocalTools' has more than one directory that need to be zipped (please note everything is a directory, not file) - whichever directory matches the regex pattern as in the code.

Below is my code:

$cmpname = $env:computername
$now = $(Get-Date -Format yyyyMMddmmhhss)
$pattern = '^(19|[2-9][0-9])\d{2}\-(0?[1-9]|1[012])\-(0[1-9]|[12]\d|3[01])T((?:[01]\d|2[0-3])\;[0-5]\d\;[0-5]\d)\.(\d{3}Z)\-' + [ regex ]::Escape($cmpname)
$toolsDir = 'C:\Users\Public\LocalTools'
$destPathZip = "C:\Users\Public\ToolsOutput.zip"

 
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$CompressionLevel = [ System.IO.Compression.CompressionLevel ]::Optimal
$IncludeBaseDirectory = $false
$stream = New-Object System.IO.FileStream($destPathZip , [ System.IO.FileMode ]::OpenOrCreate)
$zip = New-Object System.IO.Compression.ZipArchive($stream , 'update')

 
$res = Get-ChildItem "${toolsDir}" | Where-Object {$_ .Name -match "${pattern}"}

if ($res -ne $null) {
    foreach ($dir in $res) {
       $source = "${toolsDir}\${dir}"
       [ System.IO.Compression.ZipFileExtensions ]::CreateEntryFromFile($destPathZip , $source , (Split-Path $source -Leaf), $CompressionLevel)

    }
}
else {
    Write-Host "Nothing to Archive!"

} 

Above code gives me this error: error1

enter image description here

When I researched about [ System.IO.Compression.ZipFileExtensions ]::CreateEntryFromFile , it is used to add files to a zip file already created. Is this the reason I am getting the error that I get ?

I also tried [ System.IO.Compression.ZipFile ]::CreateFromDirectory($source , $destPathZip , $CompressionLevel, $IncludeBaseDirectory) instead of [ System.IO.Compression.ZipFileExtensions ]::CreateEntryFromFile($destPathZip , $source , (Split-Path $source -Leaf), $CompressionLevel)

That gives me "The file 'C:\Users\Public\ToolsOutput.zip' already exists error.

How to change the code, in order to add multiple directories in the zip file.

dig_123
  • 2,240
  • 6
  • 35
  • 59
  • The first argument to `CreateEntryFromFile` should be a target `ZipArchive` object, so pass `$zip` in place of `$destPathZip`. – Mathias R. Jessen Mar 09 '22 at 11:33
  • @MathiasR.Jessen Thanks. After making the change, the error is gone. But its creating blank zip file `ToolsOutput.zip`. It doesn't actually archive anything – dig_123 Mar 09 '22 at 12:15
  • 2
    You need to flush both streams before the data is saved to file, I'll write up an answer in a second – Mathias R. Jessen Mar 09 '22 at 12:17
  • @MathiasR.Jessen Thanks that would help. I also did another test. It says `Access to the path 'C:\Users\Public\LocalTools\dirToarchive' is denied`. The reason is probably because of the `flush` not happening. Will wait for your answer. – dig_123 Mar 09 '22 at 12:21
  • As aside, I don't know why you're doing things like this where you leave spaces and the use of double-quotes `" ${toolsDir} \ ${dir} "`, this will be literal spaces in your path string. Same thing for the pattern variable – Santiago Squarzon Mar 09 '22 at 12:26
  • @SantiagoSquarzon Thanks for the comment . That might have been an error while copying the code. Edited the question. That is not in my original code. – dig_123 Mar 09 '22 at 13:09
  • @SantiagoSquarzon Edited the code in question. Can you help on the original question please. – dig_123 Mar 09 '22 at 13:12

1 Answers1

0

There are 3 problems with your code currently:

  1. First argument passed to CreateEntryFromFile() must be a ZipArchive object in which to add the new entry - in your case you'll want to pass the $zip which you've already created for this purpose.
  2. CreateEntryFromFile only creates 1 entry for 1 file per call - to recreate a whole directory substructure you need to calculate the correct entry path for each file, eg. subdirectory/subsubdirectory/file.exe
  3. You need to properly dispose of both the ZipArchive and the underlying file stream instances in order for the data to be persisted on disk. For this, you'll need a try/finally statement.

Additionally, there's no need to create the file if there are no files to archive :)

$cmpname = $env:computername
$pattern = '^(19|[2-9][0-9])\d{2}\-(0?[1-9]|1[012])\-(0[1-9]|[12]\d|3[01])T((?:[01]\d|2[0-3])\;[0-5]\d\;[0-5]\d)\.(\d{3}Z)\-' + [regex]::Escape($cmpname)
$toolsDir = 'C:\Users\Public\LocalTools'
$destPathZip = "C:\Users\Public\ToolsOutput.zip"

Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem

$CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal

$res = Get-ChildItem -LiteralPath $toolsDir | Where-Object { $_.Name -match $pattern }

if ($res) {
  try {
    # Create file + zip archive instances
    $stream = New-Object System.IO.FileStream($destPathZip, [System.IO.FileMode]::OpenOrCreate)
    $zip = New-Object System.IO.Compression.ZipArchive($stream, [System.IO.Compression.ZipArchiveMode]::Update)

    # Discover all files to archive
    foreach ($file in $res |Get-ChildItem -File -Recurse) {
      $source = $dir.FullName

      # calculate correct relative path to the archive entry
      $relativeFilePath = [System.IO.Path]::GetRelativePath($toolsDir, $source)
      $entryName = $relativeFilePath.Replace('\', '/')

      # Make sure the first argument to CreateEntryFromFile is the ZipArchive object
      [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $source, $entryName, $CompressionLevel)
    }
  }
  finally {
    # Clean up in reverse order
    $zip, $stream | Where-Object { $_ -is [System.IDisposable] } | ForEach-Object Dispose
  }
}
else {
  Write-Host "Nothing to Archive!"
}

Calling Dispose() on $zip will cause it to flush any un-written modifications to the underlying file stream and free any additional file handles it might have acquired, whereas calling Dispose() on the underlying file stream flushes those changes to the disk and closes the file handle.

Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • It actually gives me this error: `Exception calling "CreateEntryFromFile" with "4" argument(s): "Access to the path 'C:\Users\Public\LocalTools\dirToArchive' is denied` - But I know the account from which the script is run does have access to these directories. In fact `compress-archive` is able to archive the same directories, but is taking way too much memory, that is why I'm trying to move to .NET classes. – dig_123 Mar 09 '22 at 12:51
  • @dig_123 that makes sense - `CreateEntryFromFile()` creates entries _from files_, not entire directories :) – Mathias R. Jessen Mar 09 '22 at 12:57
  • My question already mentions that I need to add directories to the zip file, not files, that is my requirement. Everything under `$toolsDir = 'C:\Users\Public\LocalTools'` that matches REGEX pattern in my code is ideally a directory – dig_123 Mar 09 '22 at 13:00
  • Please read my question, I have also mentioned that I tried using `createfromdirectory` – dig_123 Mar 09 '22 at 13:02
  • @dig_123 I completely missed that part, I've updated the answer :) – Mathias R. Jessen Mar 09 '22 at 13:12