<# .SYNOPSIS DailyCleanup.ps1 - Systembereinigung .DESCRIPTION Sicheres Systembereinigungsskript fuer Windows PowerShell 5.1+. Sicherheitsmodell: - Standardmaessig werden Dateien tatsaechlich geloescht. - Mit -DryRun laeuft das Skript als reine Vorschau (es wird nichts geloescht). Das Skript bereinigt temporaere Verzeichnisse, Caches, Crash-Dumps, den Windows-Update-Cache, den Papierkorb und optional die Ereignisprotokolle. Jeder Lauf erzeugt eine Logdatei sowie einen CSV-Detailbericht. .PARAMETER DryRun Fuehrt KEINE Loeschoperationen durch, sondern zeigt nur an, was bereinigt wuerde (Vorschau). Ohne diesen Schalter wird tatsaechlich geloescht. .PARAMETER ClearEventLogs Loescht alle Windows-Ereignisprotokolle (erfordert Admin + -Execute). .PARAMETER AllUserProfiles Bereinigt TEMP-Verzeichnisse aller Benutzerprofile (erfordert Admin). .PARAMETER LogPath Pfad zur Logdatei (Standard: \DailyCleanup.log). .PARAMETER CsvReportPath Pfad fuer den CSV-Detailbericht (Standard: \DailyCleanup_Report.csv). .PARAMETER TempAgeDays Mindestalter in Tagen fuer TEMP-Dateien (Standard: 0 = alle). .PARAMETER UpdateAgeDays Mindestalter in Tagen fuer Windows-Update-Cache und Thumbnail-Cache (Standard: 7). .PARAMETER VerboseOutput Gibt alle Log-Eintraege (auch INFO) zusaetzlich in der Konsole aus. .EXAMPLE PS> .\DailyCleanup.ps1 Fuehrt die Bereinigung tatsaechlich aus (Standardverhalten). .EXAMPLE PS> .\DailyCleanup.ps1 -DryRun Vorschau - zeigt an, was bereinigt wuerde, ohne etwas zu loeschen. .EXAMPLE PS> .\DailyCleanup.ps1 -AllUserProfiles -ClearEventLogs Bereinigt alle Benutzerprofile und loescht die Ereignisprotokolle (erfordert eine Administrator-Sitzung). .NOTES Autor : Krapp GmbH Systemadministration, Michael Ostermann Copyright (C) 2026 Krapp Beteiligungsgesellschaft mbH Version : siehe $script:Version .LINK https://learn.microsoft.com/powershell/ #> #Requires -Version 5.1 [CmdletBinding()] param( [switch] $DryRun, [switch] $ClearEventLogs, [switch] $AllUserProfiles, [ValidateNotNullOrEmpty()] [string] $LogPath = (Join-Path -Path "$(if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).ProviderPath })" -ChildPath 'DailyCleanup.log'), [ValidateNotNullOrEmpty()] [string] $CsvReportPath = (Join-Path -Path "$(if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).ProviderPath })" -ChildPath 'DailyCleanup_Report.csv'), [ValidateRange(0, 3650)] [int] $TempAgeDays = 0, [ValidateRange(0, 3650)] [int] $UpdateAgeDays = 7, [switch] $VerboseOutput ) Set-StrictMode -Version Latest # Standardmaessig wird geloescht; -DryRun erzwingt die Vorschau. $Execute = -not $DryRun # ── Skript-Metadaten (zentrale Quelle) ──────────────────────────────────────── $script:Version = '4.0' $script:Name = 'Krapp Systembereinigung' $script:SessionId = ([guid]::NewGuid().ToString('N').Substring(0, 8)).ToUpperInvariant() $script:IsAdmin = $null # wird in Test-IsAdmin einmalig zwischengespeichert #region Hilfsfunktionen ──────────────────────────────────────────────────────── function Write-Log { <# .SYNOPSIS Schreibt eine Zeile in die Logdatei und optional in die Konsole. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'SUCCESS')] [string] $Level = 'INFO' ) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $line = '[{0}] [{1}] [{2}] {3}' -f $timestamp, $script:SessionId, $Level, $Message try { Add-Content -Path $LogPath -Value $line -Encoding UTF8 -ErrorAction Stop } catch { # Wenn das Log nicht schreibbar ist, wenigstens auf der Konsole warnen. Write-Warning "Logdatei nicht schreibbar ($LogPath): $($_.Exception.Message)" } if ($VerboseOutput -or $Level -in @('WARN', 'ERROR', 'SUCCESS')) { $color = switch ($Level) { 'WARN' { 'Yellow' } 'ERROR' { 'Red' } 'SUCCESS' { 'Green' } default { 'Gray' } } Write-Host $line -ForegroundColor $color } } function Test-IsAdmin { <# .SYNOPSIS Prueft, ob die aktuelle Sitzung Administratorrechte besitzt (Ergebnis wird zwischengespeichert). #> [CmdletBinding()] [OutputType([bool])] param() if ($null -eq $script:IsAdmin) { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) $script:IsAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } return $script:IsAdmin } function Format-Bytes { <# .SYNOPSIS Formatiert eine Byte-Anzahl menschenlesbar (B/KB/MB/GB/TB). #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [int64] $Bytes ) switch ($Bytes) { { $_ -ge 1TB } { return '{0:N2} TB' -f ($Bytes / 1TB) } { $_ -ge 1GB } { return '{0:N2} GB' -f ($Bytes / 1GB) } { $_ -ge 1MB } { return '{0:N2} MB' -f ($Bytes / 1MB) } { $_ -ge 1KB } { return '{0:N2} KB' -f ($Bytes / 1KB) } default { return "$Bytes B" } } } function Get-FilesToRemove { <# .SYNOPSIS Ermittelt die zu loeschenden Dateien eines Pfades. .DESCRIPTION Beruecksichtigt optional ein Mindestalter (Days) und/oder ein Dateinamensmuster (Pattern). Es werden ausschliesslich Dateien (keine Verzeichnisse) zurueckgegeben. #> [CmdletBinding()] [OutputType([System.IO.FileInfo[]])] param( [Parameter(Mandatory)] [string] $Path, [int] $Days = 0, [string] $Pattern = '' ) if (-not (Test-Path -LiteralPath $Path)) { return @() } try { $params = @{ Path = $Path Recurse = $true Force = $true File = $true ErrorAction = 'SilentlyContinue' } if ($Pattern) { $params['Include'] = $Pattern } $files = Get-ChildItem @params if ($Days -gt 0) { $cutoff = (Get-Date).AddDays(-$Days) $files = $files | Where-Object { $_.LastWriteTime -lt $cutoff } } return @($files) } catch { Write-Log "Fehler beim Auflisten von '$Path': $($_.Exception.Message)" 'ERROR' return @() } } function Remove-FileSet { <# .SYNOPSIS Loescht die uebergebenen Dateien und gibt die Summe der freigegebenen Bytes zurueck. #> [CmdletBinding()] [OutputType([int64])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [System.IO.FileInfo[]] $Files ) $freed = [int64]0 foreach ($file in $Files) { try { $size = [int64]$file.Length Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop $freed += $size } catch { # Dateien, die gerade von einem anderen Prozess verwendet werden, # sind ein normaler, harmloser Zustand -> nur als INFO protokollieren. $ex = $_.Exception $inUse = ($ex -is [System.IO.IOException]) -or ($ex.InnerException -is [System.IO.IOException]) -or ($ex.Message -match 'von einem anderen Prozess|being used by another process') $level = if ($inUse) { 'INFO' } else { 'WARN' } Write-Log "Konnte nicht loeschen: '$($file.FullName)' - $($ex.Message)" $level } } return $freed } function Get-FileSetSize { <# .SYNOPSIS Summiert die Groesse einer Dateiliste in Bytes. #> [CmdletBinding()] [OutputType([int64])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [System.IO.FileInfo[]] $Files ) if ($Files.Count -eq 0) { return [int64]0 } $sum = $Files | Measure-Object -Property Length -Sum if ($sum -and $sum.Sum) { return [int64]$sum.Sum } return [int64]0 } function Set-ServiceState { <# .SYNOPSIS Stoppt oder startet einen Dienst. .OUTPUTS [bool] - $true, wenn der Dienst durch diesen Aufruf gestoppt wurde (Hinweis fuer einen spaeteren Neustart). #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [string] $ServiceName, [Parameter(Mandatory)] [ValidateSet('Start', 'Stop')] [string] $Action ) try { $svc = Get-Service -Name $ServiceName -ErrorAction Stop if ($Action -eq 'Stop' -and $svc.Status -eq 'Running') { Write-Log "Stoppe Dienst: $ServiceName" Stop-Service -Name $ServiceName -Force -ErrorAction Stop return $true } if ($Action -eq 'Start' -and $svc.StartType -ne 'Disabled') { Write-Log "Starte Dienst: $ServiceName" Start-Service -Name $ServiceName -ErrorAction SilentlyContinue } } catch { Write-Log "Dienst '$ServiceName' konnte nicht gesteuert werden: $($_.Exception.Message)" 'WARN' } return $false } #endregion Hilfsfunktionen #region CSV-Report ────────────────────────────────────────────────────────────── $script:CsvRows = [System.Collections.Generic.List[psobject]]::new() function Add-CsvRow { [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Description, [Parameter(Mandatory)] [string] $Path, [int64] $FreedBytes = 0, [Parameter(Mandatory)] [string] $Status ) $script:CsvRows.Add([pscustomobject]@{ SessionID = $script:SessionId Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Description = $Description Path = $Path FreedBytes = $FreedBytes FreedHuman = Format-Bytes $FreedBytes Status = $Status Mode = if ($Execute) { 'Execute' } else { 'DryRun' } }) } function Export-CsvReport { <# .SYNOPSIS Schreibt den CSV-Report ohne UTF8-BOM (PS 5.1-kompatibel). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path ) try { # Export-Csv erzeugt unter PS 5.1 ein BOM; deshalb manuell ohne BOM schreiben. $csvText = $script:CsvRows | ConvertTo-Csv -NoTypeInformation $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllLines($Path, $csvText, $utf8NoBom) Write-Log "CSV-Report geschrieben: $Path" 'SUCCESS' } catch { Write-Log "Fehler beim Schreiben des CSV-Reports: $($_.Exception.Message)" 'ERROR' } } #endregion CSV-Report #region Bereinigungslogik ─────────────────────────────────────────────────────── function Invoke-CleanupTarget { <# .SYNOPSIS Verarbeitet ein einzelnes Bereinigungsziel (Dry-Run oder Execute). .OUTPUTS [int64] - freigegebene bzw. (im Dry-Run) geschaetzte Bytes. #> [CmdletBinding()] [OutputType([int64])] param( [Parameter(Mandatory)] [string] $Description, [Parameter(Mandatory)] [string] $Path, [int] $Days = 0, [string] $Pattern = '' ) Write-Log "Verarbeite: $Description | Pfad: $Path | Mindestalter: $Days Tage" if (-not (Test-Path -LiteralPath $Path)) { Write-Log "Pfad nicht gefunden: $Path" 'WARN' Add-CsvRow -Description $Description -Path $Path -Status 'PathNotFound' return [int64]0 } $files = @(Get-FilesToRemove -Path $Path -Days $Days -Pattern $Pattern) if ($files.Count -eq 0) { Write-Log "Nichts zu loeschen in: $Path" Add-CsvRow -Description $Description -Path $Path -Status 'NothingToDelete' return [int64]0 } if ($Execute) { $freed = Remove-FileSet -Files $files Write-Log "Bereinigt: $Description -> $(Format-Bytes $freed) freigegeben" 'SUCCESS' Add-CsvRow -Description $Description -Path $Path -FreedBytes $freed -Status 'Cleaned' return $freed } $estimate = Get-FileSetSize -Files $files Write-Host (' [DRY-RUN] {0,-28} : ca. {1} ({2} Dateien)' -f $Description, (Format-Bytes $estimate), $files.Count) -ForegroundColor DarkYellow Write-Log "Dry-Run: $Description - wuerde ca. $(Format-Bytes $estimate) entfernen ($($files.Count) Dateien)" Add-CsvRow -Description $Description -Path $Path -FreedBytes $estimate -Status 'DryRun' return $estimate } function Get-CleanupTargets { <# .SYNOPSIS Erstellt die Liste aller Bereinigungsziele abhaengig von den Parametern. #> [CmdletBinding()] [OutputType([hashtable[]])] param() $list = [System.Collections.Generic.List[hashtable]]::new() $list.Add(@{ Description = 'User TEMP'; Path = $env:TEMP; Days = $TempAgeDays; Pattern = '' }) $list.Add(@{ Description = 'LocalAppData Temp'; Path = "$env:LOCALAPPDATA\Temp"; Days = $TempAgeDays; Pattern = '' }) $list.Add(@{ Description = 'Windows Temp'; Path = "$env:SystemRoot\Temp"; Days = $TempAgeDays; Pattern = '' }) $list.Add(@{ Description = 'Thumbnail Cache'; Path = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer"; Days = $UpdateAgeDays; Pattern = 'thumbcache*' }) $list.Add(@{ Description = 'IE/Edge Cache'; Path = "$env:LOCALAPPDATA\Microsoft\Windows\INetCache"; Days = $TempAgeDays; Pattern = '' }) $list.Add(@{ Description = 'Crash Dumps (User)'; Path = "$env:LOCALAPPDATA\CrashDumps"; Days = 0; Pattern = '' }) $list.Add(@{ Description = 'LiveKernel Reports'; Path = "$env:SystemRoot\LiveKernelReports"; Days = 0; Pattern = '' }) $list.Add(@{ Description = 'Minidumps'; Path = "$env:SystemRoot\Minidump"; Days = 0; Pattern = '' }) if ($AllUserProfiles) { if (-not (Test-IsAdmin)) { Write-Log "-AllUserProfiles erfordert Administratorrechte - wird uebersprungen." 'WARN' } else { $profileRoot = "$env:SystemDrive\Users" $skipNames = @('Public', 'Default', 'Default User', 'All Users') $profiles = Get-ChildItem -Path $profileRoot -Directory -ErrorAction SilentlyContinue | Where-Object { $skipNames -notcontains $_.Name } foreach ($userProfile in $profiles) { $userName = $userProfile.Name $list.Add(@{ Description = "TEMP [$userName]"; Path = "$($userProfile.FullName)\AppData\Local\Temp"; Days = $TempAgeDays; Pattern = '' }) $list.Add(@{ Description = "CrashDumps [$userName]"; Path = "$($userProfile.FullName)\AppData\Local\CrashDumps"; Days = 0; Pattern = '' }) } } } if (Test-IsAdmin) { $list.Add(@{ Description = 'Windows Update Cache'; Path = "$env:SystemRoot\SoftwareDistribution\Download"; Days = $UpdateAgeDays; Pattern = '' }) } else { Write-Log "Windows Update Cache uebersprungen (keine Administratorrechte)." 'WARN' } # Doppelte Ziele entfernen, die auf denselben physischen Pfad zeigen # (z. B. %TEMP% und %LOCALAPPDATA%\Temp sind unter Windows identisch). # Verhindert doppelte Warnungen und unnoetige Zweitlaeufe (0 B freigegeben). $deduped = [System.Collections.Generic.List[hashtable]]::new() $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($entry in $list) { $rawPath = [string]$entry['Path'] if ([string]::IsNullOrWhiteSpace($rawPath)) { continue } try { $key = [System.IO.Path]::GetFullPath($rawPath).TrimEnd('\') } catch { $key = $rawPath.TrimEnd('\') } if ($seen.Add($key)) { $deduped.Add($entry) } } return $deduped.ToArray() } function Clear-RecycleBinSafe { <# .SYNOPSIS Leert den Papierkorb (Dry-Run-faehig). #> [CmdletBinding()] param() Write-Log "Verarbeite: Papierkorb" if (-not $Execute) { Write-Host " [DRY-RUN] Papierkorb wuerde geleert." -ForegroundColor DarkYellow Write-Log "Dry-Run: Papierkorb wuerde geleert." Add-CsvRow -Description 'Papierkorb' -Path 'RecycleBin' -Status 'DryRun' return } try { Clear-RecycleBin -Force -ErrorAction Stop Write-Log "Papierkorb geleert." 'SUCCESS' Add-CsvRow -Description 'Papierkorb' -Path 'RecycleBin' -Status 'Cleaned' } catch [System.Management.Automation.ItemNotFoundException] { # Papierkorb war bereits leer. Write-Log "Papierkorb war bereits leer." 'INFO' Add-CsvRow -Description 'Papierkorb' -Path 'RecycleBin' -Status 'NothingToDelete' } catch { Write-Log "Fehler beim Leeren des Papierkorbs: $($_.Exception.Message)" 'ERROR' Add-CsvRow -Description 'Papierkorb' -Path 'RecycleBin' -Status 'Error' } } function Clear-AllEventLogs { <# .SYNOPSIS Loescht alle Windows-Ereignisprotokolle (irreversibel). #> [CmdletBinding()] param() Write-Log "Verarbeite: Event Logs" if (-not (Test-IsAdmin)) { Write-Log "Event Logs erfordern Administratorrechte - uebersprungen." 'WARN' Add-CsvRow -Description 'Event Logs' -Path 'EventLog' -Status 'SkippedNoAdmin' return } if (-not $Execute) { Write-Host " [DRY-RUN] Alle Windows-Ereignisprotokolle wuerden geloescht." -ForegroundColor DarkYellow Write-Log "Dry-Run: Alle Ereignisprotokolle wuerden geloescht." Add-CsvRow -Description 'Event Logs' -Path 'EventLog' -Status 'DryRun' return } Write-Log "Loesche alle Windows-Ereignisprotokolle (nicht umkehrbar)." 'WARN' $logNames = wevtutil el 2>$null $cleared = 0 foreach ($logName in $logNames) { try { wevtutil cl "$logName" 2>$null $cleared++ } catch { Write-Log "Fehler beim Loeschen von '$logName': $($_.Exception.Message)" 'WARN' } } Write-Log "Event Logs bereinigt ($cleared Protokolle)." 'SUCCESS' Add-CsvRow -Description 'Event Logs' -Path 'EventLog' -Status "Cleaned ($cleared)" } #endregion Bereinigungslogik #region Hauptprogramm ─────────────────────────────────────────────────────────── # Logdatei-Verzeichnis sicherstellen. try { $logDir = Split-Path -Path $LogPath -Parent if ($logDir -and -not (Test-Path -LiteralPath $logDir)) { New-Item -Path $logDir -ItemType Directory -Force -ErrorAction Stop | Out-Null } } catch { Write-Warning "Log-Verzeichnis konnte nicht erstellt werden: $($_.Exception.Message)" } $modeText = if ($Execute) { 'EXECUTE' } else { 'DRY-RUN' } $adminText = if (Test-IsAdmin) { 'Ja' } else { 'Nein' } Write-Host '' Write-Host '============================================================' -ForegroundColor Cyan Write-Host " $script:Name v$script:Version" -ForegroundColor Cyan Write-Host " Session : $script:SessionId" -ForegroundColor Cyan Write-Host " Modus : $modeText" -ForegroundColor Cyan Write-Host " Datum : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan Write-Host " Admin : $adminText" -ForegroundColor Cyan Write-Host '============================================================' -ForegroundColor Cyan Write-Host '' if (-not $Execute) { Write-Host ' HINWEIS: Vorschau-Modus (-DryRun). Es wird nichts geloescht.' -ForegroundColor Yellow Write-Host '' } Write-Log "Skript gestartet. Version=$script:Version, Execute=$Execute, AllUserProfiles=$AllUserProfiles, ClearEventLogs=$ClearEventLogs" if (-not (Test-IsAdmin)) { Write-Log "Skript laeuft OHNE Administratorrechte - einige Operationen werden uebersprungen." 'WARN' } # Windows-Update-Dienst vor der Cache-Bereinigung stoppen. $wuServiceWasStopped = $false if ((Test-IsAdmin) -and $Execute) { $wuServiceWasStopped = Set-ServiceState -ServiceName 'wuauserv' -Action 'Stop' } # Bereinigungsziele verarbeiten. $targets = Get-CleanupTargets $totalBytes = [int64]0 Write-Host '-- Bereinigung wird verarbeitet --' -ForegroundColor Cyan foreach ($target in $targets) { $totalBytes += Invoke-CleanupTarget ` -Description $target['Description'] ` -Path $target['Path'] ` -Days $target['Days'] ` -Pattern $target['Pattern'] } # Papierkorb. Clear-RecycleBinSafe # Windows-Update-Dienst wieder starten. if ($wuServiceWasStopped) { $null = Set-ServiceState -ServiceName 'wuauserv' -Action 'Start' } # Event Logs (optional). if ($ClearEventLogs) { Clear-AllEventLogs } # CSV-Report schreiben. Export-CsvReport -Path $CsvReportPath # Abschlusszusammenfassung. $modeLabel = if ($Execute) { 'Freigegeben' } else { 'Wuerde freigeben (Dry-Run)' } Write-Host '' Write-Host '============================================================' -ForegroundColor Cyan Write-Host " ABSCHLUSSBERICHT - $script:Name v$script:Version" -ForegroundColor Cyan Write-Host " Session : $script:SessionId" -ForegroundColor Cyan Write-Host " $modeLabel : $(Format-Bytes $totalBytes)" -ForegroundColor Cyan Write-Host " Log : $LogPath" -ForegroundColor Cyan Write-Host " Report : $CsvReportPath" -ForegroundColor Cyan Write-Host '============================================================' -ForegroundColor Cyan Write-Host '' Write-Log "Skript abgeschlossen. ${modeLabel}: $(Format-Bytes $totalBytes)" 'SUCCESS' #endregion Hauptprogramm