public/Get-FolderSize.ps1
function Get-FolderSize { [CmdletBinding(DefaultParameterSetName = "Path")] param( [Parameter(ParameterSetName = "Path", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [Alias('Name', 'FullName')] [string[]] $Path, [int] $Precision = 4, [switch] $RoboOnly, [switch] $ComOnly, [Parameter(ParameterSetName = "LiteralPath", Mandatory = $true, Position = 0)] [string[]] $LiteralPath, [ValidateRange(1, 128)] [byte] $RoboThreadCount = 16 ) begin { if ($RoboOnly -and $ComOnly) { Write-Error -Message "You can't use both -ComOnly and -RoboOnly. Default is COM with a fallback to robocopy." -ErrorAction Stop } if (-not $RoboOnly) { $FSO = New-Object -ComObject Scripting.FileSystemObject -ErrorAction Stop } function Get-RoboFolderSizeInternal { [CmdletBinding()] param( # Paths to report size, file count, dir count, etc. for. [string[]] $Path, [int] $Precision = 4) begin { if (-not (Get-Command -Name robocopy -ErrorAction SilentlyContinue)) { Write-Warning -Message "Fallback to robocopy failed because robocopy.exe could not be found. Path '$p'. $([datetime]::Now)." return } } process { foreach ($p in $Path) { Write-Verbose -Message "Processing path '$p' with Get-RoboFolderSizeInternal. $([datetime]::Now)." $RoboCopyArgs = @("/L","/S","/NJH","/BYTES","/FP","/NC","/NDL","/TS","/XJ","/R:0","/W:0","/MT:$RoboThreadCount") [datetime] $StartedTime = [datetime]::Now [string] $Summary = robocopy $p NULL $RoboCopyArgs | Select-Object -Last 8 [datetime] $EndedTime = [datetime]::Now [regex] $HeaderRegex = '\s+Total\s*Copied\s+Skipped\s+Mismatch\s+FAILED\s+Extras' [regex] $DirLineRegex = 'Dirs\s*:\s*(?<DirCount>\d+)(?:\s+\d+){3}\s+(?<DirFailed>\d+)\s+\d+' [regex] $FileLineRegex = 'Files\s*:\s*(?<FileCount>\d+)(?:\s+\d+){3}\s+(?<FileFailed>\d+)\s+\d+' [regex] $BytesLineRegex = 'Bytes\s*:\s*(?<ByteCount>\d+)(?:\s+\d+){3}\s+(?<BytesFailed>\d+)\s+\d+' [regex] $TimeLineRegex = 'Times\s*:\s*(?<TimeElapsed>\d+).*' [regex] $EndedLineRegex = 'Ended\s*:\s*(?<EndedTime>.+)' if ($Summary -match "$HeaderRegex\s+$DirLineRegex\s+$FileLineRegex\s+$BytesLineRegex\s+$TimeLineRegex\s+$EndedLineRegex") { New-Object PSObject -Property @{ Path = $p TotalBytes = [decimal] $Matches['ByteCount'] TotalMBytes = [math]::Round(([decimal] $Matches['ByteCount'] / 1MB), $Precision) TotalGBytes = [math]::Round(([decimal] $Matches['ByteCount'] / 1GB), $Precision) BytesFailed = [decimal] $Matches['BytesFailed'] DirCount = [decimal] $Matches['DirCount'] FileCount = [decimal] $Matches['FileCount'] DirFailed = [decimal] $Matches['DirFailed'] FileFailed = [decimal] $Matches['FileFailed'] TimeElapsed = [math]::Round([decimal] ($EndedTime - $StartedTime).TotalSeconds, $Precision) StartedTime = $StartedTime EndedTime = $EndedTime } | Select-Object -Property Path, TotalBytes, TotalMBytes, TotalGBytes, DirCount, FileCount, DirFailed, FileFailed, TimeElapsed, StartedTime, EndedTime } else { Write-Warning -Message "Path '$p' output from robocopy was not in an expected format." } } } } } process { if ($PSCmdlet.ParameterSetName -eq "Path") { $Paths = @(Resolve-Path -Path $Path | Select-Object -ExpandProperty ProviderPath -ErrorAction SilentlyContinue) } else { $Paths = @(Get-Item -LiteralPath $LiteralPath | Select-Object -ExpandProperty FullName -ErrorAction SilentlyContinue) } foreach ($p in $Paths) { Write-Verbose -Message "Processing path '$p'. $([datetime]::Now)." if (-not (Test-Path -LiteralPath $p -PathType Container)) { Write-Warning -Message "$p does not exist or is a file and not a directory. Skipping." continue } # We know we can't have -ComOnly here if we have -RoboOnly. if ($RoboOnly) { Get-RoboFolderSizeInternal -Path $p -Precision $Precision continue } $ErrorActionPreference = 'Stop' try { $StartFSOTime = [datetime]::Now $TotalBytes = $FSO.GetFolder($p).Size $EndFSOTime = [datetime]::Now if ($null -eq $TotalBytes) { if (-not $ComOnly) { Get-RoboFolderSizeInternal -Path $p -Precision $Precision continue } else { Write-Warning -Message "Failed to retrieve folder size for path '$p': $($Error[0].Exception.Message)." } } } catch { if ($_.Exception.Message -like '*PERMISSION*DENIED*') { if (-not $ComOnly) { Write-Verbose "Caught a permission denied. Trying robocopy." Get-RoboFolderSizeInternal -Path $p -Precision $Precision continue } else { Write-Warning "Failed to process path '$p' due to a permission denied error: $($_.Exception.Message)" } } Write-Warning -Message "Encountered an error while processing path '$p': $($_.Exception.Message)" continue } $ErrorActionPreference = 'Continue' New-Object PSObject -Property @{ Path = $p TotalBytes = [decimal] $TotalBytes TotalMBytes = [math]::Round(([decimal] $TotalBytes / 1MB), $Precision) TotalGBytes = [math]::Round(([decimal] $TotalBytes / 1GB), $Precision) BytesFailed = $null DirCount = $null FileCount = $null DirFailed = $null FileFailed = $null TimeElapsed = [math]::Round(([decimal] ($EndFSOTime - $StartFSOTime).TotalSeconds), $Precision) StartedTime = $StartFSOTime EndedTime = $EndFSOTime } | Select-Object -Property Path, TotalBytes, TotalMBytes, TotalGBytes, DirCount, FileCount, DirFailed, FileFailed, TimeElapsed, StartedTime, EndedTime } } end { if (-not $RoboOnly) { [void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($FSO) } [gc]::Collect() [gc]::WaitForPendingFinalizers() } } |