Compare Filehash in Powershell
Solution 1
There has to be a prettier solution, but this is what I ended up using.
# Get the file hashes
$hashsourcefile = Get-Hash $file -Algorithm "SHA256"
$hashdestfile = Get-Hash $file2 -Algorithm "SHA256"
# Compare the hashes
Compare-Object -Referenceobject $hashsourcefile -Differenceobject $hashdestfile | % { If ($_.Sideindicator -ne " ==") {$diff+=1} }
# The Hashes are different. Note this in the log
if ($diff -ne 0)
{
Add-Content -Path $cLogFile -Value " Source File Hash: $hashsourcefile does not equal
Existing Destination File Hash: $hashdestfile the files are NOT EQUAL."
}
Solution 2
I realize this is a stale thread (16 months old), but it was the top result in Google, which means it's getting a lot of views. I think this might benefit others...
Mack was right, there is a prettier & much simpler solution using Get-Hash. You can simply compare the hashes within the IF statement by comparing the Hash.
# Get the file hashes
$hashSrc = Get-FileHash $file -Algorithm "SHA256"
$hashDest = Get-FileHash $file2 -Algorithm "SHA256"
# Compare the hashes & note this in the log
If ($hashSrc.Hash -ne $hashDest.Hash)
{
Add-Content -Path $cLogFile -Value " Source File Hash: $hashSrc does not
equal Existing Destination File Hash: $hashDest the files are NOT EQUAL."
}
Solution 3
Recursive Directory File Content Diff Using MD5 Hashing
I wrote this pure PowerShell v3+ (no dependencies) recursive directory diff that compares content via MD5 hashes. With -ExportSummary [summary/path]
argument, it will export a set of csv diffs for left and right hand directory along with a summary file of the diff. Otherwise will give standard Compare-Object diff results on stdout. You can either drop the rdiff.ps1 file into your path and Set-ExecutionPolicy RemoteSigned
, or copy the contents directly into your script.
USAGE: rdiff path/to/left,path/to/right [-s path/to/summary/dir]
Here is the gist. Recommended to use the gist version since it may have additional features added over version below.
#########################################################################
### USAGE: rdiff path/to/left,path/to/right [-s path/to/summary/dir] ###
### ADD LOCATION OF THIS SCRIPT TO PATH ###
#########################################################################
[CmdletBinding()]
param (
[parameter(HelpMessage="Stores the execution working directory.")]
[string]$ExecutionDirectory=$PWD,
[parameter(Position=0,HelpMessage="Compare two directories recursively for differences.")]
[alias("c")]
[string[]]$Compare,
[parameter(HelpMessage="Export a summary to path.")]
[alias("s")]
[string]$ExportSummary
)
### FUNCTION DEFINITIONS ###
# SETS WORKING DIRECTORY FOR .NET #
function SetWorkDir($PathName, $TestPath) {
$AbsPath = NormalizePath $PathName $TestPath
Set-Location $AbsPath
[System.IO.Directory]::SetCurrentDirectory($AbsPath)
}
# RESTORES THE EXECUTION WORKING DIRECTORY AND EXITS #
function SafeExit() {
SetWorkDir /path/to/execution/directory $ExecutionDirectory
Exit
}
function Print {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Message to print.")]
[string]$Message,
[parameter(HelpMessage="Specifies a success.")]
[alias("s")]
[switch]$SuccessFlag,
[parameter(HelpMessage="Specifies a warning.")]
[alias("w")]
[switch]$WarningFlag,
[parameter(HelpMessage="Specifies an error.")]
[alias("e")]
[switch]$ErrorFlag,
[parameter(HelpMessage="Specifies a fatal error.")]
[alias("f")]
[switch]$FatalFlag,
[parameter(HelpMessage="Specifies a info message.")]
[alias("i")]
[switch]$InfoFlag = !$SuccessFlag -and !$WarningFlag -and !$ErrorFlag -and !$FatalFlag,
[parameter(HelpMessage="Specifies blank lines to print before.")]
[alias("b")]
[int]$LinesBefore=0,
[parameter(HelpMessage="Specifies blank lines to print after.")]
[alias("a")]
[int]$LinesAfter=0,
[parameter(HelpMessage="Specifies if program should exit.")]
[alias("x")]
[switch]$ExitAfter
)
PROCESS {
if($LinesBefore -ne 0) {
foreach($i in 0..$LinesBefore) { Write-Host "" }
}
if($InfoFlag) { Write-Host "$Message" }
if($SuccessFlag) { Write-Host "$Message" -ForegroundColor "Green" }
if($WarningFlag) { Write-Host "$Message" -ForegroundColor "Orange" }
if($ErrorFlag) { Write-Host "$Message" -ForegroundColor "Red" }
if($FatalFlag) { Write-Host "$Message" -ForegroundColor "Red" -BackgroundColor "Black" }
if($LinesAfter -ne 0) {
foreach($i in 0..$LinesAfter) { Write-Host "" }
}
if($ExitAfter) { SafeExit }
}
}
# VALIDATES STRING MIGHT BE A PATH #
function ValidatePath($PathName, $TestPath) {
If([string]::IsNullOrWhiteSpace($TestPath)) {
Print -x -f "$PathName is not a path"
}
}
# NORMALIZES RELATIVE OR ABSOLUTE PATH TO ABSOLUTE PATH #
function NormalizePath($PathName, $TestPath) {
ValidatePath "$PathName" "$TestPath"
$TestPath = [System.IO.Path]::Combine((pwd).Path, $TestPath)
$NormalizedPath = [System.IO.Path]::GetFullPath($TestPath)
return $NormalizedPath
}
# VALIDATES STRING MIGHT BE A PATH AND RETURNS ABSOLUTE PATH #
function ResolvePath($PathName, $TestPath) {
ValidatePath "$PathName" "$TestPath"
$ResolvedPath = NormalizePath $PathName $TestPath
return $ResolvedPath
}
# VALIDATES STRING RESOLVES TO A PATH AND RETURNS ABSOLUTE PATH #
function RequirePath($PathName, $TestPath, $PathType) {
ValidatePath $PathName $TestPath
If(!(Test-Path $TestPath -PathType $PathType)) {
Print -x -f "$PathName ($TestPath) does not exist as a $PathType"
}
$ResolvedPath = Resolve-Path $TestPath
return $ResolvedPath
}
# Like mkdir -p -> creates a directory recursively if it doesn't exist #
function MakeDirP {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path create.")]
[string]$Path
)
PROCESS {
New-Item -path $Path -itemtype Directory -force | Out-Null
}
}
# GETS ALL FILES IN A PATH RECURSIVELY #
function GetFiles {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path to get files for.")]
[string]$Path
)
PROCESS {
ls $Path -r | where { !$_.PSIsContainer }
}
}
# GETS ALL FILES WITH CALCULATED HASH PROPERTY RELATIVE TO A ROOT DIRECTORY RECURSIVELY #
# RETURNS LIST OF @{RelativePath, Hash, FullName}
function GetFilesWithHash {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path to get directories for.")]
[string]$Path,
[parameter(HelpMessage="The hash algorithm to use.")]
[string]$Algorithm="MD5"
)
PROCESS {
$OriginalPath = $PWD
SetWorkDir path/to/diff $Path
GetFiles $Path | select @{N="RelativePath";E={$_.FullName | Resolve-Path -Relative}},
@{N="Hash";E={(Get-FileHash $_.FullName -Algorithm $Algorithm | select Hash).Hash}},
FullName
SetWorkDir path/to/original $OriginalPath
}
}
# COMPARE TWO DIRECTORIES RECURSIVELY #
# RETURNS LIST OF @{RelativePath, Hash, FullName}
function DiffDirectories {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Directory to compare left.")]
[alias("l")]
[string]$LeftPath,
[parameter(Mandatory=$TRUE,Position=1,HelpMessage="Directory to compare right.")]
[alias("r")]
[string]$RightPath
)
PROCESS {
$LeftHash = GetFilesWithHash $LeftPath
$RightHash = GetFilesWithHash $RightPath
diff -ReferenceObject $LeftHash -DifferenceObject $RightHash -Property RelativePath,Hash
}
}
### END FUNCTION DEFINITIONS ###
### PROGRAM LOGIC ###
if($Compare.length -ne 2) {
Print -x "Compare requires passing exactly 2 path parameters separated by comma, you passed $($Compare.length)." -f
}
Print "Comparing $($Compare[0]) to $($Compare[1])..." -a 1
$LeftPath = RequirePath path/to/left $Compare[0] container
$RightPath = RequirePath path/to/right $Compare[1] container
$Diff = DiffDirectories $LeftPath $RightPath
$LeftDiff = $Diff | where {$_.SideIndicator -eq "<="} | select RelativePath,Hash
$RightDiff = $Diff | where {$_.SideIndicator -eq "=>"} | select RelativePath,Hash
if($ExportSummary) {
$ExportSummary = ResolvePath path/to/summary/dir $ExportSummary
MakeDirP $ExportSummary
$SummaryPath = Join-Path $ExportSummary summary.txt
$LeftCsvPath = Join-Path $ExportSummary left.csv
$RightCsvPath = Join-Path $ExportSummary right.csv
$LeftMeasure = $LeftDiff | measure
$RightMeasure = $RightDiff | measure
"== DIFF SUMMARY ==" > $SummaryPath
"" >> $SummaryPath
"-- DIRECTORIES --" >> $SummaryPath
"`tLEFT -> $LeftPath" >> $SummaryPath
"`tRIGHT -> $RightPath" >> $SummaryPath
"" >> $SummaryPath
"-- DIFF COUNT --" >> $SummaryPath
"`tLEFT -> $($LeftMeasure.Count)" >> $SummaryPath
"`tRIGHT -> $($RightMeasure.Count)" >> $SummaryPath
"" >> $SummaryPath
$Diff | Format-Table >> $SummaryPath
$LeftDiff | Export-Csv $LeftCsvPath -f
$RightDiff | Export-Csv $RightCsvPath -f
}
$Diff
SafeExit
mack
Updated on July 16, 2022Comments
-
mack almost 2 years
I'm a powershell. I've been staring at my screen for most of the afternoon trying to figure out how to compare the file hash of multiple files that are in two different directories. The script will download files from an FTP site into a directory ($cDlPath) and eventually copy them to another directory ($cDestPath). I want to compare the filehash from the files to be sure nothing has changed since they were downloaded. I'm using the Get-Hash cmdlet to get the file hash, but I can't figure out how to compare the two hashes. If the files are unequal I want to be able to identify the altered file(s) by name so the files can be checked.
I've been fiddling around with the code below, but it doesn't seem to be what I'm after.
Compare-Object ` -ReferenceObject $(Get-ChildItem $cDestPath -Recurse | Where-Object {!$_.psiscontainer } | Get-Hash -Algorithm $cHashAlg) ` -DifferenceObject $(Get-ChildItem $cDlPath -Recurse | Where-Object {!$_.psiscontainer } | Get-Hash -Algorithm $cHashAlg)
Any help would be greatly appreciated.
I'm using the code below and I seem to be a little closer.
Compare-Object $(Get-ChildItem $cDlPath -Recurse $_ | Where-Object { !$_.PsIsContainer } | Select-Object Name, FullName, Length, @{Name=”SHA256 Hash”; Expression={ Get-Hash $_.FullName -Algorithm "SHA256" }}, LastWriteTime) $( Get-ChildItem $cDestPath -Recurse $_ | Where-Object { !$_.PsIsContainer } | Select-Object Name, FullName, Length, @{Name=”SHA256 Hash”; Expression={ Get-Hash $_.FullName -Algorithm "SHA256" }}, LastWriteTime) -property @ ("Name", “FullName”,”SHA256 Hash”, "Length", "LastWriteTime" ) | Add-Content -Path $cLogFile
It still looks like it isn't completely right though because there are some hashes that are the same and the output to the logfile is ugly. The files should only be in the log file if they have the same hash.
@{Name=nothing.xlsx; FullName=C:\Test\nothing.xlsx; SHA256 Hash=E74424B6324DE014CB0C896DA29D67A2A729E31DF57119E840CA4BD9A9E41754; Length=8891; LastWriteTime=7/31/2012 1:33:11 PM; SideIndicator=<=} @{Name=test.txt; FullName=C:\Test\test.txt; SHA256 Hash=FC43E73579DB001751A29C1F7A8E2E36E46A53662B63013F0AE500AA896DE056; Length=174; LastWriteTime=7/31/2012 4:52:52 PM; SideIndicator=<=} @{Name=testfile.txt; FullName=C:\Test\testfile.txt; SHA256 Hash=2B2DB80CAF93224A49A7C94E8EA5BCB1B86D421EA2DB83285149ECAE6DEAA105; Length=415; LastWriteTime=7/27/2012 12:01:21 PM; SideIndicator=<=} @{Name=nothing.xlsx; FullName=C:\Test\Old\nothing.xlsx; SHA256 Hash=22603417927343A485862CE93790203EE7C2DB092C2060C92D44B736A01FD37E; Length=8978; LastWriteTime=7/31/2012 4:40:43 PM; SideIndicator=<=} @{Name=test.txt; FullName=C:\Test\Old\test.txt; SHA256 Hash=FC43E73579DB001751A29C1F7A8E2E36E46A53662B63013F0AE500AA896DE056; Length=174; LastWriteTime=7/31/2012 4:52:52 PM; SideIndicator=<=} @{Name=testfile.txt; FullName=C:\Test\Old\testfile.txt; SHA256 Hash=0B35A9F7F500B46469E2C1759F92D222983C4FDF4AAE316C0F2861FC70D0FD2B; Length=447; LastWriteTime=7/31/2012 4:52:40 PM; SideIndicator=<=}
-
EBGreen almost 12 yearsWill the files have the same name in each folder?
-
mack almost 12 yearsThey will not always have the same name but they could.
-
EBGreen almost 12 yearsOk, that changes things a bit.
-
cchamberlain almost 9 yearsThis whole thread is a bit confusing due to their being no standard Get-Hash commandlet that I am aware of. Since 4.0 there is Get-Filehash but the only thing I've found to a Get-Hash is here. All the answers to the thread also call non-existant Get-Hash to add to confusion.
-
mack almost 9 years@cchamberlain, Get-Hash is found in the Powershell Community Extension
-
cchamberlain almost 9 years@mack - I see, guessing its somewhat obsolete now with the standard Get-FileHash. Thanks for the update.
-
-
mack almost 12 yearsThanks, I'll work on it with this in mind and let you know the result.
-
mack almost 12 yearsThanks Keith. I'm going to try this out.
-
cyanat over 4 yearsI think you should use .Hash instead of .HashString:
If ($hashSrc.Hash -ne $hashDest.Hash)
-
William Oettmeier about 4 yearsI ran into this issue recently and cyanat is right. I will edit the post to make it ".Hash" instead.
-
Christopher K. about 4 yearsI could not find the
Get-Hash
cmdlet, for me there was aGet-FileHash
cmdlet which seems to do the correct thing and can be used the same way.