Public/system/Get-DiskCleanupInfo.ps1
|
#Requires -Version 5.1 function Get-DiskCleanupInfo { <# .SYNOPSIS Scans a Windows computer and reports what can be cleaned up without deleting anything .DESCRIPTION Analyzes multiple cleanup categories on local or remote Windows computers and returns size information for each category. Categories include temporary files, Windows Update cache, Recycle Bin, crash dumps, old logs, browser caches, Windows.old, and thumbnail caches. No files are deleted. .PARAMETER ComputerName One or more computer names to scan. Defaults to the local computer. Accepts pipeline input by value and by property name. .PARAMETER Category One or more cleanup categories to scan. Valid values are TempFiles, WindowsUpdate, RecycleBin, CrashDumps, OldLogs, BrowserCache, WindowsOld, ThumbnailCache, and All. Defaults to All. .PARAMETER OlderThanDays Number of days used to filter the TempFiles and OldLogs categories. Only files older than this threshold are reported for those categories. Defaults to 30. Has no effect on other categories. .PARAMETER Credential Optional PSCredential for authenticating to remote computers. Not used for local queries. .EXAMPLE Get-DiskCleanupInfo Scans all cleanup categories on the local computer. .EXAMPLE Get-DiskCleanupInfo -ComputerName 'SRV01' -Category 'TempFiles', 'OldLogs' Scans only TempFiles and OldLogs categories on remote server SRV01. .EXAMPLE 'SRV01', 'SRV02' | Get-DiskCleanupInfo -Category 'BrowserCache' Scans browser cache on multiple remote servers via pipeline. .OUTPUTS PSWinOps.DiskCleanupInfo Returns one object per cleanup category per computer with file count, size in bytes and megabytes, and oldest/newest file timestamps. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-04-10 Requires: PowerShell 5.1+ / Windows only Requires: Administrator privileges for full scan accuracy .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands #> [CmdletBinding()] [OutputType('PSWinOps.DiskCleanupInfo')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ValidateSet('TempFiles', 'WindowsUpdate', 'RecycleBin', 'CrashDumps', 'OldLogs', 'BrowserCache', 'WindowsOld', 'ThumbnailCache', 'All')] [string[]]$Category = 'All', [Parameter(Mandatory = $false)] [ValidateRange(1, 3650)] [int]$OlderThanDays = 30, [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" $allCategories = @( 'TempFiles' 'WindowsUpdate' 'RecycleBin' 'CrashDumps' 'OldLogs' 'BrowserCache' 'WindowsOld' 'ThumbnailCache' ) if ($Category -contains 'All') { $resolvedCategories = $allCategories } else { $resolvedCategories = $Category } Write-Verbose -Message "[$($MyInvocation.MyCommand)] Categories: $($resolvedCategories -join ', ')" $scanBlock = { param( [string[]]$CategoriesToScan, [int]$LogAgeDays ) $cutoffDate = (Get-Date).AddDays(-$LogAgeDays) # Helper: measure a file collection and return a result hashtable function Measure-FileCollection { param( [string]$CategoryName, [string]$BasePath, [object[]]$FileList ) $fileCount = 0 $sizeBytes = [long]0 $oldestFile = $null $newestFile = $null if ($FileList -and $FileList.Count -gt 0) { $measure = $FileList | Measure-Object -Property Length -Sum $fileCount = $measure.Count $sizeBytes = [long]$measure.Sum $sorted = $FileList | Sort-Object -Property LastWriteTime $oldestFile = $sorted[0].LastWriteTime $newestFile = $sorted[-1].LastWriteTime } @{ Category = $CategoryName Path = $BasePath FileCount = [int]$fileCount SizeBytes = $sizeBytes SizeMB = [math]::Round($sizeBytes / 1MB, 2) OldestFile = $oldestFile NewestFile = $newestFile } } $results = [System.Collections.Generic.List[hashtable]]::new() # --- TempFiles --- if ($CategoriesToScan -contains 'TempFiles') { $tempPaths = @( $env:TEMP (Join-Path -Path $env:SystemRoot -ChildPath 'Temp') ) foreach ($tempPath in $tempPaths) { if (Test-Path -LiteralPath $tempPath) { $files = @( Get-ChildItem -LiteralPath $tempPath -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.LastWriteTime -lt $cutoffDate } ) $results.Add((Measure-FileCollection -CategoryName 'TempFiles' -BasePath $tempPath -FileList $files)) } } } # --- WindowsUpdate --- if ($CategoriesToScan -contains 'WindowsUpdate') { $wuPath = Join-Path -Path $env:SystemRoot -ChildPath 'SoftwareDistribution\Download' if (Test-Path -LiteralPath $wuPath) { $files = @(Get-ChildItem -LiteralPath $wuPath -Recurse -File -Force -ErrorAction SilentlyContinue) $results.Add((Measure-FileCollection -CategoryName 'WindowsUpdate' -BasePath $wuPath -FileList $files)) } else { $results.Add((Measure-FileCollection -CategoryName 'WindowsUpdate' -BasePath $wuPath -FileList @())) } } # --- RecycleBin --- if ($CategoriesToScan -contains 'RecycleBin') { $recyclePath = Join-Path -Path $env:SystemDrive -ChildPath '$Recycle.Bin' $rbFiles = @(Get-ChildItem -LiteralPath $recyclePath -Recurse -File -Force -ErrorAction SilentlyContinue) $results.Add((Measure-FileCollection -CategoryName 'RecycleBin' -BasePath $recyclePath -FileList $rbFiles)) } # --- CrashDumps --- if ($CategoriesToScan -contains 'CrashDumps') { $dumpFiles = [System.Collections.Generic.List[object]]::new() $dumpPaths = @( (Join-Path -Path $env:SystemRoot -ChildPath 'Minidump') (Join-Path -Path $env:SystemRoot -ChildPath 'LiveKernelReports') ) foreach ($dumpPath in $dumpPaths) { if (Test-Path -LiteralPath $dumpPath) { $found = @(Get-ChildItem -LiteralPath $dumpPath -Filter '*.dmp' -Recurse -File -Force -ErrorAction SilentlyContinue) foreach ($f in $found) { $dumpFiles.Add($f) } } } $memoryDmpPath = Join-Path -Path $env:SystemRoot -ChildPath 'MEMORY.DMP' if (Test-Path -LiteralPath $memoryDmpPath) { $memDmp = Get-Item -LiteralPath $memoryDmpPath -Force -ErrorAction SilentlyContinue if ($memDmp) { $dumpFiles.Add($memDmp) } } $results.Add((Measure-FileCollection -CategoryName 'CrashDumps' -BasePath (Join-Path -Path $env:SystemRoot -ChildPath 'Minidump') -FileList $dumpFiles.ToArray())) } # --- OldLogs --- if ($CategoriesToScan -contains 'OldLogs') { $logPaths = @( (Join-Path -Path $env:SystemRoot -ChildPath 'Logs') (Join-Path -Path $env:SystemRoot -ChildPath 'System32\LogFiles') ) $inetpubLogs = 'C:\inetpub\logs' if (Test-Path -LiteralPath $inetpubLogs) { $logPaths += $inetpubLogs } $allLogFiles = [System.Collections.Generic.List[object]]::new() foreach ($logPath in $logPaths) { if (Test-Path -LiteralPath $logPath) { $found = @( Get-ChildItem -LiteralPath $logPath -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object -FilterScript { ($_.Extension -eq '.log' -or $_.Extension -eq '.etl') -and $_.LastWriteTime -lt $cutoffDate } ) foreach ($f in $found) { $allLogFiles.Add($f) } } } $results.Add((Measure-FileCollection -CategoryName 'OldLogs' -BasePath (Join-Path -Path $env:SystemRoot -ChildPath 'Logs') -FileList $allLogFiles.ToArray())) } # --- BrowserCache --- if ($CategoriesToScan -contains 'BrowserCache') { $skipProfiles = @('Public', 'Default', 'Default User', 'All Users') $cacheRelatives = @( 'AppData\Local\Google\Chrome\User Data\*\Cache\*' 'AppData\Local\Microsoft\Edge\User Data\*\Cache\*' 'AppData\Local\Mozilla\Firefox\Profiles\*\cache2\*' ) $allCacheFiles = [System.Collections.Generic.List[object]]::new() $usersDir = Join-Path -Path $env:SystemDrive -ChildPath 'Users' if (Test-Path -LiteralPath $usersDir) { $userDirs = @( Get-ChildItem -LiteralPath $usersDir -Directory -Force -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.Name -notin $skipProfiles } ) foreach ($userDir in $userDirs) { foreach ($rel in $cacheRelatives) { $fullGlob = Join-Path -Path $userDir.FullName -ChildPath $rel $found = @(Get-ChildItem -Path $fullGlob -Recurse -File -Force -ErrorAction SilentlyContinue) foreach ($f in $found) { $allCacheFiles.Add($f) } } } } $results.Add((Measure-FileCollection -CategoryName 'BrowserCache' -BasePath "$usersDir\*\AppData\Local" -FileList $allCacheFiles.ToArray())) } # --- WindowsOld --- if ($CategoriesToScan -contains 'WindowsOld') { $windowsOldPath = Join-Path -Path $env:SystemDrive -ChildPath 'Windows.old' if (Test-Path -LiteralPath $windowsOldPath) { $files = @(Get-ChildItem -LiteralPath $windowsOldPath -Recurse -File -Force -ErrorAction SilentlyContinue) $results.Add((Measure-FileCollection -CategoryName 'WindowsOld' -BasePath $windowsOldPath -FileList $files)) } } # --- ThumbnailCache --- if ($CategoriesToScan -contains 'ThumbnailCache') { $thumbPattern = Join-Path -Path $env:SystemDrive -ChildPath 'Users\*\AppData\Local\Microsoft\Windows\Explorer\thumbcache_*.db' $thumbFiles = @(Get-ChildItem -Path $thumbPattern -File -Force -ErrorAction SilentlyContinue) $results.Add((Measure-FileCollection -CategoryName 'ThumbnailCache' -BasePath "$env:SystemDrive\Users\*\AppData\Local\Microsoft\Windows\Explorer" -FileList $thumbFiles)) } $results.ToArray() } } process { foreach ($machine in $ComputerName) { try { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Scanning '$machine'" # Build ArgumentList explicitly to prevent array flattening # when $resolvedCategories contains multiple elements $scanArgs = [object[]]::new(2) $scanArgs[0] = [string[]]$resolvedCategories $scanArgs[1] = [int]$OlderThanDays $invokeParams = @{ ComputerName = $machine ScriptBlock = $scanBlock ArgumentList = $scanArgs } if ($PSBoundParameters.ContainsKey('Credential')) { $invokeParams['Credential'] = $Credential } $rawResults = @(Invoke-RemoteOrLocal @invokeParams) foreach ($raw in $rawResults) { [PSCustomObject]@{ PSTypeName = 'PSWinOps.DiskCleanupInfo' ComputerName = $machine Category = $raw.Category Path = $raw.Path FileCount = [int]$raw.FileCount SizeBytes = [long]$raw.SizeBytes SizeMB = [double]$raw.SizeMB OldestFile = $raw.OldestFile NewestFile = $raw.NewestFile Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' } } } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_" continue } } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |