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. 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 } } |