Add Powershell/Invoke-DiskCleanupScan.ps1

This commit is contained in:
2026-04-23 09:54:29 +10:00
parent 5f4f0a9808
commit daec2070d8

View File

@@ -0,0 +1,545 @@
#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 ""