215 lines
5.7 KiB
PowerShell
215 lines
5.7 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Tree-style folder size analysis with depth + minimum size filtering.
|
|
.DESCRIPTION
|
|
Scans a starting path, calculates folder sizes up to a specified depth,
|
|
and outputs a readable tree list (indented paths) for folders >= MinSize.
|
|
|
|
Defaults:
|
|
- Path : C:\
|
|
- Depth : 3
|
|
- MinSize: 500MB
|
|
|
|
Supports MinSize as:
|
|
- numeric bytes (e.g., 1073741824)
|
|
- string with units (e.g., "1GB", "750MB", "200KB")
|
|
|
|
Optional environment-variable overrides (RMM friendly):
|
|
- ROOT_PATH
|
|
- DEPTH
|
|
- MIN_SIZE
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Position = 0)]
|
|
[string] $Path = "C:\",
|
|
|
|
[ValidateRange(1, 256)]
|
|
[int] $Depth = 3,
|
|
|
|
[object] $MinSize = "500MB"
|
|
)
|
|
|
|
begin {
|
|
|
|
function Test-IsAdminOrSystem {
|
|
try {
|
|
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
|
|
# SYSTEM check (PS 5.1 safe)
|
|
if ($id.Name -eq 'NT AUTHORITY\SYSTEM') {
|
|
return $true
|
|
}
|
|
|
|
$principal = New-Object Security.Principal.WindowsPrincipal($id)
|
|
return $principal.IsInRole(
|
|
[Security.Principal.WindowsBuiltInRole]::Administrator
|
|
)
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Convert-ToBytes {
|
|
param([Parameter(Mandatory)][object]$Value)
|
|
|
|
if ($null -eq $Value) { return [int64]0 }
|
|
|
|
if ($Value -is [byte] -or
|
|
$Value -is [int] -or
|
|
$Value -is [long] -or
|
|
$Value -is [int64] -or
|
|
$Value -is [double] -or
|
|
$Value -is [decimal]) {
|
|
return [int64]$Value
|
|
}
|
|
|
|
$s = ($Value.ToString()).Trim()
|
|
|
|
$m = [regex]::Match(
|
|
$s,
|
|
'^\s*(?<num>\d+(\.\d+)?)\s*(?<unit>bytes|b|kb|mb|gb|tb|pb)?\s*$',
|
|
'IgnoreCase'
|
|
)
|
|
|
|
if (-not $m.Success) {
|
|
throw "Invalid MinSize value: $s"
|
|
}
|
|
|
|
$num = [double]$m.Groups['num'].Value
|
|
$unit = $m.Groups['unit'].Value.ToLower()
|
|
|
|
switch ($unit) {
|
|
'' { return [int64]$num }
|
|
'b' { return [int64]$num }
|
|
'bytes' { return [int64]$num }
|
|
'kb' { return $num * 1KB }
|
|
'mb' { return $num * 1MB }
|
|
'gb' { return $num * 1GB }
|
|
'tb' { return $num * 1TB }
|
|
'pb' { return $num * 1PB }
|
|
default { return [int64]$num }
|
|
}
|
|
}
|
|
|
|
function Format-Bytes {
|
|
param([int64]$Bytes)
|
|
|
|
$units = @("Bytes","KB","MB","GB","TB","PB")
|
|
$value = [double]$Bytes
|
|
$i = 0
|
|
|
|
while ($value -ge 1024 -and $i -lt ($units.Count - 1)) {
|
|
$value /= 1024
|
|
$i++
|
|
}
|
|
|
|
if ($i -eq 0) {
|
|
return "{0:0} {1}" -f $value, $units[$i]
|
|
}
|
|
|
|
return "{0:N2} {1}" -f $value, $units[$i]
|
|
}
|
|
|
|
# Environment overrides
|
|
if ($env:ROOT_PATH) { $Path = $env:ROOT_PATH }
|
|
if ($env:DEPTH) { $Depth = [int]$env:DEPTH }
|
|
if ($env:MIN_SIZE) { $MinSize = $env:MIN_SIZE }
|
|
|
|
try {
|
|
$Path = (Get-Item -LiteralPath $Path -ErrorAction Stop).FullName
|
|
}
|
|
catch {
|
|
throw "Path '$Path' does not exist or is not accessible."
|
|
}
|
|
|
|
if (-not $Path.EndsWith('\')) {
|
|
$Path += '\'
|
|
}
|
|
|
|
$MinBytes = Convert-ToBytes $MinSize
|
|
|
|
if (-not (Test-IsAdminOrSystem)) {
|
|
Write-Warning "Not running as Administrator or SYSTEM. Sizes may be understated."
|
|
}
|
|
|
|
function Get-FolderSizeAtDepth {
|
|
param(
|
|
[string] $Folder,
|
|
[int] $DepthLeft
|
|
)
|
|
|
|
$node = [PSCustomObject]@{
|
|
Name = $Folder
|
|
Bytes = 0
|
|
Children = @()
|
|
}
|
|
|
|
if ($DepthLeft -le 1) {
|
|
try {
|
|
$node.Bytes = (
|
|
Get-ChildItem -LiteralPath $Folder -File -Recurse -Force -ErrorAction SilentlyContinue |
|
|
Measure-Object -Property Length -Sum
|
|
).Sum
|
|
}
|
|
catch {
|
|
$node.Bytes = 0
|
|
}
|
|
|
|
return $node
|
|
}
|
|
|
|
$items = Get-ChildItem -LiteralPath $Folder -Force -ErrorAction SilentlyContinue |
|
|
Where-Object { -not ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) }
|
|
|
|
foreach ($item in $items) {
|
|
if ($item.PSIsContainer) {
|
|
$child = Get-FolderSizeAtDepth -Folder $item.FullName -DepthLeft ($DepthLeft - 1)
|
|
$node.Bytes += $child.Bytes
|
|
$node.Children += $child
|
|
}
|
|
else {
|
|
$node.Bytes += $item.Length
|
|
}
|
|
}
|
|
|
|
return $node
|
|
}
|
|
|
|
function Emit-Tree {
|
|
param(
|
|
$Node,
|
|
[string] $BasePath,
|
|
[int] $Level
|
|
)
|
|
|
|
$relative = $Node.Name
|
|
if ($relative.StartsWith($BasePath, 'OrdinalIgnoreCase')) {
|
|
$relative = ".\" + $relative.Substring($BasePath.Length)
|
|
}
|
|
|
|
$indent = ' ' * (($Level + 1) * 2)
|
|
|
|
[PSCustomObject]@{
|
|
Path = $indent + $relative
|
|
Size = Format-Bytes $Node.Bytes
|
|
Bytes = $Node.Bytes
|
|
Depth = $Level
|
|
}
|
|
|
|
foreach ($child in $Node.Children) {
|
|
Emit-Tree -Node $child -BasePath $BasePath -Level ($Level + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
process {
|
|
$root = Get-FolderSizeAtDepth -Folder $Path -DepthLeft $Depth
|
|
|
|
Emit-Tree -Node $root -BasePath $Path -Level 0 |
|
|
Where-Object { $_.Bytes -ge $MinBytes } |
|
|
Select-Object Path, Size
|
|
} |