5

I am trying to base64 encode a ~66MB zip file to a string and write it to a file using Powershell. I'm working with a limitation that ultimately I have to include the base64 encoded file string directly into a Powershell script, such that when the script is run at a different location, the zip file can be recreated from it. I'm not limited to using Powershell to create the base64 encoded string. It's just what I'm most familiar with.

The code I'm currently using:

$file = 'C:\zipfile.zip'
$filebytes = Get-Content $file -Encoding byte
$fileBytesBase64 = [System.Convert]::ToBase64String($filebytes)
$fileBytesBase64 | Out-File 'C:\base64encodedString.txt'

Previously, the files I have worked with have been small enough that encoding was relatively fast. However, I am now finding that the file I'm encoding results in the process eating up all my RAM and ultimately is untenably slow. I get the feeling that there's a better way to do this, and would appreciate any suggestion.

abnerkanezzer
  • 77
  • 1
  • 5
  • 1
    [This](https://stackoverflow.com/a/57820500/4137916), which is C# but could be translated to PowerShell code fairly easily. Note that depending on how you embed the Base64 string in the script, it may be equally inefficient in recreating the file if special care is not taken. – Jeroen Mostert Mar 03 '21 at 18:37
  • @JeroenMostert , in Powershell it makes extremely slow performance in my case. Possibly because of `InputBlockSize` \ `OutputBlockSize` are too small and can not be changed. – filimonic Mar 03 '21 at 19:33

2 Answers2

3

⚠️ UPDATE 2023-08-17: There is a better solution.


Im my case it takes less than 1 second to encode or decode 117-Mb file.

Src file size: 117.22 MiB
Tgt file size: 156.3 MiB
Decoded size: 117.22 MiB
Encoding time: 0.294
Decoding time: 0.708

Code I making measures:

$pathSrc = 'D:\blend5\scena31.blend'
$pathTgt = 'D:\blend5\scena31.blend.b64'
$encoding = [System.Text.Encoding]::ASCII

$bytes = [System.IO.File]::ReadAllBytes($pathSrc)
Write-Host "Src file size: $([Math]::Round($bytes.Count / 1Mb,2)) MiB"
$swEncode = [System.Diagnostics.Stopwatch]::StartNew()
$B64String = [System.Convert]::ToBase64String($bytes, [System.Base64FormattingOptions]::None)
$swEncode.Stop()
[System.IO.File]::WriteAllText($pathTgt, $B64String, $encoding)

$B64String = [System.IO.File]::ReadAllText($pathTgt, $encoding)
Write-Host "Tgt file size: $([Math]::Round($B64String.Length / 1Mb,2)) MiB"
$swDecode = [System.Diagnostics.Stopwatch]::StartNew()
$bytes = [System.Convert]::FromBase64String($B64String)
$swDecode.Stop()
Write-Host "Decoded size: $([Math]::Round($bytes.Count / 1Mb,2)) MiB"

Write-Host "Encoding time: $([Math]::Round($swEncode.Elapsed.TotalSeconds,3)) s"
Write-Host "Decoding time: $([Math]::Round($swDecode.Elapsed.TotalSeconds,3)) s"
filimonic
  • 3,988
  • 2
  • 19
  • 26
2

UPDATE 2023-08-17 for big files, memory usage and speed

As @JohnRanger mentioned, there is a problem with previous answer that it has source file limit to ~1.5 GiB and memory-consumable.

Solution is to use file streams and CryptoStream(... ToBase64Transform...)

$sourcePath = "C:\test\windows_11.iso"
$targetPath = "C:\test\windows_11.iso.b64"
$size = Get-Item -Path $sourcePath | Select -ExpandProperty Length
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

$converterStream = [System.Security.Cryptography.CryptoStream]::new(
    [System.IO.File]::OpenRead($sourcePath),
    [System.Security.Cryptography.ToBase64Transform]::new(), 
    [System.Security.Cryptography.CryptoStreamMode]::Read,
    $false) # keepOpen = $false => When we close $converterStream, it will close source file stream also
$targetFileStream = [System.IO.File]::Create($targetPath)
$converterStream.CopyTo($targetFileStream)
$converterStream.Close() # And it also closes source file stream because of keepOpen = $false parameter.
$targetFileStream.Close() # Flush() is called internally.

$stopwatch.Stop()
Write-Host "Elapsed: $($stopwatch.Elapsed.TotalSeconds) seconds for $([Math]::Round($size / 1MB))-Mbyte file"

⚠️ This code runs over 30 times faster on PS7 compared to PS5


PS7: It takes 3.1 seconds for 5316 MiB file on my machine.

File copying with [IO.File]::Copy(...) takes 1.8 seconds.


PS5: It takes 115 seconds on my machine. I played with buffer size in [System.IO.File]::Create(...) and $converterStream.CopyTo(...), but did not get any reasonable performance difference : for 5316 MiB I had worst result of 115 seconds for default buffer sizes and best 112 seconds while playing buffer sizes. Maybe for slow targets buffer sizes will have more impact (i.e. working with slow disk or network share). Performance always hit single core of CPU.

File copying with [IO.File]::Copy() takes 1.8 seconds.

filimonic
  • 3,988
  • 2
  • 19
  • 26
  • 1
    Wow! 7.8 Seconds for a 3.7 GB file to encode. And thx for the ultra fast providing of a solution. – John Ranger Aug 17 '23 at 12:44
  • I'm sure this solution can be optimized for better performance and multi-threading (ex, reading file and converting in parallel), but I don't see any reason to do it in common case. – filimonic Aug 17 '23 at 12:46
  • 1
    In my opinion, it is quite fast: Elapsed: 7.8696603 seconds for 3654-Mbyte file. (The machine it runs on has a crucial 1TB SSD disk and 32 GB RAM. Looks that it converts nearly at the read/write limit of the disk). This is running under latest Powershell 7. – John Ranger Aug 17 '23 at 12:51
  • 1
    @JohnRanger wow. On PS5 it takes 115 seconds while on PS7 it takes 3.1 seconds in my case. Interesting.. – filimonic Aug 17 '23 at 13:02
  • 1
    The huge differences (115 sec to 3.1 secs) seems to come from optimizations within the underlying .NET core framework with Pwsh 7. I have seen this already in other cases. Microsoft has really worked on the improvement of the execution speed. – John Ranger Aug 17 '23 at 13:16