<# .SYNOPSIS Removes files older than a specified number of days from a target directory. .DESCRIPTION Cleans up old files from a given path. Designed for high-volume deletion of small files (e.g. Tomcat temp, IIS logs). Supports recursive scan, logging, and progress reporting. .PARAMETER Path Mandatory. Target directory to clean up. .PARAMETER DaysOld Files older than this many days will be deleted. Default: 7. .PARAMETER Recurse If specified, subdirectories are also scanned. Default: enabled. .PARAMETER LogDir Directory for log output. Created if it does not exist. Default: C:\Temp. .PARAMETER LogEnabled Enable or disable logging. Default: $true. .EXAMPLE .\Invoke-OldFilePurge.ps1 -Path "C:\Program Files\Apache Software Foundation\Tomcat 9.0\temp" .EXAMPLE .\Invoke-OldFilePurge.ps1 -Path "D:\IIS\Logs" -DaysOld 30 -LogDir "D:\Logs" .NOTES Author: Petr Štěpán Created: 2026-07-02 Version: 1.0.1 Requires: PowerShell 5.1+ Changelog: 1.0.1 - Enforced minimum PowerShell version, enabled strict mode and stop-on-error, fixed $AllFiles.Count being unreliable on Windows PowerShell 5.1 when 0 or 1 file matched. 1.0.0 - Initial version #> #Requires -Version 5.1 [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, Position = 0, HelpMessage = "Path to the directory to clean up.")] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter()] [ValidateRange(1, 3650)] [int]$DaysOld = 7, [Parameter()] [switch]$Recurse = $true, [Parameter()] [string]$LogDir = "C:\Temp", [Parameter()] [bool]$LogEnabled = $true ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # --- Validate source path ----------------------------------------------- if (-not (Test-Path -LiteralPath $Path -PathType Container)) { Write-Error "Path '$Path' does not exist or is not a directory." return } # --- Prepare log file ----------------------------------------------------- if ($LogEnabled) { if (-not (Test-Path -LiteralPath $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null Write-Verbose "Log directory created: $LogDir" } $LogFile = Join-Path $LogDir ("FileCleanup_{0}.log" -f (Get-Date -Format 'yyyyMMdd_HHmmss')) } function Write-Log { param([string]$Message, [string]$Level = "INFO") $line = "{0} [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level.PadRight(5), $Message if ($LogEnabled) { $line | Out-File -FilePath $LogFile -Append -Encoding UTF8 } Write-Verbose $line } # --- Collect target files -------------------------------------------------- $Cutoff = (Get-Date).AddDays(-$DaysOld) $GciParams = @{ LiteralPath = $Path File = $true Recurse = $Recurse.IsPresent Force = $true # include hidden/system files } Write-Log "=== Invoke-OldFilePurge started ===" Write-Log "Path : $Path" Write-Log "DaysOld : $DaysOld (cutoff: $($Cutoff.ToString('yyyy-MM-dd HH:mm:ss')))" Write-Log "Recurse : $($Recurse.IsPresent)" Write-Log "LogFile : $(if ($LogEnabled) { $LogFile } else { 'disabled' })" Write-Host "Scanning '$Path' for files older than $DaysOld days..." -ForegroundColor Cyan # Wrapped in @() so .Count is reliable on PS 5.1 even when 0 or exactly 1 file matches $AllFiles = @(Get-ChildItem @GciParams -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $Cutoff }) $Total = $AllFiles.Count if ($Total -eq 0) { Write-Host "No files found matching the criteria." -ForegroundColor Green Write-Log "No files matched. Exiting." return } Write-Log "Files to delete: $Total" # --- Batch deletion optimised for large numbers of small files ------------- # .NET IO is significantly faster than Remove-Item in a loop for many small files. $Deleted = 0 $Failed = 0 $BatchSize = 500 # update progress every N files $StopWatch = [System.Diagnostics.Stopwatch]::StartNew() foreach ($File in $AllFiles) { if ($PSCmdlet.ShouldProcess($File.FullName, "Delete")) { try { [System.IO.File]::Delete($File.FullName) $Deleted++ Write-Log "DELETED: $($File.FullName)" } catch { $Failed++ Write-Log "FAILED : $($File.FullName) - $($_.Exception.Message)" -Level "WARN" } } # Progress bar - update every BatchSize files or on the last one if (($Deleted + $Failed) % $BatchSize -eq 0 -or ($Deleted + $Failed) -eq $Total) { $Processed = $Deleted + $Failed $Pct = [math]::Round(($Processed / $Total) * 100, 1) $Elapsed = $StopWatch.Elapsed $Rate = if ($Elapsed.TotalSeconds -gt 0) { [math]::Round($Processed / $Elapsed.TotalSeconds) } else { 0 } Write-Progress ` -Activity "Invoke-OldFilePurge - $Path" ` -Status "Deleted: $Deleted | Failed: $Failed | Rate: $Rate files/s" ` -PercentComplete $Pct ` -CurrentOperation "$Processed / $Total files processed" } } $StopWatch.Stop() Write-Progress -Activity "Invoke-OldFilePurge" -Completed # --- Summary ----------------------------------------------------------- $Summary = "Completed in $($StopWatch.Elapsed.ToString('hh\:mm\:ss\.ff')) - Deleted: $Deleted | Failed: $Failed | Total: $Total" Write-Log $Summary Write-Log "=== Invoke-OldFilePurge finished ===" Write-Host "" Write-Host $Summary -ForegroundColor $(if ($Failed -gt 0) { 'Yellow' } else { 'Green' }) if ($LogEnabled) { Write-Host "Log: $LogFile" -ForegroundColor DarkGray }