<# LXS - Disk Cleanup (Windows) Description: Reclaim disk space (temp files, Windows Update cache, recycle bin, Prefetch, error reports, WinSxS component store). Repo: https://git.hyko.cx/hykocx/lxs Usage: cleanup.ps1 [-Yes] [-NoUpdateCache] [-NoRecycleBin] [-NoPrefetch] [-NoComponentCleanup] [-Help] #> # Load LXS common library (colors, UI helpers, spinner, loggers, guards). # Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and # only hits the network when this script is run standalone. $LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' } if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" } $LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null } if ($LxsLibPath -and (Test-Path $LxsLibPath)) { . $LxsLibPath } else { try { $LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop } catch { Write-Error 'Failed to fetch lib/common.ps1' exit 1 } . ([scriptblock]::Create($LxsLibSource)) } $env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_cleanup.log' $LxsOpts = Read-LxsFlags -Arguments $args -Known @( '-Yes', '-y', '-NoUpdateCache', '-NoRecycleBin', '-NoPrefetch', '-NoComponentCleanup', '-Help', '-h' ) if (Show-LxsUnknownFlags $LxsOpts) { exit 1 } $Yes = Test-LxsFlag $LxsOpts @('-Yes', '-y') $NoUpdateCache = Test-LxsFlag $LxsOpts @('-NoUpdateCache') $NoRecycleBin = Test-LxsFlag $LxsOpts @('-NoRecycleBin') $NoPrefetch = Test-LxsFlag $LxsOpts @('-NoPrefetch') $NoComponentCleanup = Test-LxsFlag $LxsOpts @('-NoComponentCleanup') $Help = Test-LxsFlag $LxsOpts @('-Help', '-h') if ($Help) { Write-Host @' Usage: cleanup.ps1 [options] Options: -Yes Skip the confirmation prompt -NoUpdateCache Skip the Windows Update download cache -NoRecycleBin Skip emptying the recycle bin -NoPrefetch Skip the Prefetch folder -NoComponentCleanup Skip DISM /StartComponentCleanup (the slow one) -Help Show this help '@ exit 0 } function Get-LxsFreeBytes { try { return (New-Object System.IO.DriveInfo "$env:SystemDrive\").AvailableFreeSpace } catch { return 0 } } function Format-LxsSize { param([double]$Bytes) if ($Bytes -ge 1GB) { return ('{0:N2} GB' -f ($Bytes / 1GB)) } if ($Bytes -ge 1MB) { return ('{0:N1} MB' -f ($Bytes / 1MB)) } return ('{0:N0} KB' -f ($Bytes / 1KB)) } function Get-LxsFolderSize { param([string]$Path) if (-not (Test-Path $Path)) { return 0 } try { $sum = (Get-ChildItem -Path $Path -Recurse -Force -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum if ($null -eq $sum) { return 0 } return $sum } catch { return 0 } } # Delete the *contents* of a folder, never the folder itself. Files locked by a # running process are skipped silently — that is expected on a live system. function Clear-LxsFolderContents { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Label ) if (-not (Test-Path $Path)) { Write-Host "$($script:Gray) $Label — not present, skipped$($script:NC)" return 0 } $before = Get-LxsFolderSize -Path $Path $skipped = 0 Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue | ForEach-Object { try { Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction Stop } catch { $skipped++ } } $after = Get-LxsFolderSize -Path $Path $freed = [math]::Max(0, $before - $after) $suffix = if ($skipped -gt 0) { " ($skipped item(s) in use, skipped)" } else { '' } Write-LxsOk "$Label — $(Format-LxsSize $freed) reclaimed$suffix" return $freed } # ═══════════════════════════════════════════════════════════════════════════ # Plan # ═══════════════════════════════════════════════════════════════════════════ if (-not (Assert-LxsWindows)) { exit 1 } $isAdmin = Test-LxsAdmin $doUpdateCache = (-not $NoUpdateCache) -and $isAdmin $doPrefetch = (-not $NoPrefetch) -and $isAdmin $doComponent = (-not $NoComponentCleanup) -and $isAdmin Clear-Host Show-LxsBoxTop -Title 'DISK CLEANUP' Write-Host '' Write-Host 'The following locations will be cleaned:' Write-Host ' - User temp folder' if ($isAdmin) { Write-Host ' - System temp folder (C:\Windows\Temp)' } if (-not $NoRecycleBin) { Write-Host ' - Recycle bin (all drives)' } if ($doUpdateCache) { Write-Host ' - Windows Update download cache' } if ($doPrefetch) { Write-Host ' - Prefetch folder' } if ($isAdmin) { Write-Host ' - Windows Error Reporting queue and Delivery Optimization cache' } if ($doComponent) { Write-Host ' - WinSxS component store (DISM /StartComponentCleanup) - slow' } Write-Host '' if (-not $isAdmin) { Write-LxsWarn 'Not running as administrator: system-wide locations are skipped.' Write-Host "$($script:Gray) Re-run from an elevated terminal to clean them.$($script:NC)" Write-Host '' } Show-LxsSeparator Write-Host '' if (-not (Confirm-LxsAction -Question 'Proceed with the cleanup?' -DefaultYes -AssumeYes:$Yes)) { Write-LxsInfo 'Cancelled.' exit 0 } # ═══════════════════════════════════════════════════════════════════════════ # Run # ═══════════════════════════════════════════════════════════════════════════ $freeBefore = Get-LxsFreeBytes Write-Host '' Write-LxsInfo "Free space before: $(Format-LxsSize $freeBefore)" Write-Host '' Clear-LxsFolderContents -Path (Get-LxsTempDir) -Label 'User temp' | Out-Null if ($isAdmin) { Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'Temp') -Label 'System temp' | Out-Null Clear-LxsFolderContents -Path (Join-Path $env:ProgramData 'Microsoft\Windows\WER\ReportQueue') -Label 'Error reports' | Out-Null Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'SoftwareDistribution\DeliveryOptimization') -Label 'Delivery Optimization' | Out-Null } if ($doPrefetch) { Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'Prefetch') -Label 'Prefetch' | Out-Null } if (-not $NoRecycleBin) { try { Clear-RecycleBin -Force -ErrorAction Stop Write-LxsOk 'Recycle bin emptied' } catch { # Clear-RecycleBin throws when the bin is already empty. Write-Host "$($script:Gray) Recycle bin — already empty or not accessible$($script:NC)" } } if ($doUpdateCache) { # The download cache can only be cleared with the update services stopped; # they are restarted afterwards whether or not the deletion succeeded. $services = @('wuauserv', 'bits') $stopped = @() foreach ($svc in $services) { try { $s = Get-Service -Name $svc -ErrorAction Stop if ($s.Status -eq 'Running') { Stop-Service -Name $svc -Force -ErrorAction Stop $stopped += $svc } } catch { Write-LxsWarn "Could not stop $svc — the update cache may be partially locked." } } try { Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'SoftwareDistribution\Download') -Label 'Windows Update cache' | Out-Null } finally { foreach ($svc in $stopped) { try { Start-Service -Name $svc -ErrorAction Stop } catch { Write-LxsWarn "Failed to restart $svc — start it manually." } } } } if ($doComponent) { Write-Host '' Write-LxsInfo 'Running DISM component cleanup (several minutes, no output until done)...' $ok = Invoke-LxsSpinner -Message 'DISM /StartComponentCleanup' -FilePath 'dism.exe' ` -ArgumentList @('/Online', '/Cleanup-Image', '/StartComponentCleanup') if (-not $ok) { Write-LxsWarn 'Component cleanup did not complete; see the log above.' } } # Windows.old is reported, never auto-deleted: removing it forfeits the # ability to roll back a feature update. $windowsOld = Join-Path $env:SystemDrive 'Windows.old' if (Test-Path $windowsOld) { Write-Host '' Write-LxsWarn "$windowsOld exists ($(Format-LxsSize (Get-LxsFolderSize -Path $windowsOld)))." Write-Host "$($script:Gray) It holds your previous Windows install. Removing it blocks rollback;$($script:NC)" Write-Host "$($script:Gray) use Settings > System > Storage > Temporary files if you want it gone.$($script:NC)" } $freeAfter = Get-LxsFreeBytes Write-Host '' Show-LxsSeparator Write-Host '' Write-LxsOk "Free space after: $(Format-LxsSize $freeAfter)" Write-LxsOk "Reclaimed: $(Format-LxsSize ([math]::Max(0, $freeAfter - $freeBefore)))" Write-Host '' exit 0