scripts/Get-FileChecksum.ps1
5.8 KB| 1 | #requires -Version 5.1 |
| 2 | <# |
| 3 | .SYNOPSIS |
| 4 | 计算文件哈希或按校验清单核验文件完整性。 |
| 5 | .DESCRIPTION |
| 6 | 基于 Get-FileHash 实现:支持单文件/多文件/目录递归计算,五种哈希算法, |
| 7 | 可生成 sha256sum 风格清单,或按清单逐个核验并汇总 OK/FAILED/MISSING。全程离线。 |
| 8 | .PARAMETER Path |
| 9 | 一个或多个文件/目录路径(计算模式)。 |
| 10 | .PARAMETER Algorithm |
| 11 | 哈希算法:SHA256、SHA1、SHA384、SHA512、MD5,默认 SHA256。 |
| 12 | .PARAMETER Recurse |
| 13 | 目录递归处理子文件夹。 |
| 14 | .PARAMETER Expected |
| 15 | 预期哈希值,传入时与实际值比对。 |
| 16 | .PARAMETER Manifest |
| 17 | 将哈希结果写入校验清单(UTF-8 无 BOM)。 |
| 18 | .PARAMETER Verify |
| 19 | 校验模式:指定清单文件路径。 |
| 20 | .EXAMPLE |
| 21 | .\Get-FileChecksum.ps1 -Path .\dist -Recurse -Manifest .\dist\SHA256SUMS.txt |
| 22 | .EXAMPLE |
| 23 | .\Get-FileChecksum.ps1 -Verify .\dist\SHA256SUMS.txt |
| 24 | #> |
| 25 | [CmdletBinding(DefaultParameterSetName = 'Compute')] |
| 26 | param( |
| 27 | [Parameter(ParameterSetName = 'Compute', Position = 0)] |
| 28 | [string[]]$Path, |
| 29 | |
| 30 | [Parameter(ParameterSetName = 'Compute')] |
| 31 | [ValidateSet('SHA256', 'SHA1', 'SHA384', 'SHA512', 'MD5')] |
| 32 | [string]$Algorithm = 'SHA256', |
| 33 | |
| 34 | [Parameter(ParameterSetName = 'Compute')] |
| 35 | [switch]$Recurse, |
| 36 | |
| 37 | [Parameter(ParameterSetName = 'Compute')] |
| 38 | [string]$Expected, |
| 39 | |
| 40 | [Parameter(ParameterSetName = 'Compute')] |
| 41 | [string]$Manifest, |
| 42 | |
| 43 | [Parameter(ParameterSetName = 'Verify', Mandatory = $true)] |
| 44 | [string]$Verify |
| 45 | ) |
| 46 | |
| 47 | Set-StrictMode -Version 2.0 |
| 48 | |
| 49 | # 收集文件 |
| 50 | function Resolve-TargetFiles { |
| 51 | param([string[]]$Targets, [switch]$RecurseFlag) |
| 52 | $files = New-Object System.Collections.Generic.List[string] |
| 53 | foreach ($t in $Targets) { |
| 54 | if (-not (Test-Path -LiteralPath $t)) { |
| 55 | Write-Warning "路径不存在,已跳过:$t"; continue |
| 56 | } |
| 57 | $item = Get-Item -LiteralPath $t |
| 58 | if ($item.PSIsContainer) { |
| 59 | $childParams = @{ LiteralPath = $item.FullName; File = $true; Force = $true } |
| 60 | if ($RecurseFlag) { $childParams.Recurse = $true } |
| 61 | Get-ChildItem @childParams | ForEach-Object { $files.Add($_.FullName) } |
| 62 | } else { |
| 63 | $files.Add($item.FullName) |
| 64 | } |
| 65 | } |
| 66 | return $files |
| 67 | } |
| 68 | |
| 69 | if ($PSCmdlet.ParameterSetName -eq 'Verify') { |
| 70 | # 校验模式 |
| 71 | if (-not (Test-Path -LiteralPath $Verify -PathType Leaf)) { |
| 72 | throw "找不到清单文件:$Verify" |
| 73 | } |
| 74 | $manifestFull = (Resolve-Path -LiteralPath $Verify).Path |
| 75 | $baseDir = Split-Path -Parent $manifestFull |
| 76 | $lines = [System.IO.File]::ReadAllLines($manifestFull) |
| 77 | |
| 78 | # 清单首行可含 # algorithm: <name>,默认 SHA256 |
| 79 | $algoName = 'SHA256' |
| 80 | foreach ($l in $lines) { |
| 81 | if ($l -match '^#\s*algorithm:\s*([A-Za-z0-9]+)') { |
| 82 | $algoName = $matches[1].ToUpper(); break |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | $okCount = 0; $failCount = 0; $missingCount = 0 |
| 87 | $results = New-Object System.Collections.Generic.List[object] |
| 88 | |
| 89 | foreach ($line in $lines) { |
| 90 | if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) { continue } |
| 91 | $parts = $line -split '\s+', 2 |
| 92 | if ($parts.Count -lt 2) { continue } |
| 93 | $expectedHash = $parts[0].Trim().ToLower() |
| 94 | $fileName = $parts[1].Trim() |
| 95 | if ($fileName.StartsWith('*')) { $fileName = $fileName.Substring(1) } |
| 96 | |
| 97 | $target = if ([System.IO.Path]::IsPathRooted($fileName)) { |
| 98 | $fileName |
| 99 | } else { |
| 100 | [System.IO.Path]::GetFullPath((Join-Path $baseDir $fileName)) |
| 101 | } |
| 102 | |
| 103 | if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { |
| 104 | $missingCount++ |
| 105 | $results.Add([pscustomobject]@{ Status = 'MISSING'; File = $fileName }) |
| 106 | continue |
| 107 | } |
| 108 | |
| 109 | $actualHash = (Get-FileHash -LiteralPath $target -Algorithm $algoName).Hash.ToLower() |
| 110 | if ($actualHash -eq $expectedHash) { |
| 111 | $okCount++ |
| 112 | $results.Add([pscustomobject]@{ Status = 'OK'; File = $fileName }) |
| 113 | } else { |
| 114 | $failCount++ |
| 115 | $results.Add([pscustomobject]@{ Status = 'FAILED'; File = $fileName }) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | $results | Format-Table -AutoSize | Out-String | Write-Output |
| 120 | "SUMMARY: OK=$okCount FAILED=$failCount MISSING=$missingCount ALGORITHM=$algoName" |
| 121 | if ($failCount -gt 0 -or $missingCount -gt 0) { exit 1 } else { exit 0 } |
| 122 | } |
| 123 | |
| 124 | # 计算模式 |
| 125 | if (-not $Path -or @($Path).Count -eq 0) { |
| 126 | throw '计算模式必须提供 -Path。' |
| 127 | } |
| 128 | $files = @(Resolve-TargetFiles -Targets $Path -RecurseFlag:$Recurse) |
| 129 | if ($files.Count -eq 0) { |
| 130 | throw '没有找到可计算的文件。' |
| 131 | } |
| 132 | |
| 133 | $records = New-Object System.Collections.Generic.List[object] |
| 134 | foreach ($f in $files) { |
| 135 | $hash = (Get-FileHash -LiteralPath $f -Algorithm $Algorithm).Hash.ToLower() |
| 136 | $records.Add([pscustomobject]@{ |
| 137 | Algorithm = $Algorithm |
| 138 | Hash = $hash |
| 139 | Path = $f |
| 140 | }) |
| 141 | } |
| 142 | |
| 143 | # 单文件且传 Expected 时做比对 |
| 144 | if ($Expected -and $files.Count -eq 1) { |
| 145 | $actual = $records[0].Hash |
| 146 | if ($actual -eq $Expected.Trim().ToLower()) { |
| 147 | "MATCH: $actual" |
| 148 | } else { |
| 149 | "MISMATCH: expected $($Expected.Trim().ToLower()) actual $actual" |
| 150 | exit 1 |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | if ($Manifest) { |
| 155 | $manifestDir = Split-Path -Parent ([System.IO.Path]::GetFullPath($Manifest)) |
| 156 | $manifestLines = New-Object System.Collections.Generic.List[string] |
| 157 | $manifestLines.Add("# algorithm: $Algorithm") |
| 158 | foreach ($r in $records) { |
| 159 | $rel = [System.IO.Path]::GetRelativePath($manifestDir, $r.Path).Replace('\', '/') |
| 160 | $manifestLines.Add("$($r.Hash) $rel") |
| 161 | } |
| 162 | [System.IO.File]::WriteAllLines([System.IO.Path]::GetFullPath($Manifest), $manifestLines, (New-Object System.Text.UTF8Encoding($false))) |
| 163 | "MANIFEST_WRITTEN: $([System.IO.Path]::GetFullPath($Manifest)) ($($records.Count) files, $Algorithm)" |
| 164 | } else { |
| 165 | $records | Format-Table Algorithm, Hash, Path -AutoSize | Out-String | Write-Output |
| 166 | } |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

