<# .SYNOPSIS Windows-Wartungsskript - bereinigt Temp, Caches, Logs, WU-Cache, Papierkorb usw. .DESCRIPTION Laeuft lokal auf Clients und Servern. Schreibt ein ausfuehrliches Transcript-Log nach C:\ProgramData\Maintenance\Logs und gibt ein Stats-Objekt zurueck, das vom zentralen Runner aggregiert werden kann. .PARAMETER MinFileAgeDays Dateien, die juenger als X Tage sind, werden ausgelassen (Default 1). .PARAMETER SkipDISM Ueberspringt DISM /StartComponentCleanup /ResetBase (langlaufend, irreversibel). .PARAMETER SkipCleanMgr Ueberspringt cleanmgr /sagerun:1. .PARAMETER SkipBrowserCache Ueberspringt Edge/Chrome/Firefox-Caches. .PARAMETER LogRoot Zielverzeichnis fuer Logs (Default C:\ProgramData\Maintenance\Logs). .EXAMPLE .\Invoke-Maintenance.ps1 .\Invoke-Maintenance.ps1 -SkipDISM -SkipCleanMgr #> [CmdletBinding()] param( [int]$MinFileAgeDays = 1, [switch]$SkipDISM, [switch]$SkipCleanMgr, [switch]$SkipBrowserCache, [string]$LogRoot = "C:\ProgramData\Maintenance\Logs" ) #region --- Setup --------------------------------------------------------------- $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' if (-not (Test-Path $LogRoot)) { New-Item -ItemType Directory -Path $LogRoot -Force | Out-Null } $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' $hostName = $env:COMPUTERNAME $logFile = Join-Path $LogRoot "$hostName`_$stamp.log" $statsFile = Join-Path $LogRoot "$hostName`_$stamp.stats.json" Start-Transcript -Path $logFile -Force | Out-Null $script:Stats = [ordered]@{ ComputerName = $hostName StartedAt = (Get-Date).ToString('s') Steps = @() TotalFreedMB = 0 Errors = 0 } function Write-Section($Title) { Write-Host "" Write-Host ("=" * 78) Write-Host (" {0}" -f $Title) Write-Host ("=" * 78) } function Get-FolderSizeMB { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return 0 } try { $sum = (Get-ChildItem -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum if ($null -eq $sum) { return 0 } return [math]::Round($sum / 1MB, 2) } catch { return 0 } } function Remove-ContentSafely { <# Loescht den Inhalt eines Ordners, ueberspringt gesperrte Dateien, respektiert MinFileAgeDays. Gibt freigegebene MB zurueck. #> param( [Parameter(Mandatory)] [string]$Path, [int]$MinAgeDays = $MinFileAgeDays ) if (-not (Test-Path -LiteralPath $Path)) { return 0 } $before = Get-FolderSizeMB -Path $Path $cutoff = (Get-Date).AddDays(-$MinAgeDays) $deleted = 0; $skipped = 0 Get-ChildItem -LiteralPath $Path -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer -and $_.LastWriteTime -lt $cutoff } | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop; $deleted++ } catch { $skipped++ } } # Leere Unterordner entfernen Get-ChildItem -LiteralPath $Path -Force -Recurse -Directory -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | ForEach-Object { if (-not (Get-ChildItem -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue)) { try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch {} } } $after = Get-FolderSizeMB -Path $Path $freed = [math]::Max(0, $before - $after) Write-Host (" {0}: -{1} MB (vorher {2} MB, gesperrt: {3})" -f $Path, $freed, $before, $skipped) return $freed } function Add-Step($Name, $FreedMB, $Note = "") { $script:Stats.Steps += [pscustomobject]@{ Step = $Name FreedMB = [math]::Round($FreedMB, 2) Note = $Note } $script:Stats.TotalFreedMB = [math]::Round($script:Stats.TotalFreedMB + $FreedMB, 2) } #endregion Write-Section "Windows-Wartung auf $hostName - $stamp" #region --- 1. Windows Temp ---------------------------------------------------- Write-Section "1. Windows Temp" $f = 0 $f += Remove-ContentSafely -Path "$env:windir\Temp" $f += Remove-ContentSafely -Path "$env:SystemDrive\Temp" -MinAgeDays $MinFileAgeDays Add-Step "WindowsTemp" $f #endregion #region --- 2. User Temps / Caches -------------------------------------------- Write-Section "2. User Temps / Caches (alle Profile)" $f = 0 $profiles = Get-ChildItem 'C:\Users' -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin @('Public','Default','Default User','All Users') } foreach ($p in $profiles) { $targets = @( "$($p.FullName)\AppData\Local\Temp", "$($p.FullName)\AppData\Local\Microsoft\Windows\INetCache", "$($p.FullName)\AppData\Local\Microsoft\Windows\WebCache", "$($p.FullName)\AppData\Local\Microsoft\Windows\Explorer" # Thumbnail-Cache ) foreach ($t in $targets) { $f += Remove-ContentSafely -Path $t } } Add-Step "UserTemps" $f #endregion #region --- 3. Browser-Caches -------------------------------------------------- if (-not $SkipBrowserCache) { Write-Section "3. Browser-Caches (Edge / Chrome / Firefox)" $f = 0 foreach ($p in $profiles) { $browserPaths = @( "$($p.FullName)\AppData\Local\Microsoft\Edge\User Data\Default\Cache", "$($p.FullName)\AppData\Local\Microsoft\Edge\User Data\Default\Code Cache", "$($p.FullName)\AppData\Local\Google\Chrome\User Data\Default\Cache", "$($p.FullName)\AppData\Local\Google\Chrome\User Data\Default\Code Cache" ) foreach ($b in $browserPaths) { $f += Remove-ContentSafely -Path $b } # Firefox: Cache liegt unter zufaelligem Profilordner $ffRoot = "$($p.FullName)\AppData\Local\Mozilla\Firefox\Profiles" if (Test-Path $ffRoot) { Get-ChildItem $ffRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object { $f += Remove-ContentSafely -Path (Join-Path $_.FullName 'cache2') } } } Add-Step "BrowserCaches" $f } #endregion #region --- 4. Windows Update Cache ------------------------------------------- Write-Section "4. Windows Update Cache (SoftwareDistribution\Download)" $f = 0 try { Stop-Service -Name wuauserv,bits,cryptsvc -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 $f = Remove-ContentSafely -Path "$env:windir\SoftwareDistribution\Download" -MinAgeDays 0 } finally { Start-Service -Name wuauserv,bits,cryptsvc -ErrorAction SilentlyContinue } Add-Step "WindowsUpdateCache" $f #endregion #region --- 5. CBS / Panther / Setup-Logs ------------------------------------- Write-Section "5. CBS / Panther / Setup-Logs" $f = 0 $f += Remove-ContentSafely -Path "$env:windir\Logs\CBS" -MinAgeDays 7 $f += Remove-ContentSafely -Path "$env:windir\Panther" -MinAgeDays 7 $f += Remove-ContentSafely -Path "$env:windir\Logs\DISM" -MinAgeDays 7 $f += Remove-ContentSafely -Path "$env:windir\Logs\WindowsUpdate" -MinAgeDays 7 Add-Step "SystemLogs" $f #endregion #region --- 6. Prefetch -------------------------------------------------------- Write-Section "6. Prefetch" $f = Remove-ContentSafely -Path "$env:windir\Prefetch" -MinAgeDays 7 Add-Step "Prefetch" $f #endregion #region --- 7. Delivery Optimization Cache ------------------------------------ Write-Section "7. Delivery Optimization Cache" $f = Remove-ContentSafely -Path "$env:windir\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization\Cache" -MinAgeDays 0 Add-Step "DeliveryOptimization" $f #endregion #region --- 8. WER / CrashDumps ----------------------------------------------- Write-Section "8. WER / CrashDumps / MEMORY.DMP" $f = 0 $f += Remove-ContentSafely -Path "$env:ProgramData\Microsoft\Windows\WER\ReportQueue" -MinAgeDays 1 $f += Remove-ContentSafely -Path "$env:ProgramData\Microsoft\Windows\WER\ReportArchive" -MinAgeDays 7 $f += Remove-ContentSafely -Path "$env:ProgramData\Microsoft\Windows\WER\Temp" -MinAgeDays 1 $f += Remove-ContentSafely -Path "$env:LOCALAPPDATA\CrashDumps" -MinAgeDays 7 if (Test-Path "$env:windir\MEMORY.DMP") { $sz = (Get-Item "$env:windir\MEMORY.DMP").Length / 1MB try { Remove-Item "$env:windir\MEMORY.DMP" -Force -ErrorAction Stop; $f += $sz } catch {} } $f += Remove-ContentSafely -Path "$env:windir\Minidump" -MinAgeDays 7 Add-Step "CrashDumps" $f #endregion #region --- 9. IIS-Logs (falls vorhanden) ------------------------------------- if (Test-Path "$env:SystemDrive\inetpub\logs\LogFiles") { Write-Section "9. IIS-Logs (aelter 30 Tage)" $cutoff = (Get-Date).AddDays(-30) $f = 0; $before = Get-FolderSizeMB "$env:SystemDrive\inetpub\logs\LogFiles" Get-ChildItem "$env:SystemDrive\inetpub\logs\LogFiles" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | ForEach-Object { try { Remove-Item $_.FullName -Force -ErrorAction Stop } catch {} } $after = Get-FolderSizeMB "$env:SystemDrive\inetpub\logs\LogFiles" $f = [math]::Max(0, $before - $after) Write-Host (" IIS-Logs: -{0} MB" -f $f) Add-Step "IISLogs" $f } #endregion #region --- 10. Papierkorb (alle Laufwerke) ----------------------------------- Write-Section "10. Papierkorb" $f = 0 try { # Groesse vorher ueber Shell ermitteln ist umstaendlich - wir nutzen Clear-RecycleBin Clear-RecycleBin -Force -ErrorAction Stop Write-Host " Papierkorb geleert." } catch { Write-Host " Papierkorb-Clear fehlgeschlagen: $($_.Exception.Message)" $script:Stats.Errors++ } Add-Step "RecycleBin" $f "geleert (Groessenmessung n/a)" #endregion #region --- 11. DISM Component Cleanup ---------------------------------------- if (-not $SkipDISM) { Write-Section "11. DISM /StartComponentCleanup /ResetBase (langlaufend, irreversibel)" try { $winsxsBefore = Get-FolderSizeMB "$env:windir\WinSxS" & dism.exe /Online /Cleanup-Image /StartComponentCleanup /ResetBase | Out-Host $winsxsAfter = Get-FolderSizeMB "$env:windir\WinSxS" $f = [math]::Max(0, $winsxsBefore - $winsxsAfter) Add-Step "DISM" $f } catch { Write-Host " DISM-Fehler: $($_.Exception.Message)" $script:Stats.Errors++ Add-Step "DISM" 0 "Fehler" } } else { Add-Step "DISM" 0 "uebersprungen" } #endregion #region --- 12. cleanmgr /sagerun:1 ------------------------------------------- if (-not $SkipCleanMgr) { Write-Section "12. cleanmgr /sagerun:1" # Standard-Profil 1 anlegen, falls nicht vorhanden $regBase = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches' if (Test-Path $regBase) { Get-ChildItem $regBase | ForEach-Object { try { New-ItemProperty -Path $_.PSPath -Name 'StateFlags0001' -Value 2 -PropertyType DWord -Force -ErrorAction SilentlyContinue | Out-Null } catch {} } } try { Start-Process -FilePath "$env:windir\System32\cleanmgr.exe" -ArgumentList '/sagerun:1' -Wait -WindowStyle Hidden Write-Host " cleanmgr beendet." Add-Step "CleanMgr" 0 "ausgefuehrt" } catch { Write-Host " cleanmgr-Fehler: $($_.Exception.Message)" $script:Stats.Errors++ Add-Step "CleanMgr" 0 "Fehler" } } else { Add-Step "CleanMgr" 0 "uebersprungen" } #endregion #region --- 13. Eigene alte Logs aufraeumen (>30 Tage) ------------------------ Write-Section "13. Wartungs-Logs > 30 Tage rotieren" Get-ChildItem $LogRoot -File -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } | ForEach-Object { try { Remove-Item $_.FullName -Force -ErrorAction Stop } catch {} } #endregion #region --- Abschluss --------------------------------------------------------- $script:Stats.EndedAt = (Get-Date).ToString('s') Write-Section "Zusammenfassung" $script:Stats.Steps | Format-Table Step, FreedMB, Note -AutoSize | Out-Host Write-Host ("Gesamt freigegeben: {0} MB ({1} GB)" -f $script:Stats.TotalFreedMB, [math]::Round($script:Stats.TotalFreedMB/1024,2)) Write-Host ("Fehler: {0}" -f $script:Stats.Errors) $script:Stats | ConvertTo-Json -Depth 5 | Set-Content -Path $statsFile -Encoding UTF8 Stop-Transcript | Out-Null # Stats-Objekt fuer Remoting zurueckgeben [pscustomobject]$script:Stats #endregion