Private/Get-PathHash.ps1
function Get-PathHash { <# .SYNOPSIS Calculates SHA-256 hash for a file or directory. .DESCRIPTION Computes cryptographic hash values for files and directories to enable backup integrity verification and change detection. For directories, creates a composite hash based on all contained files and their paths. .PARAMETER Path The file or directory path to hash. .PARAMETER Algorithm The hash algorithm to use. Available options: SHA1, SHA256, SHA384, SHA512, MD5. Defaults to SHA256. .OUTPUTS String containing the computed hash value. .NOTES For directories, the hash is computed by: 1. Getting all files recursively with their relative paths 2. Computing hash for each file 3. Creating sorted list of "path:hash" entries 4. Hashing the concatenated string This provides meaningful change detection for directory structures. .EXAMPLE PS > Get-PathHash -Path 'C:\Documents\report.pdf' Returns SHA-256 hash of the file .EXAMPLE PS > Get-PathHash -Path 'C:\Documents' Returns composite SHA-256 hash of the directory contents #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $false)] [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')] [string] $Algorithm = 'SHA256' ) try { if (-not (Test-Path -Path $Path)) { Write-Warning "Get-PathHash> Path does not exist: $Path" return $null } if (Test-Path -Path $Path -PathType Leaf) { # File hash - straightforward Write-Verbose "Get-PathHash> Computing $Algorithm hash for file: $Path" $hash = Get-FileHash -Path $Path -Algorithm $Algorithm return $hash.Hash } else { # Directory hash - composite approach Write-Verbose "Get-PathHash> Computing $Algorithm composite hash for directory: $Path" $files = Get-ChildItem -Path $Path -File -Recurse | Sort-Object FullName if (-not $files) { Write-Verbose 'Get-PathHash> Empty directory, returning hash of empty string' $emptyHash = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm) $hashBytes = $emptyHash.ComputeHash([System.Text.Encoding]::UTF8.GetBytes('')) $emptyHash.Dispose() return [System.BitConverter]::ToString($hashBytes) -replace '-', '' } $hashEntries = @() $basePath = (Resolve-Path $Path).Path foreach ($file in $files) { try { $relativePath = $file.FullName.Substring($basePath.Length).TrimStart('\', '/') $fileHash = Get-FileHash -Path $file.FullName -Algorithm $Algorithm $hashEntries += "${relativePath}:$($fileHash.Hash)" } catch { Write-Warning "Get-PathHash> Failed to hash file $($file.FullName): $_" # Include path with error marker for consistency $relativePath = $file.FullName.Substring($basePath.Length).TrimStart('\', '/') $hashEntries += "${relativePath}:ERROR" } } # Create composite hash from sorted entries $sortedEntries = $hashEntries | Sort-Object $compositeString = $sortedEntries -join "`n" $hashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm) $hashBytes = $hashAlgorithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($compositeString)) $hashAlgorithm.Dispose() $finalHash = [System.BitConverter]::ToString($hashBytes) -replace '-', '' Write-Verbose "Get-PathHash> Computed composite hash from $($files.Count) files" return $finalHash } } catch { Write-Warning "Get-PathHash> Failed to compute hash for $Path : $_" return $null } } |