#Requires -RunAsAdministrator <# .SYNOPSIS C: Drive Disk Space Report — generates a self-contained HTML report for stakeholders. .DESCRIPTION Scans the C: drive across all common space consumers and produces a polished, printable HTML report. No cleanup is performed. Safe to run at any time. .NOTES Supports: Windows Server 2016, 2019, 2022, 2025 Run as: Administrator Output: HTML file on the Desktop (or path specified by -OutputPath) Author: SBCIT #> param( [string]$OutputPath = "$env:USERPROFILE\Desktop", [string]$ClientName = "" ) Set-StrictMode -Version Latest $ErrorActionPreference = 'SilentlyContinue' # ───────────────────────────────────────────── # HELPERS # ───────────────────────────────────────────── function Format-Size { param([long]$Bytes) if ($Bytes -ge 1GB) { return "{0:N2} GB" -f ($Bytes / 1GB) } if ($Bytes -ge 1MB) { return "{0:N2} MB" -f ($Bytes / 1MB) } if ($Bytes -ge 1KB) { return "{0:N2} KB" -f ($Bytes / 1KB) } return "$Bytes B" } function Get-FolderSize { param([string]$Path) if (-not (Test-Path $Path)) { return 0 } try { (Get-ChildItem -Path $Path -Recurse -Force -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum } catch { 0 } } function Get-RecycleBinSize { $total = 0 try { $shell = New-Object -ComObject Shell.Application $recycle = $shell.Namespace(0xA) foreach ($item in $recycle.Items()) { try { $total += $item.Size } catch {} } } catch {} return $total } function Get-SeverityClass { param([long]$Bytes) if ($Bytes -ge 1GB) { return "sev-critical" } if ($Bytes -ge 200MB) { return "sev-warning" } if ($Bytes -ge 10MB) { return "sev-minor" } return "sev-ok" } function Get-SeverityLabel { param([long]$Bytes) if ($Bytes -ge 1GB) { return "High" } if ($Bytes -ge 200MB) { return "Medium" } if ($Bytes -ge 10MB) { return "Low" } return "Minimal" } # ───────────────────────────────────────────── # CONSOLE PROGRESS # ───────────────────────────────────────────── Write-Host "" Write-Host " SBCIT // Disk Space Report Generator" -ForegroundColor DarkCyan Write-Host " Scanning $env:COMPUTERNAME — please wait...`n" -ForegroundColor Gray # ───────────────────────────────────────────── # DRIVE STATE # ───────────────────────────────────────────── $drive = Get-PSDrive C $driveTotal = $drive.Used + $drive.Free $driveFree = $drive.Free $driveUsed = $drive.Used $freePct = [math]::Round(($driveFree / $driveTotal) * 100, 1) $usedPct = [math]::Round(($driveUsed / $driveTotal) * 100, 1) $totalGB = [math]::Round($driveTotal / 1GB, 2) $usedGB = [math]::Round($driveUsed / 1GB, 2) $freeGB = [math]::Round($driveFree / 1GB, 2) $driveStatus = if ($freePct -lt 10) { "Critical" } elseif ($freePct -lt 20) { "Low" } else { "Healthy" } $driveStatusClass = if ($freePct -lt 10) { "sev-critical" } elseif ($freePct -lt 20) { "sev-warning" } else { "sev-ok" } # OS Version $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue $osName = if ($osInfo) { $osInfo.Caption } else { "Windows Server" } $osBuild = if ($osInfo) { "Build $($osInfo.BuildNumber)" } else { "" } # Uptime $uptime = "" if ($osInfo -and $osInfo.LastBootUpTime) { $span = New-TimeSpan -Start $osInfo.LastBootUpTime -End (Get-Date) $uptime = "$($span.Days)d $($span.Hours)h $($span.Minutes)m" } # ───────────────────────────────────────────── # SCAN # ───────────────────────────────────────────── $findings = [System.Collections.Generic.List[PSCustomObject]]::new() function Add-Finding { param( [string]$Category, [string]$Label, [string]$Path, [long]$Size, [string]$Description, [string]$Note = "" ) $findings.Add([PSCustomObject]@{ Category = $Category Label = $Label Path = $Path Size = $Size Description = $Description Note = $Note }) } # Temp Files Add-Finding "Temporary Files" "Windows Temp Folder" "C:\Windows\Temp" (Get-FolderSize "C:\Windows\Temp") "System-wide temporary files created by Windows and applications." "" Add-Finding "Temporary Files" "System Temp ($env:TEMP)" $env:TEMP (Get-FolderSize $env:TEMP) "Temporary files for the current user session." "" Add-Finding "Temporary Files" "User AppData Temp" "C:\Users\$env:USERNAME\AppData\Local\Temp" (Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Temp") "Per-user application temporary data." "" # Windows Update Add-Finding "Windows Update" "Update Download Cache" "C:\Windows\SoftwareDistribution\Download" (Get-FolderSize "C:\Windows\SoftwareDistribution\Download") "Cached Windows Update downloads. Can be safely cleared when no updates are pending." "" Add-Finding "Windows Update" "WinSxS Component Backups" "C:\Windows\WinSxS\Backup" (Get-FolderSize "C:\Windows\WinSxS\Backup") "Old component backups retained after Windows updates." "" # System Logs $cbsSize = Get-FolderSize "C:\Windows\Logs\CBS" Add-Finding "System Logs" "Component-Based Servicing Logs" "C:\Windows\Logs\CBS" $cbsSize "Logs generated during Windows component servicing and updates." "" $evtxSize = 0 try { $evtxSize = (Get-ChildItem "C:\Windows\System32\winevt\Logs" -Filter "*.evtx" -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum } catch {} Add-Finding "System Logs" "Windows Event Logs" "C:\Windows\System32\winevt\Logs" $evtxSize "Application, System, and Security event logs." "Backed up to Zipline before clearing." # Error Reporting Add-Finding "Error Reporting" "WER Report Queue" "C:\ProgramData\Microsoft\Windows\WER\ReportQueue" (Get-FolderSize "C:\ProgramData\Microsoft\Windows\WER\ReportQueue") "Pending Windows Error Reports awaiting upload to Microsoft." "" Add-Finding "Error Reporting" "WER Report Archive" "C:\ProgramData\Microsoft\Windows\WER\ReportArchive" (Get-FolderSize "C:\ProgramData\Microsoft\Windows\WER\ReportArchive") "Previously sent Windows Error Reports retained locally." "" Add-Finding "Error Reporting" "User Error Reports" "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\WER" (Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\WER") "Per-user application crash reports." "" # Crash Dumps $miniDump = Get-FolderSize "C:\Windows\Minidump" $memDump = if (Test-Path "C:\Windows\MEMORY.DMP") { (Get-Item "C:\Windows\MEMORY.DMP").Length } else { 0 } Add-Finding "Crash Dumps" "Minidump Files" "C:\Windows\Minidump" $miniDump "Small memory dumps generated when the system encounters a critical error." "" Add-Finding "Crash Dumps" "Full Memory Dump" "C:\Windows\MEMORY.DMP" $memDump "Complete RAM snapshot from last system crash. Large file — only present after a BSOD." "" # Delivery Optimisation Add-Finding "Delivery Optimisation" "DO Peer Cache" "C:\Windows\SoftwareDistribution\DeliveryOptimization" (Get-FolderSize "C:\Windows\SoftwareDistribution\DeliveryOptimization") "Windows Update peer-to-peer delivery cache." "" Add-Finding "Delivery Optimisation" "DO Network Service Cache" "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization" (Get-FolderSize "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization") "Delivery Optimisation metadata cache." "" # IIS Logs foreach ($iisPath in @("C:\inetpub\logs\LogFiles","C:\Windows\System32\LogFiles\W3SVC1")) { $sz = Get-FolderSize $iisPath if ($sz -gt 0) { Add-Finding "IIS Web Server Logs" "IIS Logs ($iisPath)" $iisPath $sz "Internet Information Services access and error logs." "Cleanup removes files older than 30 days." } } # Recycle Bin Add-Finding "Recycle Bin" "C: Recycle Bin" 'C:\$Recycle.Bin' (Get-RecycleBinSize) "Files deleted by users but not yet permanently removed." "" # Cache Add-Finding "Cache" "Thumbnail Cache" "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\Explorer" (Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\Explorer") "Cached image thumbnails. Rebuilds automatically when browsing folders." "" Add-Finding "Cache" "Font Cache" "C:\Windows\ServiceProfiles\LocalService\AppData\Local\FontCache" (Get-FolderSize "C:\Windows\ServiceProfiles\LocalService\AppData\Local\FontCache") "Font rendering cache. Rebuilds automatically on next boot." "" # Installer Cache $installerCache = Get-FolderSize "C:\Windows\Installer\`$PatchCache`$" Add-Finding "Installer Cache" "MSI Patch Cache" "C:\Windows\Installer\`$PatchCache`$" $installerCache "Cached MSI installer patches. Clearing may affect application repair." "⚠ Verify before removing." # Old Profiles $systemProfiles = @('Administrator','Default','Default User','Public','All Users','LocalService','NetworkService','systemprofile') $oldProfiles = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin $systemProfiles -and $_.Name -ne $env:USERNAME } foreach ($prof in $oldProfiles) { $sz = Get-FolderSize $prof.FullName Add-Finding "User Profiles" "Profile: $($prof.Name)" $prof.FullName $sz "User profile folder for an account that may no longer be active." "⚠ Verify account status before removing." } # Legacy Add-Finding "Legacy" "Downloaded Program Files" "C:\Windows\Downloaded Program Files" (Get-FolderSize "C:\Windows\Downloaded Program Files") "Legacy ActiveX and Java components. Generally safe to clear on modern servers." "" # Filter zero-size findings $findings = $findings | Where-Object { $_.Size -gt 0 } # ───────────────────────────────────────────── # DERIVED STATS # ───────────────────────────────────────────── $totalRecoverable = ($findings | Measure-Object -Property Size -Sum).Sum $recoverablePct = if ($driveTotal -gt 0) { [math]::Round(($totalRecoverable / $driveTotal) * 100, 1) } else { 0 } $projectedFreeGB = [math]::Round(($driveFree + $totalRecoverable) / 1GB, 2) $projectedFreePct = [math]::Round((($driveFree + $totalRecoverable) / $driveTotal) * 100, 1) $categoryGroups = $findings | Group-Object Category | Sort-Object { ($_.Group | Measure-Object -Property Size -Sum).Sum } -Descending $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $fileStamp = Get-Date -Format 'yyyyMMdd-HHmmss' $reportFile = Join-Path $OutputPath "$($env:COMPUTERNAME)_DiskReport_$fileStamp.html" $reportTitle = if ($ClientName) { "$ClientName — $($env:COMPUTERNAME)" } else { $env:COMPUTERNAME } # ───────────────────────────────────────────── # BUILD HTML # ───────────────────────────────────────────── $rowsHtml = [System.Text.StringBuilder]::new() foreach ($group in $categoryGroups) { $groupTotal = ($group.Group | Measure-Object -Property Size -Sum).Sum $isFirst = $true $rowCount = $group.Group.Count foreach ($item in ($group.Group | Sort-Object Size -Descending)) { $sev = Get-SeverityClass $item.Size $sevLabel = Get-SeverityLabel $item.Size $sizeStr = Format-Size $item.Size $noteHtml = if ($item.Note) { "$([System.Web.HttpUtility]::HtmlEncode($item.Note))" } else { "" } if ($isFirst) { $catCell = "
| Category | Item | Description | Size | Impact |
|---|
Invoke-DiskCleanupScan.ps1) which handles all items above safely with confirmation prompts.