public/Invoke-OctaDiskCleanup.ps1
|
function Get-OctaCleanupTargets { <# .SYNOPSIS Measures real, current reclaimable bytes per target (FR-045). Never caches/estimates. #> [CmdletBinding()] param() function Measure-OctaPathBytes { param([string[]]$Paths) $total = 0L foreach ($p in $Paths) { $items = Get-ChildItem -Path $p -Recurse -Force -ErrorAction SilentlyContinue $total += ($items | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum } return $total } $targets = @() $targets += [pscustomobject]@{ Name = 'User temp files'; Paths = @("$env:TEMP\*") MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:TEMP\*")) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Deleted temp files are not recoverable.' } $targets += [pscustomobject]@{ Name = 'System temp files'; Paths = @("$env:WINDIR\Temp\*") MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\Temp\*")) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Deleted temp files are not recoverable.' } $targets += [pscustomobject]@{ Name = 'Prefetch'; Paths = @("$env:WINDIR\Prefetch\*.pf") MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\Prefetch\*.pf")) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Windows regenerates prefetch data as needed; deleted files are not restored.' } $targets += [pscustomobject]@{ Name = 'Thumbnail cache'; Paths = @("$env:LOCALAPPDATA\Microsoft\Windows\Explorer\thumbcache_*.db") MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:LOCALAPPDATA\Microsoft\Windows\Explorer\thumbcache_*.db")) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Thumbnails are regenerated on demand; deleted cache files are not restored.' } $targets += [pscustomobject]@{ Name = 'Windows Update leftovers'; Paths = @("$env:WINDIR\SoftwareDistribution\Download\*") MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\SoftwareDistribution\Download\*")) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Windows re-downloads update files if needed again.' } $targets += [pscustomobject]@{ Name = 'Crash dumps'; Paths = @("$env:WINDIR\Minidump\*", "$env:WINDIR\memory.dmp") MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\Minidump\*", "$env:WINDIR\memory.dmp")) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Crash dump files are diagnostic-only; deleting them loses that diagnostic history.' } $recycleBinBytes = 0L try { $shell = New-Object -ComObject Shell.Application $recycleBin = $shell.Namespace(10) $recycleBinBytes = ($recycleBin.Items() | ForEach-Object { $_.Size } | Measure-Object -Sum).Sum } catch { $recycleBinBytes = 0L } $targets += [pscustomobject]@{ Name = 'Recycle Bin'; Paths = @('(Recycle Bin)') MeasuredBytes = $recycleBinBytes RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Emptying the Recycle Bin permanently deletes its contents.' } # 007: browser caches - each measured only if that browser is actually installed, never # shown as zero for an absent one. $browserCaches = @( @{ Name = 'Edge cache'; Path = "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Cache" }, @{ Name = 'Chrome cache'; Path = "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Cache" }, @{ Name = 'Firefox cache'; Path = "$env:APPDATA\Mozilla\Firefox\Profiles" } ) foreach ($bc in $browserCaches) { if ($bc.Name -eq 'Firefox cache') { $profileDirs = @(Get-ChildItem -Path $bc.Path -Directory -Filter '*.default*' -ErrorAction SilentlyContinue) if ($profileDirs.Count -eq 0) { continue } $cachePaths = $profileDirs | ForEach-Object { Join-Path $_.FullName 'cache2\*' } } else { if (-not (Test-Path $bc.Path)) { continue } $cachePaths = @("$($bc.Path)\*") } $targets += [pscustomobject]@{ Name = $bc.Name; Paths = $cachePaths MeasuredBytes = (Measure-OctaPathBytes -Paths $cachePaths) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Browser caches are regenerated as needed; deleted cache files are not restored.' } } # 009/Kudu parity: gaming platform caches - each independently existence-gated, same pattern # as browser caches above (never shown for a platform that isn't installed). Paths verified # against public vendor documentation (research.md); Steam/Epic not live-tested on the dev # machine (neither installed there) - closed out via VM validation instead. $steamInstallPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Valve\Steam' -ErrorAction SilentlyContinue).InstallPath $gamingCaches = @( @{ Name = 'Steam shader cache'; Path = if ($steamInstallPath) { Join-Path $steamInstallPath 'steamapps\shadercache' } else { $null } }, @{ Name = 'Epic Games Launcher cache'; Path = "$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache" }, @{ Name = 'NVIDIA DirectX shader cache'; Path = "$env:LOCALAPPDATA\NVIDIA\DXCache" }, @{ Name = 'NVIDIA OpenGL shader cache'; Path = "$env:LOCALAPPDATA\NVIDIA\GLCache" } ) foreach ($gc in $gamingCaches) { if (-not $gc.Path -or -not (Test-Path -LiteralPath $gc.Path)) { continue } $targets += [pscustomobject]@{ Name = $gc.Name; Paths = @("$($gc.Path)\*") MeasuredBytes = (Measure-OctaPathBytes -Paths @("$($gc.Path)\*")) RiskLevel = 'Safe'; Reversible = $false IrreversibleReason = 'Shader/launcher caches are regenerated as needed; deleted cache files are not restored.' } } $windowsOldPath = "$env:SystemDrive\Windows.old" if (Test-Path $windowsOldPath) { $targets += [pscustomobject]@{ Name = 'Old Windows installation (Windows.old)'; Paths = @($windowsOldPath) MeasuredBytes = (Measure-OctaPathBytes -Paths @("$windowsOldPath\*")) RiskLevel = 'Risky'; Reversible = $false IrreversibleReason = 'Removing this forfeits the ability to roll back a Windows feature upgrade.' } } return $targets } function Clear-OctaFileSecurely { <# .SYNOPSIS Overwrites a file's content with random bytes, streamed in bounded-size chunks (never loads the whole file into memory), before the caller deletes it normally. 007 US3. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Path) try { $stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Write) try { $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $buffer = New-Object byte[] 65536 $remaining = $stream.Length $stream.Position = 0 while ($remaining -gt 0) { $chunk = [Math]::Min($buffer.Length, $remaining) $rng.GetBytes($buffer, 0, $chunk) $stream.Write($buffer, 0, $chunk) $remaining -= $chunk } $stream.Flush() } finally { $stream.Dispose() } } catch { # Locked/inaccessible file - left for the normal Remove-Item pass to skip/report. } } function Invoke-OctaDiskCleanup { <# .SYNOPSIS Dry-run and (optionally) apply disk cleanup across known, standard Windows locations. FR-045/FR-046. Attempts a System Restore Point before deleting anything, even though the deletions themselves are not undo-able the way a registry action is. .PARAMETER SecureDelete 007 US3: overwrites each file's content before deleting it, instead of a normal delete that leaves recoverable data on disk. Slower - discloses this before the user confirms. #> [CmdletBinding()] param( [switch]$Apply, [switch]$Yes, [switch]$SecureDelete ) $targets = Get-OctaCleanupTargets $totalBytes = ($targets | Measure-Object -Property MeasuredBytes -Sum).Sum Write-Host 'Disk Cleanup targets (all irreversible except where noted):' foreach ($t in $targets) { $gb = [Math]::Round($t.MeasuredBytes / 1GB, 2) Write-Host (" {0,-40} {1,8} GB [{2}]" -f $t.Name, $gb, $t.RiskLevel) } Write-Host ("Total reclaimable: {0} GB" -f [Math]::Round($totalBytes / 1GB, 2)) if (-not $Apply) { return [pscustomobject]@{ Status = 'DryRun'; Targets = $targets; TotalBytes = $totalBytes } } $safeTargets = @($targets | Where-Object { $_.RiskLevel -ne 'Risky' }) $riskyTargets = @($targets | Where-Object { $_.RiskLevel -eq 'Risky' }) if ($SecureDelete) { Write-Host 'Secure delete is on: files will be overwritten before removal (slower).' } if (-not $Yes) { $response = Read-Host 'Delete these files? (y/N)' if ($response -notin @('y', 'Y', 's', 'S')) { return [pscustomobject]@{ Status = 'Cancelled' } } } $applyTargets = $safeTargets if ($riskyTargets.Count -gt 0) { Write-Host 'Risky targets (confirm separately):' foreach ($t in $riskyTargets) { Write-Host " $($t.Name)" } if ($Yes) { $applyTargets += $riskyTargets } else { $riskyResponse = Read-Host 'Delete these too? (y/N)' if ($riskyResponse -in @('y', 'Y', 's', 'S')) { $applyTargets += $riskyTargets } } } New-OctaRestorePoint -Description 'Octa disk cleanup' | Out-Null $freedBytes = 0L foreach ($t in $applyTargets) { if ($t.Name -eq 'Recycle Bin') { Clear-RecycleBin -Force -ErrorAction SilentlyContinue } else { foreach ($p in $t.Paths) { if ($SecureDelete) { Get-ChildItem -Path $p -Recurse -File -Force -ErrorAction SilentlyContinue | ForEach-Object { Clear-OctaFileSecurely -Path $_.FullName } } Remove-Item -Path $p -Recurse -Force -ErrorAction SilentlyContinue } } $freedBytes += $t.MeasuredBytes } return [pscustomobject]@{ Status = 'Success'; FreedBytes = $freedBytes } } |