<# .SYNOPSIS Tells you whether two files, or two whole folders, really contain the same bytes. .DESCRIPTION Uses Get-FileHash to compare a file against another file, a folder against another folder (matched by relative path), or a file against a hash you were given, like the SHA-256 on a download page. Files with different sizes are reported as different without hashing them. Read-only: nothing is changed. .PARAMETER ReferencePath The file or folder you trust. .PARAMETER DifferencePath The file or folder you're checking against it. .PARAMETER ExpectedHash A published hash to check ReferencePath against, instead of a second file. .PARAMETER Algorithm SHA256 (default), SHA384, SHA512, SHA1 or MD5. .PARAMETER Recurse When comparing folders, include subfolders. .PARAMETER DifferencesOnly When comparing folders, leave matching files out of the results. .EXAMPLE .\Compare-FileHash.ps1 -ReferencePath .\setup.iso -ExpectedHash 3F2A...9C1D .EXAMPLE .\Compare-FileHash.ps1 -ReferencePath D:\Photos -DifferencePath \\nas01\Backup\Photos -Recurse -DifferencesOnly #> [CmdletBinding(DefaultParameterSetName = 'Compare')] param( [Parameter(Mandatory, Position = 0)][ValidateScript({ Test-Path -LiteralPath $_ })][string]$ReferencePath, [Parameter(Mandatory, Position = 1, ParameterSetName = 'Compare')][ValidateScript({ Test-Path -LiteralPath $_ })][string]$DifferencePath, [Parameter(Mandatory, ParameterSetName = 'Expected')][ValidatePattern('^[0-9A-Fa-f]{32,128}$')][string]$ExpectedHash, [ValidateSet('SHA256', 'SHA384', 'SHA512', 'SHA1', 'MD5')][string]$Algorithm = 'SHA256', [switch]$Recurse, [switch]$DifferencesOnly ) function Get-Hash([string]$File) { (Get-FileHash -LiteralPath $File -Algorithm $Algorithm).Hash } function Compare-OneFile([string]$Name, [IO.FileInfo]$Ref, [IO.FileInfo]$Dif) { $out = [ordered]@{ Name = $Name; Status = $null; ReferenceHash = $null; DifferenceHash = $null; Algorithm = $Algorithm } if (-not $Dif) { $out.Status = 'OnlyInReference' } elseif (-not $Ref) { $out.Status = 'OnlyInDifference' } elseif ($Ref.Length -ne $Dif.Length) { $out.Status = 'Different'; $out.ReferenceHash = '(size differs)' } else { try { $out.ReferenceHash = Get-Hash $Ref.FullName $out.DifferenceHash = Get-Hash $Dif.FullName $out.Status = if ($out.ReferenceHash -eq $out.DifferenceHash) { 'Match' } else { 'Different' } } catch { $out.Status = "Error: $($_.Exception.Message)" } } [pscustomobject]$out } $refItem = Get-Item -LiteralPath $ReferencePath if ($PSCmdlet.ParameterSetName -eq 'Expected') { if ($refItem.PSIsContainer) { throw '-ExpectedHash works on a single file, not a folder.' } $actual = Get-Hash $refItem.FullName return [pscustomobject]@{ Name = $refItem.Name Status = if ($actual -eq $ExpectedHash.Trim()) { 'Match' } else { 'Different' } ReferenceHash = $actual Expected = $ExpectedHash.ToUpper() Algorithm = $Algorithm } } $difItem = Get-Item -LiteralPath $DifferencePath if ($refItem.PSIsContainer -ne $difItem.PSIsContainer) { throw 'Compare a file with a file, or a folder with a folder.' } if (-not $refItem.PSIsContainer) { return Compare-OneFile $refItem.Name $refItem $difItem } # Folders: index both sides by path relative to their root, then walk the union of names. $index = foreach ($root in $refItem, $difItem) { $table = @{} foreach ($f in Get-ChildItem -LiteralPath $root.FullName -File -Recurse:$Recurse -Force) { $table[$f.FullName.Substring($root.FullName.TrimEnd('\', '/').Length + 1)] = $f } , $table } $refFiles, $difFiles = $index Write-Verbose "Reference: $($refFiles.Count) files. Difference: $($difFiles.Count) files." $names = @($refFiles.Keys) + @($difFiles.Keys) | Sort-Object -Unique foreach ($name in $names) { $row = Compare-OneFile $name $refFiles[$name] $difFiles[$name] if (-not ($DifferencesOnly -and $row.Status -eq 'Match')) { $row } }