I file duplicati possono occupare spazio prezioso sul tuo disco rigido e rallentare il tuo computer. In questo articolo, ti mostrerò come liberare spazio sul tuo disco rigido realizzando uno script PowerShell che trova ed elimina i file duplicati in modo facile e veloce e senza installare nessun programma.
Se ancora non sai cos’è PowerShell, ho preparato una piccola guida introduttiva che puoi trovare a questo link che ti spiegherà cos’è PowerShell e perché è utile conoscerlo, ma adesso non perdiamoci in chiacchiere e cominciamo!
L’idea nasce quando ho acquistato il mio primo NAS della QNAP modello TS-233 ed avevo l’esigenza di spostare i file da tutti i miei dispositivi ed evitare che ci fossero duplicati per risparmiare spazio. Andremo ad indicizzare ogni file presente nella/e cartella/e che vogliamo andare ad ripulire sfruttando il concetto di hash.
Ma cos’è un hash?
Per semplificare, un algoritmo di hash è una funzione matematica che “comprime” un’informazione di qualsiasi lunghezza (come una parola, una frase o un file) in una stringa di caratteri di lunghezza fissa, chiamata, appunto, “hash”.
Gli hash, quindi, possono essere considerati come delle impronte digitali che identificano, in maniera univoca e sicura, un file ed il suo il contenuto e, essendo nient’altro che una stringa di caratteri, sono facili da confrontare per determinare se due file siano identici anche se con nomi e/o path differenti.
Cosa ti serve
- Un computer con PowerShell installato (su windows è installato di default)
- Un editor di testo come notepad o qualcosa di più avanzato (opzionale) come Notepad++, PowerShell ISE o Visual Studio Code
Come funziona
Lo script PowerShell eseguirà una scansione delle cartelle selezionate, calcolerà l’hash SHA256 di ogni file ed andrà a marcare I file con lo stesso hash ovvero duplicati. Lo script mostrerà un elenco di tutti i file duplicati trovati, indicando la loro dimensione e la cartella in cui si trovano.
Trovare i file duplicati
Creiamo quindi un dizionario che ad ogni hash relazionerà uno o più file.
$hashtable = New-Object System.Collections.Hashtable
Fatto ciò, scorriamo ogni singolo file e ne calcoliamo l’hash relativo aggiungendolo al dizionario creato nel precedente step.
$hash = (Get-FileHash -Path $item.FullName -Algorithm SHA256).Hash
Questo ci permetterà di indentificare, man mano che scansioniamo, i file duplicati che all’occorrenza andremo a marcare come tali.
# Check if hash exists in the hash table
if($hashtable.ContainsKey($hash)){
# Found duplicate! Increment counters
$duplicates++;
$bytes += $item.Length
$toRemove = $true
}
Al termine della scansione, lo script produrrà un file CSV (comma separated values) contenente l’elenco di tutti i file scansionati, con il relativo hash, con allegata l’informazione se è da rimuovere oppure no. Di base lo script marca come “da rimuovere” tutti i file successivi al primo identificati come duplicati.
Ecco un esempio in azione.
. 'D:\Scripts\Find-DuplicateFiles.ps1' -Paths D:\foto\ -OutputPath D:\
5 items to analyze
Duplicates found: 2 (1,00 Mb)
Result file: D:\duplicated_files_20240422_0959.csv created
Il file prodotto
"Hash","File","CreationTime","LastWriteTime","Size","ToRemove"
"F954AC8B009F965C052519C4E1E395A9F15328596A2B1EAF373D74FE7E169A5F","D:\foto\f1.jpg","22/04/2024 09:57:46","22/04/2024 09:59:10","1048576","False"
"18B423094A1956209D0C777BBAE92A8B7CF74F17739DF7DB4B72BF932DF7CD51","D:\foto\f2.jpg","22/04/2024 09:57:46","22/04/2024 09:59:10","2048","False"
"F954AC8B009F965C052519C4E1E395A9F15328596A2B1EAF373D74FE7E169A5F","D:\foto\f3.jpg","22/04/2024 09:57:46","22/04/2024 09:59:10","1048576","True"
"67FA8B7E479417053708E46F2B3669C2F1C2857DF57ACBFF83AC6A06EA0232E9","D:\foto\foto.jpg","22/04/2024 09:57:46","22/04/2024 09:59:10","1024","False"
"67FA8B7E479417053708E46F2B3669C2F1C2857DF57ACBFF83AC6A06EA0232E9","D:\foto\fotografia.jpg","22/04/2024 09:57:46","22/04/2024 09:59:10","1024","True"
Notiamo che i due file duplicati trovati sono stati marcati come “ToRemove:True” e corrispondono a due file differenti e lo capiamo tramite l’hash.
Quando i record da analizzare diventano tanti, è utile utilizzare un programma come Microsoft Excel per andare a filtrare ed ordinare le colonne.
Elimina i file duplicati
Controlliamo il file di output e verifichiamo i file marcati come da rimuovere (es. marchiamo i file più vecchi come “da rimuovere”). Una volta marcati tutti i file, il comando seguente elimina i file duplicati.
(Import-Csv -Delimiter "," -Path D:\duplicated_files_20240422_1011.csv | ?{$_.ToRemove -eq $true}).FullName | Remove-Item
Il comando sopra, importa il CSV generato dallo script e rimuove ogni file marcato.
Spero che l’idea via sia piaciuta e che vi abbia ispirato per la creazione di un vostro script. Fatemi sapere cosa ne pensate con un commento.
Di seguito vi lascio lo script completo:
[Cmdletbinding()]
param(
# Array of paths to search for duplicates
[Parameter(Mandatory=$true)]
[string[]] $Paths,
# Switch parameter to enable searching subfolders
[switch] $Recurse,
[Parameter(Mandatory=$true)]
[string] $OutputPath,
[switch]$PassThru
)
# Initialize variables
$hashtable = New-Object System.Collections.Hashtable # Hash table to store hash and file paths
$files = @()
$bytes = 0 # Total size of duplicate files (bytes)
$duplicates = 0 # Number of duplicate files found
if(!(Test-Path $OutputPath))
{
Write-Warning "Path: $OutputPath not exists"
exit
}
# Loop through each provided path
foreach ($path in $Paths) {
# Check if path exists
if(!(Test-Path $path))
{
Write-Host "Path $path not found"
continue;
}
# Get all files from the path (including subfolders if Recurse is set)
$items = Get-ChildItem -Path $path -Recurse:$Recurse -File
Write-Host "$($items.Count) items to analyze"
# Progress bar initialization
$i = 1
Write-Progress -Activity "Evaluating $($items.Count) files [path: $path]" -Status ([string]::Format("Duplicates: {0} ({1:N2} Mb)", $duplicates, $bytes/1Mb)) -PercentComplete 0
# Loop through each file in the path
foreach ($item in $items) {
Write-Progress -Activity "Evaluating $i of $($items.Count) files [path: $path]" -Status ([string]::Format("Duplicates: {0} ({1:N2} Mb)", $duplicates, $bytes/1Mb)) -PercentComplete ($i++/$items.Count * 100)
# Calculate SHA256 hash of the file
$hash = (Get-FileHash -Path $item.FullName -Algorithm SHA256).Hash
$toRemove = $false
# Check if hash exists in the hash table
if($hashtable.ContainsKey($hash)){
# Found duplicate! Increment counters
$duplicates++;
$bytes += $item.Length
$toRemove = $true
}
# Add file path to the hash table entry for the current hash
$props = [Ordered]@{Hash = $hash; FullName=$item.FullName; CreationTime=$item.CreationTime; LastWriteTime=$item.LastWriteTime; Size=$item.Length ;ToRemove=$toRemove}
$obj = New-Object -Property $props -TypeName psobject
$hashtable[$hash] += @($item.FullName)
$files += $obj
}
}
# Display summary of duplicate files found
Write-Host ([string]::Format("Duplicates found: {0} ({1:N2} Mb)", $duplicates, $bytes/1Mb))
# Output the hash table containing duplicate file information
$export = Join-Path $OutputPath ("duplicated_files_" + [datetime]::Now.ToString("yyyyMMdd_HHmm") + ".csv")
$files| Export-Csv -Delimiter "," -Path $export -Force -NoTypeInformation
Write-Host "Result file: $export created"
if($PassThru)
{
Write-Output $files
}
