Skip to content

Instantly share code, notes, and snippets.

@tuklusan
Last active December 16, 2025 10:58
Show Gist options
  • Select an option

  • Save tuklusan/a4103618abfa80a3b0e460780b3d807d to your computer and use it in GitHub Desktop.

Select an option

Save tuklusan/a4103618abfa80a3b0e460780b3d807d to your computer and use it in GitHub Desktop.
Remove-Duplicate-Files.ps1 - Finds duplicate filenames across subdirectories and keeps only the latest version [Windows Powershell]
<#
.DISCLAIMER
THIS SCRIPT PERFORMS PERMANENT FILE DELETION. USE AT YOUR OWN RISK!
- Always test with -WhatIf parameter first
- Ensure you have backups before running
- Author provides NO WARRANTY and assumes NO LIABILITY for data loss
- You are solely responsible for testing and verifying results
.SYNOPSIS
Finds duplicate filenames across subdirectories and keeps only the latest version.
.DESCRIPTION
This script scans the specified directory and all subdirectories for files with the same name.
It compares the LastWriteTime of duplicate files and keeps only the newest version, deleting older duplicates.
.PARAMETER Path
The root directory to scan for duplicate files. Defaults to current directory.
.PARAMETER Recurse
Include all subdirectories in the search. Default is true.
.PARAMETER ExtensionFilter
Filter files by specific extension (e.g., "*.txt", "*.pdf").
.PARAMETER UseCreationTime
Use CreationTime instead of LastWriteTime to determine which file is newest.
.PARAMETER KeepOldest
Keep the oldest file instead of the newest (inverse logic).
.PARAMETER LogFile
Specify a log file to record all actions.
.EXAMPLE
.\Remove-DuplicateFiles.ps1 -Path "C:\MyFiles" -WhatIf
Scans C:\MyFiles and shows what would be deleted without actually deleting.
.EXAMPLE
.\Remove-DuplicateFiles.ps1 -Path "C:\MyFiles" -ExtensionFilter "*.txt"
Only processes .txt files.
.EXAMPLE
.\Remove-DuplicateFiles.ps1 -Path "C:\MyFiles" -KeepOldest -Confirm:$false
Keeps the oldest file and deletes newer duplicates without confirmation.
.NOTES
Always test with -WhatIf first to ensure no important files are accidentally deleted.
#>
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
param(
[Parameter(Position=0)]
[string]$Path = ".",
[bool]$Recurse = $true,
[string]$ExtensionFilter,
[switch]$UseCreationTime,
[switch]$KeepOldest,
[string]$LogFile
)
# Function to format file size
function Format-FileSize {
param([long]$Bytes)
if ($Bytes -ge 1GB) {
return "{0:N2} GB" -f ($Bytes / 1GB)
} elseif ($Bytes -ge 1MB) {
return "{0:N2} MB" -f ($Bytes / 1MB)
} elseif ($Bytes -ge 1KB) {
return "{0:N2} KB" -f ($Bytes / 1KB)
} else {
return "{0} bytes" -f $Bytes
}
}
# Function to write to log file
function Write-Log {
param(
[string]$Message,
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "$timestamp [$Level] $Message"
if ($LogFile) {
Add-Content -Path $LogFile -Value $logMessage -Force
}
switch ($Level) {
"ERROR" { Write-Host $Message -ForegroundColor Red }
"WARNING" { Write-Host $Message -ForegroundColor Yellow }
"INFO" { Write-Host $Message -ForegroundColor Gray }
"SUCCESS" { Write-Host $Message -ForegroundColor Green }
default { Write-Host $Message }
}
}
# Clear screen and display header
Clear-Host
Write-Host "===============================================" -ForegroundColor Cyan
Write-Host "Duplicate File Cleanup Script" -ForegroundColor Cyan
Write-Host "===============================================" -ForegroundColor Cyan
Write-Host ""
# Log start if log file specified
if ($LogFile) {
try {
$LogFile = Resolve-Path -Path $LogFile -ErrorAction Stop
} catch {
$LogFile = Join-Path -Path (Get-Location) -ChildPath "DuplicateCleanup.log"
}
Write-Log "Script started" "INFO"
Write-Log "Path: $Path" "INFO"
if ($ExtensionFilter) {
Write-Log "Extension filter: $ExtensionFilter" "INFO"
}
if ($UseCreationTime) {
Write-Log "Using CreationTime for comparison" "INFO"
}
if ($KeepOldest) {
Write-Log "Keeping oldest file (inverse mode)" "INFO"
}
}
# Validate path
if (-not (Test-Path $Path -PathType Container)) {
$errorMsg = "Error: Path '$Path' does not exist or is not a directory."
Write-Log $errorMsg "ERROR"
exit 1
}
$Path = Resolve-Path $Path
Write-Log "Scanning directory: $Path" "INFO"
if ($ExtensionFilter) {
Write-Log "Extension filter: $ExtensionFilter" "INFO"
}
# Determine which time property to use for comparison
$timeProperty = if ($UseCreationTime) { "CreationTime" } else { "LastWriteTime" }
Write-Log "Using $timeProperty for file comparison" "INFO"
if ($KeepOldest) {
Write-Log "Keeping OLDEST file in each duplicate group" "WARNING"
} else {
Write-Log "Keeping NEWEST file in each duplicate group" "INFO"
}
Write-Host ""
# Build search parameters
$searchParams = @{
Path = $Path
Recurse = $Recurse
File = $true
ErrorAction = 'SilentlyContinue'
}
if ($ExtensionFilter) {
$searchParams.Include = $ExtensionFilter
}
# Get all files
Write-Log "Finding files..." "INFO"
try {
$allFiles = Get-ChildItem @searchParams | Where-Object { -not $_.PSIsContainer }
} catch {
$errorMsg = "Error accessing files: $_"
Write-Log $errorMsg "ERROR"
exit 1
}
if ($null -eq $allFiles -or $allFiles.Count -eq 0) {
Write-Log "No files found matching the criteria." "WARNING"
exit 0
}
Write-Log "Found $($allFiles.Count) files to process." "INFO"
Write-Host ""
# Group files by name (case-insensitive)
Write-Log "Analyzing duplicates..." "INFO"
$fileGroups = $allFiles | Group-Object -Property Name -NoElement
# Find files with duplicate names
$duplicateGroups = $allFiles | Group-Object -Property Name | Where-Object { $_.Count -gt 1 }
if ($duplicateGroups.Count -eq 0) {
Write-Log "No duplicate filenames found." "SUCCESS"
exit 0
}
Write-Log "Found $($duplicateGroups.Count) groups of duplicate filenames." "INFO"
Write-Host ""
# Variables for tracking
$totalFilesToDelete = 0
$totalSpaceToSave = 0
$filesToDelete = @()
$filesToKeep = @()
$processedGroups = @()
# Analyze each duplicate group
foreach ($group in $duplicateGroups) {
Write-Host "Processing: $($group.Name)" -ForegroundColor Cyan
# Sort files by selected time property
if ($KeepOldest) {
# Keep oldest, delete newer - sort ascending (oldest first)
$sortedFiles = $group.Group | Sort-Object @{Expression = {if ($UseCreationTime) { $_.CreationTime } else { $_.LastWriteTime }}}
} else {
# Keep newest, delete older - sort descending (newest first)
$sortedFiles = $group.Group | Sort-Object @{Expression = {if ($UseCreationTime) { $_.CreationTime } else { $_.LastWriteTime }}} -Descending
}
# The first file is the one to keep
$fileToKeep = $sortedFiles[0]
$filesToKeep += $fileToKeep
# Get the time value using the property name
$timeValue = if ($UseCreationTime) { $fileToKeep.CreationTime } else { $fileToKeep.LastWriteTime }
# Use string formatting to avoid parsing issues
Write-Log " Keeping: $($fileToKeep.FullName)" "INFO"
Write-Log (" {0}: {1}" -f $timeProperty, $timeValue) "INFO"
Write-Log " Size: $(Format-FileSize $fileToKeep.Length)" "INFO"
# All other files in the group are marked for deletion
for ($i = 1; $i -lt $sortedFiles.Count; $i++) {
$fileToDelete = $sortedFiles[$i]
$filesToDelete += $fileToDelete
$totalFilesToDelete++
$totalSpaceToSave += $fileToDelete.Length
# Get the time value for the file to delete
$deleteTimeValue = if ($UseCreationTime) { $fileToDelete.CreationTime } else { $fileToDelete.LastWriteTime }
Write-Log " Deleting: $($fileToDelete.FullName)" "WARNING"
Write-Log (" {0}: {1}" -f $timeProperty, $deleteTimeValue) "INFO"
Write-Log " Size: $(Format-FileSize $fileToDelete.Length)" "INFO"
}
# Store group info for reporting
$processedGroups += [PSCustomObject]@{
FileName = $group.Name
FilesCount = $group.Count
FileToKeep = $fileToKeep.FullName
FilesToDelete = ($sortedFiles[1..($sortedFiles.Count-1)] | ForEach-Object { $_.FullName }) -join "; "
SpaceSaved = ($sortedFiles[1..($sortedFiles.Count-1)] | Measure-Object -Property Length -Sum).Sum
}
Write-Host ""
}
# Display summary
Write-Host "===============================================" -ForegroundColor Cyan
Write-Host "SUMMARY" -ForegroundColor Cyan
Write-Host "===============================================" -ForegroundColor Cyan
Write-Log "Files to keep: $($filesToKeep.Count)" "INFO"
Write-Log "Files to delete: $totalFilesToDelete" "WARNING"
Write-Log "Space to save: $(Format-FileSize $totalSpaceToSave)" "INFO"
Write-Host ""
if ($totalFilesToDelete -eq 0) {
Write-Log "No files to delete." "SUCCESS"
exit 0
}
# Display detailed report if WhatIf mode or no confirmation requested
if ($WhatIfPreference -or ($ConfirmPreference -ne 'High')) {
Write-Host "Detailed Report:" -ForegroundColor Yellow
Write-Host "----------------" -ForegroundColor Yellow
foreach ($group in $processedGroups) {
Write-Host "`nFile: $($group.FileName)" -ForegroundColor Cyan
Write-Host " Total duplicates: $($group.FilesCount)" -ForegroundColor Gray
Write-Host " Keeping: $($group.FileToKeep)" -ForegroundColor Green
$deleteFiles = $group.FilesToDelete -split '; '
foreach ($file in $deleteFiles) {
Write-Host " Deleting: $file" -ForegroundColor Red
}
}
Write-Host ""
}
# Delete the files (if not in WhatIf mode)
$deletedCount = 0
$skippedCount = 0
if (-not $WhatIfPreference) {
foreach ($file in $filesToDelete) {
try {
if ($PSCmdlet.ShouldProcess($file.FullName, "Delete")) {
Remove-Item -Path $file.FullName -Force -ErrorAction Stop
Write-Log "Deleted: $($file.FullName)" "SUCCESS"
$deletedCount++
}
}
catch {
$errorMsg = "Error deleting $($file.FullName): $_"
Write-Log $errorMsg "ERROR"
$skippedCount++
}
}
} else {
Write-Log "WHAT IF MODE: No files were actually deleted." "INFO"
}
# Final summary
Write-Host ""
Write-Host "===============================================" -ForegroundColor Cyan
Write-Host "COMPLETED" -ForegroundColor Cyan
Write-Host "===============================================" -ForegroundColor Cyan
if ($WhatIfPreference) {
Write-Log "WHAT IF MODE: No files were actually deleted." "INFO"
}
Write-Log "Files successfully deleted: $deletedCount" "SUCCESS"
if ($skippedCount -gt 0) {
Write-Log "Files skipped: $skippedCount" "WARNING"
}
if ($deletedCount -gt 0) {
$actualSpaceSaved = ($totalSpaceToSave * $deletedCount / $totalFilesToDelete)
Write-Log "Space saved: $(Format-FileSize $actualSpaceSaved)" "SUCCESS"
}
# Save detailed report to CSV if requested
if ($LogFile -and $processedGroups.Count -gt 0) {
$reportDir = Split-Path $LogFile -Parent
$reportFile = Join-Path $reportDir "DuplicateCleanup_Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
try {
$processedGroups | Export-Csv -Path $reportFile -NoTypeInformation -Force
Write-Log "Detailed report saved to: $reportFile" "INFO"
} catch {
Write-Log "Could not save report to CSV: $_" "WARNING"
}
}
Write-Log "Script completed" "INFO"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment