Compare Filehash in Powershell

34,282

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
Share:
34,282
mack
Author by

mack

Updated on July 16, 2022

Comments

  • mack
    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
      EBGreen almost 12 years
      Will the files have the same name in each folder?
    • mack
      mack almost 12 years
      They will not always have the same name but they could.
    • EBGreen
      EBGreen almost 12 years
      Ok, that changes things a bit.
    • cchamberlain
      cchamberlain almost 9 years
      This 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
      mack almost 9 years
      @cchamberlain, Get-Hash is found in the Powershell Community Extension
    • cchamberlain
      cchamberlain almost 9 years
      @mack - I see, guessing its somewhat obsolete now with the standard Get-FileHash. Thanks for the update.
  • mack
    mack almost 12 years
    Thanks, I'll work on it with this in mind and let you know the result.
  • mack
    mack almost 12 years
    Thanks Keith. I'm going to try this out.
  • cyanat
    cyanat over 4 years
    I think you should use .Hash instead of .HashString: If ($hashSrc.Hash -ne $hashDest.Hash)
  • William Oettmeier
    William Oettmeier about 4 years
    I ran into this issue recently and cyanat is right. I will edit the post to make it ".Hash" instead.
  • Christopher K.
    Christopher K. about 4 years
    I could not find the Get-Hash cmdlet, for me there was a Get-FileHash cmdlet which seems to do the correct thing and can be used the same way.