Tutorials

Libera spazio sul tuo disco rigido! Con PowerShell

I file duplicati possono occupare spazio prezioso sul tuo disco rigido e rallentare il tuo computer. In questo articolo, ti mostrerò come utilizzare PowerShell per trovare ed eliminare i file duplicati in modo facile e veloce.

L’dea di base è quella di andare ad indicizzare ogni file presente nella/e cartella/e che vogliamo andare ad ripulire e per farlo sfrutteremo 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 scansiona le cartelle che selezioni e calcola l’hash SHA256 di ogni file. I file con lo stesso hash sono considerati duplicati. Lo script ti mostrerà un elenco di tutti i file duplicati trovati, indicando la loro dimensione e la cartella in cui si trovano.

Lo Script

Per prima cosa occorre andare a creare un dizionario per permetterci di associare ai singoli hash gli eventuali path dei file duplicati.

$hashtable = New-Object System.Collections.Hashtable

Fatto ciò, andiamo a scorrere ogni singolo file e ne calcoliamo l’hash relativo e lo aggiungiamo 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"

Possiamo notare 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.

Conclusioni

Ora che abbiamo il nostro file di output possiamo andare visionare i file marcati come da rimuovere ed eventualmente modificare la scelta fatta in automatico dallo script (es. potremmo voler che vengano cancellati i file con data di ultima scrittura meno recente). Una volta terminata la nostra revisione possiamo andare ad eliminare tutti i file marcati come da rimuovere con il seguente comando:

(Import-Csv -Delimiter "," -Path D:\duplicated_files_20240422_1011.csv | ?{$_.ToRemove -eq $true}).FullName | Remove-Item

Dove stiamo essenzialmente reimportando i dati generati dallo script andando poi a filtrarli per la colonna ToRemove prendendo solo quelli marcati come “da rimuovere e per questi ultimi ne richiediamo la rimozione.

Di seguito 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

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *