Add Invoke-OldFilePurge script and README documentation for file cleanup utility
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<#
|
||||
.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 }
|
||||
@@ -0,0 +1,119 @@
|
||||
# Invoke-OldFilePurge
|
||||
|
||||
## Overview
|
||||
|
||||
This PowerShell script deletes files older than a specified number of days from a target directory.
|
||||
|
||||
It is designed for high-volume deletion of small files — for example Tomcat temp folders, IIS logs, or other directories that accumulate large numbers of files over time. The function:
|
||||
|
||||
- Scans a directory (optionally recursively) for files older than a given age
|
||||
- Deletes matching files using `[System.IO.File]::Delete()` for speed
|
||||
- Reports progress (percentage, rate, deleted/failed counts) via `Write-Progress`
|
||||
- Logs every deletion (and failure) to a timestamped log file
|
||||
- Supports `-WhatIf` / `-Confirm` via `SupportsShouldProcess`
|
||||
|
||||
---
|
||||
|
||||
# Prerequisites
|
||||
|
||||
## PowerShell
|
||||
|
||||
PowerShell 5.1 or later.
|
||||
|
||||
---
|
||||
|
||||
# Files
|
||||
|
||||
## Invoke-OldFilePurge.ps1
|
||||
|
||||
Defines the `Invoke-OldFilePurge` function. Dot-source the file or copy the function into your session/module before use.
|
||||
|
||||
```powershell
|
||||
. .\Invoke-OldFilePurge.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Parameters
|
||||
|
||||
| Parameter | Mandatory | Default | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| `Path` | Yes | — | Target directory to clean up. |
|
||||
| `DaysOld` | No | `7` | Files with `LastWriteTime` older than this many days are deleted. Range: 1–3650. |
|
||||
| `Recurse` | No | Enabled | If specified, subdirectories are also scanned. |
|
||||
| `LogDir` | No | `C:\Temp` | Directory for the log file. Created automatically if it does not exist. |
|
||||
| `LogEnabled` | No | `$true` | Enables or disables logging. |
|
||||
|
||||
---
|
||||
|
||||
# Running the script
|
||||
|
||||
Preview what would be deleted without actually deleting anything:
|
||||
|
||||
```powershell
|
||||
Invoke-OldFilePurge.ps1 -Path "C:\Program Files\Apache Software Foundation\Tomcat 9.0\temp" -WhatIf
|
||||
```
|
||||
|
||||
Delete files older than 7 days (default):
|
||||
|
||||
```powershell
|
||||
Invoke-OldFilePurge.ps1 -Path "C:\Program Files\Apache Software Foundation\Tomcat 9.0\temp"
|
||||
```
|
||||
|
||||
Delete files older than 30 days and write logs to a custom directory:
|
||||
|
||||
```powershell
|
||||
Invoke-OldFilePurge.ps1 -Path "D:\IIS\Logs" -DaysOld 30 -LogDir "D:\Logs"
|
||||
```
|
||||
|
||||
Run without logging:
|
||||
|
||||
```powershell
|
||||
Invoke-OldFilePurge.ps1 -Path "D:\Temp" -LogEnabled $false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Logging
|
||||
|
||||
Each execution (when `LogEnabled` is `$true`) creates a log file:
|
||||
|
||||
```
|
||||
FileCleanup_YYYYMMDD_HHMMSS.log
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
2026-07-02 09:12:11 [INFO ] === Invoke-OldFilePurge started ===
|
||||
2026-07-02 09:12:11 [INFO ] Path : D:\IIS\Logs
|
||||
2026-07-02 09:12:11 [INFO ] DaysOld : 30 (cutoff: 2026-06-02 09:12:11)
|
||||
2026-07-02 09:12:11 [INFO ] Recurse : True
|
||||
2026-07-02 09:12:11 [INFO ] Files to delete: 1245
|
||||
2026-07-02 09:12:12 [INFO ] DELETED: D:\IIS\Logs\u_ex260101.log
|
||||
2026-07-02 09:12:12 [WARN ] FAILED : D:\IIS\Logs\u_ex260102.log — file in use
|
||||
2026-07-02 09:13:05 [INFO ] Completed in 00:00:53.42 — Deleted: 1244 | Failed: 1 | Total: 1245
|
||||
2026-07-02 09:13:05 [INFO ] === Invoke-OldFilePurge finished ===
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Progress reporting
|
||||
|
||||
While running, the script displays a progress bar updated every 500 processed files (or on the last file), showing the count deleted, failed, and the deletion rate (files/second).
|
||||
|
||||
---
|
||||
|
||||
# Error Handling
|
||||
|
||||
- Validates that `Path` exists and is a directory before starting.
|
||||
- Each file deletion is wrapped in `try/catch`; failures are counted and logged with `[WARN]` but do not stop the run.
|
||||
- Supports `-WhatIf` and `-Confirm` (via `SupportsShouldProcess`) to preview or confirm deletions before they happen.
|
||||
|
||||
---
|
||||
|
||||
# Notes
|
||||
|
||||
- Files are matched purely on `LastWriteTime`; there is no filter on file extension or name.
|
||||
- Hidden and system files are included (`-Force`).
|
||||
- Deletion uses `[System.IO.File]::Delete()` instead of `Remove-Item`, which is significantly faster for large numbers of small files.
|
||||
Reference in New Issue
Block a user