Skip to content

Instantly share code, notes, and snippets.

@loopyd
Last active May 1, 2024 17:23
Show Gist options
  • Save loopyd/26b1cba08d26d6b8f4ef43ad49669076 to your computer and use it in GitHub Desktop.
Save loopyd/26b1cba08d26d6b8f4ef43ad49669076 to your computer and use it in GitHub Desktop.
[PowerShell 7] Get-HashEx - All-in-one file and folder hashing module for Windows PowerShell 7.0+
<#
.Synopsis
Hash a file or a folder via the lifestrea- er I mean, bytestream. Then return the results.
.Description
I improved Get-FileHash just a bit. Hash a file or a folder via the lifestrea- er I mean,
bytestream. Then returns the results. A lot of options, and what ever supported, hardware
accelerated CryptoStream HashAlgorithm provider you want. Also with real time progress,
which you can turn off if you want.
FEATURES:
a. Get cryptographic hash of single file with progress.
b. Custom functionality to recursively hash contents of entire folders
c. Combined hash for entire folder via recursive hashing.
d. Uses the CryptoStream and HashAlgorithm provider for speed.
.Parameter Path
The path to the file or folder to compute a hash for. It can be a relative
or an absolute path.
.Parameter Algorithm
One of System.Cryptography.HashAlgorithm options, as a string.
.Parameter BufferSize
Size of the stream buffer in bytes, can perform better depending on your disk.
Default is 1mb (1048576 bytes), which is good for most SSDs. For HDDs, you
should set this to your block size to align reads and writes correctly for
efficient streaming / protect drive brakes from wear. Keep your blocks
aligned to the cylander for best results when using System.IO.Stream asyncronous
functions.
.Parameter Combine
Optionally specify this when -Folder switch is also specified. It will produce a single
entry containing folder properties and the accumulated hash (Transformed hash)
.Parameter NoProgress
Optionally specify this flag to surpress progress output.
.Parameter Folder
Optionally specify this flag to indicate that the Path parameter specified is a folder
that should be recursively hashed.
#>
Function Get-HashEx() {
param(
[Parameter(Mandatory = $true, Position = 0)][string]$Path,
[Parameter(Mandatory = $false)][string]$Algorithm = "SHA256",
[Parameter(Mandatory = $false)][int64]$BufferSize = 1mb,
[Parameter(Mandatory = $false)][switch]$Combine = $false,
[Parameter(Mandatory = $false)][switch]$NoProgress = $false,
[Parameter(Mandatory = $false)][switch]$Folder = $false
)
begin {
if ($Combine -And $Folder) {
$AlgorithmObj = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm);
$CryptoStream = [System.Security.Cryptography.CryptoStream]::new(([System.IO.Stream]::Null), $AlgorithmObj, "Write");
} elseif (-Not $Folder) {
$AlgorithmObj = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm);
$CryptoStream = [System.Security.Cryptography.CryptoStream]::new(([System.IO.Stream]::Null), $AlgorithmObj, "Write");
};
$buffer = New-Object Byte[] $BufferSize;
if ((-Not $NoProgress) -And $Folder) {
$filemeasures = (Get-ChildItem $Path -Recurse -Attributes !Directory,!Directory+Hidden | Measure-Object -Property Length -Sum);
$filecount = $filemeasures.Count;
$filesize = $filemeasures.Sum;
$filemeasures = $null;
$processedfiles = 0;
$FT = @()
} elseif ($NoProgress -And $Folder) {
$FT = @()
};
$Error = $false;
}
process {
try {
if ($Folder) {
Get-ChildItem $Path -Recurse -Attributes !Directory,!Directory+Hidden -ErrorAction SilentlyContinue |% {
$myfileobj = $_;
# Display progress
if (-Not $NoProgress) {
$prog = ($processedfiles / $filecount);
Write-Progress -Id 1 -Activity "Get-HashEx is running..." -Status ("Progress: {0:P2}" -f $prog) -PercentComplete ($prog * 100) -CurrentOperation $myfileobj.FullName;
$processedfiles++;
};
# If we are not combining items, we need to initialize a fresh CryptoStream
# each iteration.
if (-Not $Combine) {
$AlgorithmObj = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm);
$CryptoStream = [System.Security.Cryptography.CryptoStream]::new(([System.IO.Stream]::Null), $AlgorithmObj, "Write");
}
# Stream the file to buffer and into CryptoStream.
$FileStream = [System.IO.File]::OpenRead($_.FullName);
while ( $bytesRead = $FileStream.Read($buffer, 0, $BufferSize) ){
# Display progress for Get-FileHash by tracking FileStream position.
if ((-Not $NoProgress)) {
$prog2 = ($FileStream.Position / $FileStream.Length);
Write-Progress -Id 2 -Activity "CryptoStream is running..." -Status ("Progress: {0:P2}" -f ($prog2)) -PercentComplete ($prog2 * 100) -CurrentOperation ("{0} of {1} bytes hashed" -f $FileStream.Position, $FileStream.Length);
}
# Write to the Stream from the buffer and then flush the CryptoStream block queue.
$CryptoStream.Write($buffer, 0, $bytesRead);
$CryptoStream.Flush();
}
$FileStream.Close(); $FileStream.Dispose();
# If we are not combining items, finalize the CryptoStream, store the result into
# the table, and then dispose of the CryptoStream and HashAlgorithm provider
# we used in this iteration.
if (-Not $Combine) {
$CryptoStream.FlushFinalBlock();
$myfilehash = $(($AlgorithmObj.Hash | ForEach-Object {$_.ToString("X2")}) -join '');
$myrelativename = ($myfileobj.FullName).Replace([System.IO.Path]::GetFullPath($Path), ".\")
$mylastwritetime = [int64]((New-TimeSpan -Start ([timezone]::CurrentTimeZone.ToLocalTime([datetime]'1/1/1970')) -End $myfileobj.LastWriteTime).TotalMilliseconds)
$mysize = ($myfileobj.Length)
$item = [PSCustomObject]@{
FullName = ($myfileobj.FullName)
RelativeName = $myrelativename
Size = $mysize
LastWriteTime = $mylastwritetime
Hash = $myfilehash
};
$FT += $item;
$CryptoStream.Close(); $CryptoStream.Dispose(); $AlgorithmObj.Dispose();
} else {
$CryptoStream.Flush();
}
}
# After everything has completed, if we are combining items, finalize the CryptoStream,
# and store the folder properties as a single entry in the table, then destroy the
# CryptoStream and the HashAlgorithm provider.
if ($Combine) {
$CryptoStream.FlushFinalBlock();
$myfileobj = $(Get-Item -Path $([System.IO.Path]::GetFullPath($Path)) -Force);
$myfilehash = $(($AlgorithmObj.Hash | ForEach-Object {$_.ToString("X2")}) -join '');
$myrelativename = ($myfileobj.FullName).Replace([System.IO.Path]::GetFullPath($Path), ".\")
$mylastwritetime = [int64]((New-TimeSpan -Start ([timezone]::CurrentTimeZone.ToLocalTime([datetime]'1/1/1970')) -End $myfileobj.LastWriteTime).TotalMilliseconds)
[int64]$mysize = [int64]((Get-ChildItem $Path -Recurse -Attributes !Directory,!Directory+Hidden | Measure-Object -Property Length -Sum).Sum);
$item = [PSCustomObject]@{
FullName = ($myfileobj.FullName)
RelativeName = $myrelativename
Size = $mysize
LastWriteTime = $mylastwritetime
Hash = $myfilehash
};
$FT += $item;
$CryptoStream.Close(); $CryptoStream.Dispose(); $AlgorithmObj.Dispose();
}
} else {
# Stream the file to buffer and into CryptoStream.
$FileStream = [System.IO.File]::OpenRead([System.IO.Path]::GetFullPath($Path));
while ( $bytesRead = $FileStream.Read($buffer, 0, $BufferSize) ){
# Display progress for Get-FileHash by tracking FileStream position.
if ((-Not $NoProgress)) {
$prog2 = ($FileStream.Position / $FileStream.Length);
Write-Progress -Id 2 -Activity "Get-HashEx is running..." -Status ("Progress: {0:P2}" -f ($prog2)) -PercentComplete ($prog2 * 100) -CurrentOperation ("{0} of {1} bytes hashed" -f $FileStream.Position, $FileStream.Length);
}
# Write to the Stream from the buffer and then flush the CryptoStream block queue.
$CryptoStream.Write($buffer, 0, $bytesRead);
$CryptoStream.Flush();
}
$FileStream.Close(); $FileStream.Dispose();
# Finalize the CryptoStream, store the result into the table, and then dispose of
# the CryptoStream and HashAlgorithm provider.
$CryptoStream.FlushFinalBlock();
$myfileobj = $(Get-Item -Path $([System.IO.Path]::GetFullPath($Path)) -Force)
$myfilehash = $(($AlgorithmObj.Hash | ForEach-Object {$_.ToString("X2")}) -join '');
$myrelativename = ($myfileobj.FullName).Replace([System.IO.Path]::GetFullPath($Path), ".\")
$mylastwritetime = [int64]((New-TimeSpan -Start ([timezone]::CurrentTimeZone.ToLocalTime([datetime]'1/1/1970')) -End $myfileobj.LastWriteTime).TotalMilliseconds)
$mysize = ($myfileobj.Length)
$FT = [PSCustomObject]@{
FullName = ($myfileobj.FullName)
RelativeName = $myrelativename
Size = $mysize
LastWriteTime = $mylastwritetime
Hash = $myfilehash
};
$FileStream.Close(); $FileStream.Dispose();
$CryptoStream.Close(); $CryptoStream.Dispose(); $AlgorithmObj.Dispose();
}
} catch {
Write-Error "$($_.Exception.Message)`n`nStack trace: $($_.Exception.StackTrace)";
$Error = $true;
}
}
end {
if (($Combine -And ($Error -Eq $false)) -Or ((-Not $Combine) -And ($Error -Eq $false))) {
$result = $FT;
} else {
$result = $null;
}
$result
}
}
Export-Module -Function Get-HashEx
@kimboslice99
Copy link

kimboslice99 commented Sep 3, 2022

Very nice, seem to have issues with paths that have '[' in them though, I've tried escaping these brackets with no luck

Seem to be able to fix this by changing line 173 to -LiteralPath rather than just -Path

@pmolodo
Copy link

pmolodo commented May 1, 2024

This errored for me because it didn't recognize Export-Module - I changed it to Export-ModuleMember and it worked. Was this a typo, or was the function renamed at some point?

Here's the exact error I got:

Import-Module Get-HashEx
Export-Module : The term 'Export-Module' is not recognized as the name of a cmdlet, function, script file, or
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try
again.
At C:\Users\XXX\Documents\WindowsPowerShell\Modules\Get-HashEx\Get-HashEx.psm1:202 char:1
+ Export-Module -Function Get-HashEx
+ ~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Export-Module:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment