Elimina i file duplicati con PowerShell

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
}

Lascia un commento