跳到主要内容

file-hash-checker-20261106

收藏0

本地离线文件哈希校验工具,计算单个或目录下多个文件的 SHA256/SHA1/SHA384/SHA512/MD5 哈希,可生成标准校验清单并按清单核验文件完整性,输出 OK/FAILED/缺失状态,全程不联网。

让 AI 安装

Mercury最新版本 0.1.0更新于 2026-11-06下载 0 次

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')]
26param(
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
47Set-StrictMode -Version 2.0
48
49# 收集文件
50function 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
69if ($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# 计算模式
125if (-not $Path -or @($Path).Count -eq 0) {
126 throw '计算模式必须提供 -Path。'
127}
128$files = @(Resolve-TargetFiles -Targets $Path -RecurseFlag:$Recurse)
129if ($files.Count -eq 0) {
130 throw '没有找到可计算的文件。'
131}
132
133$records = New-Object System.Collections.Generic.List[object]
134foreach ($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 时做比对
144if ($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
154if ($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}

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。