3

I am having trouble trying to call a function for a script that I'm using to build a list of zip files in a folder on my PC. The final CSV I need is to create is a list of the zip files with their uncompressed sizes. Here is what I have so far (compiled from several posts):

Function to get the uncompressed size:

function Get-UncompressedZipFileSize {

param (
    $Path
)

$shell = New-Object -ComObject shell.application
$zip = $shell.NameSpace($Path)
$size = 0
foreach ($item in $zip.items()) {
    if ($item.IsFolder) {
        $size += Get-UncompressedZipFileSize -Path $item.Path
    } else {
        $size += $item.size
    }
}


[System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$shell) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

return $size
}

Here is my Array Creation:

$arr = @()
gci C:\zips -recurse | ? {$_.PSIsContainer -eq $False} | % {
$obj = New-Object PSObject
$obj | Add-Member NoteProperty Name $_.Name
$obj | Add-Member NoteProperty FullPath $_.FullName
$arr += $obj
}
$arr | Export-CSV -notypeinformation c:\zips

I'm stuck at creating a new member object into my array that will call the get-uncompressedzipfilesize function to pass that size back into the array as a new column in my zip. Is something like this even possible.?

Harry123
  • 49
  • 7

2 Answers2

4

Here is an alternative using ZipFile Class. The SizeConvert Class is inspired from this answer. The output of the Get-ZipFileSize would be the absolute path of the Zip File, its compressed and expanded size and its formatted friendly sizes (i.e.: 7.88 MB instead of 8262942).

using namespace System.IO
using namespace System.IO.Compression
using namespace System.Linq

function Get-ZipFileSize {
    [cmdletbinding()]
    param(
        [parameter(ValueFromPipelineByPropertyName)]
        [string] $FullName
    )

    begin {
        if(-not $IsCoreCLR) {
            Add-Type -AssemblyName System.IO.Compression.FileSystem
        }
        
        class SizeConvert {
            static [string[]] $Suffix = "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"

            static [string] ToFriendlySize([int64] $Length, [int] $DecimalPoints) {
                $idx = 0

                while ($Length -ge 1kb) {
                    $Length /= 1kb
                    $idx++
                }
                return '{0} {1}' -f [math]::Round($Length, $DecimalPoints), [SizeConvert]::Suffix[$idx]
            }
        }
    }
    process {
        try {
            $zip = [ZipFile]::OpenRead($FullName)

            $expanded   = [Enumerable]::Sum([Int64[]] $zip.Entries.Length)
            $compressed = [Enumerable]::Sum([int64[]] $zip.Entries.CompressedLength)

            [pscustomobject]@{
                FilePath            = $FullName
                RawExpanded         = $expanded
                RawCompressed       = $compressed
                FormattedExpanded   = [SizeConvert]::ToFriendlySize($expanded, 2)
                FormattedCompressed = [SizeConvert]::ToFriendlySize($compressed, 2)
            }
        }
        catch {
            $PSCmdlet.WriteError($_)
        }
        finally {
            if($zip -is [System.IDisposable]) {
                $zip.Dispose()
            }
        }
    }
}

Get-ChildItem -Filter *.zip -Recurse | Get-ZipFileSize | Export-Csv ....
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    I guess `SizeConvert` is future-proof, supporting yottabytes. :) – zett42 Feb 12 '22 at 21:53
  • @zett42 LOL I guess yes! Didn't want to modify the function too much as I think the author did a great job but yeah YB seem a bit of an overkill :P – Santiago Squarzon Feb 12 '22 at 21:56
  • 1
    You might also comsider using the Windows Shell [shlwapi.h `StrFormatByteSize`](https://learn.microsoft.com/windows/win32/api/shlwapi/nf-shlwapi-strformatbytesizew) function for convenient conversion, see [How to convert value to KB, MB, or GB depending on digit placeholders?](https://stackoverflow.com/a/57535324/1701026) – iRon Feb 13 '22 at 12:06
  • @iRon that looks very interesting never seen that before, unfortunately it wouldn't be compatible with Linux – Santiago Squarzon Feb 13 '22 at 14:12
3

To make this simpler, since you're only calling your function to get the size of the current folder (zip), you can use a Calculated Property for this:

$Path = "C:\Zips"
Get-ChildItem -Path $Path -Directory -Recurse | 
    Select-Object -Property Name, FullName,
    @{
        Name = "Size"
        Expression = {
            Get-UncompressedZipFileSize -Path $_.FullName
        }
    } | Export-Csv -Path "$Path\zip.csv" -Force -NoTypeInformation -Append 

On another note, if you ever find yourself explicitly adding to an array, take advantage of PowerShell's pipeline streaming.

$Path = "C:\Zips"
Get-ChildItem -Path $Path -Directory -Recurse | 
    ForEach-Object -Process {
        [PSCustomObject]@{
            Name = $_.Name
            FullPath = $_.FullName
            Size = Get-UncompressedZipFileSize -Path $_.FullName
        } | Export-Csv -Path "$Path\zip.csv" -Force -NoTypeInformation -Append 
    }

Not only is adding to a fixed array (+=) computationally expensive (if you have a large directory), it is slow. Fixed arrays mean just that, they are a fixed size and in order for you to add to it, it needs to be broken down and recreated. An alternate solution would by an arraylist but, in this case - and in most cases - it's not needed.

  • Get-ChildItem also includes a -Directory switch to search for just folders. Presented in V3.
  • I would recommend searching for the file extension of the compressed folders as well so you don't run into any issues using -Filter.
mklement0
  • 382,024
  • 64
  • 607
  • 775
Abraham Zinala
  • 4,267
  • 3
  • 9
  • 24
  • 2
    Nice, though I didn't know those poor arrays get _broken down_ :) – mklement0 Feb 12 '22 at 19:32
  • I think I read that in the book "PowerShell in Practice". Lol could be wrong – Abraham Zinala Feb 12 '22 at 19:34
  • 1
    :) Well, I guess eventually _garbage-collecting_ the old array can be though of as breaking it down... – mklement0 Feb 12 '22 at 19:35
  • Would love to see these concepts in actual code; to get a better understanding. It's just so difficult lol – Abraham Zinala Feb 12 '22 at 19:38
  • 3
    Garbage collection is built into .NET and performed on demand, so there's rarely a need to control it explicitly in user code, though it is possible via the [`[GC]`](https://learn.microsoft.com/en-us/dotnet/api/system.gc) class; the linked topic also leads to background information. – mklement0 Feb 12 '22 at 19:47
  • 1
    Thank you thank you thank you!! I ended up going with the Calculated Property solution and created my own array and calling in my custom function!! Hooray! – Harry123 Feb 13 '22 at 16:18