Last active
December 16, 2025 10:58
-
-
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]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .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