判断哪些下载文件需要修复

ShellBeginner
立即练习

介绍

Field Notes 正在筹备一个社区周末活动。资源包中有 PDF 指南、活动资料和一个允许为空的可选致谢文件。仅凭文件名和看起来合理的大小,无法判断下载是否完整。在获取任何新文件之前,你将生成一份状态清单,让组织者知道具体哪些文件需要处理。

你应已在 Shell for Beginners 中学习过 Bash 的基本参数、条件和循环。本实验会讲解 SHA256、保留完整行的读取方法以及这里使用的解析方式。工具和原创合成资源均已准备好。Workspace 和 Download Desk 共用一个容器;本实验独立初始化。预计用时包含阅读和普通输入错误的处理。

建立可靠的文件状态清单

在这一步中,你将依据可信校验清单,生成实际本地资源包的状态清单,不修复或改写其中任何文件。

先查看一个有用的文件

从 Workspace 开始,这个终端已准备好处理 UTF-8 文本。你的工作目录是 /home/labex/project/download-desk。pwd 显示当前目录。如果找不到某个相对路径,请用 ls 检查,并通过下面完整的 cd 命令返回工作目录。

cd /home/labex/project/download-desk
ls pack

切换到 Download Desk。在 Welcome guide.pdf 旁选择 Open file。新打开的浏览器页面应显示社区活动日程和东入口集合点。必要时使用浏览器的 PDF 缩放控件;PDF 使用固定页面,并不会像移动网页那样重新排列文本。返回 Download Desk 页面,选择该文件旁的 Download,保存同一份本地文件。然后切回 Workspace。

Download Desk 中指向实际本地资源的链接

使用 Welcome guide.pdf 旁的 Open file 或 Download。

这个阅读器只提供 pack 中实际存在的文件,不能从来源补回缺失文件。文本资源可能显示为纯文本,可选的空文件可能显示为空白页面。仅凭这些现象仍不能判断文件是否正确。

使用独立的参考清单

组织者单独提供了 /opt/labex/download-pack-state/expected.sha256,它不在可写的资源包中。SHA256 摘要是文件字节的一个 64 字符指纹。与独立可信的值比较,可以发现字节不同的文件,包括大小完全相同的损坏文件。它不能证明发布者身份:如果文件及其校验值都由攻击者提供,两者一致并不能让该发布者变得可信。不要用待检查文件重新生成这份参考清单。

cat 显示文件内容,不会修改文件:

cat /opt/labex/download-pack-state/expected.sha256

每一行由 64 个小写十六进制字符组成的摘要、两个空格以及完整文件名构成。本课程使用提供好的、可读、非空、以换行符结尾且严格符合此格式的清单。文件名是单层的可打印 ASCII 名称,可以包含空格。斜杠、反斜杠、CR、LF、制表符以及 .、.. 这两个名称不在支持范围内。资源包及其目录链都是真实目录;清单中的成员是本地普通文件,或尚不存在。不支持链接文件、在成员名称处出现的子目录,以及任意转义校验格式。在这个诊断实验中,请保持所有原资源包成员和 notes.txt 不变。

sha256sum 输出文件摘要和名称。双引号让带空格的文件名保持为一个参数。-- 表示命令选项结束。目录前缀也能防止以 - 开头的文件基本名称被解释为选项。

sha256sum -- "pack/Workshop card.txt"

将输出的摘要与参考清单中对应的一行比较。两者不同。这个损坏文件与正确资料的字节数相同,因此只看大小无法发现问题。

sha256sum --check 读取清单,并以当前目录为基准检查其中列出的路径。圆括号让命令在子 shell 中执行:其中的 cd 不会改变外层终端的目录。

(cd pack && sha256sum --check /opt/labex/download-pack-state/expected.sha256)

这里的 && 表示只有成功进入 pack 后才运行检查。应看到 Welcome guide.pdf: OK、Workshop card.txt: FAILED、Map notes.txt: FAILED open or read、Optional credits.txt: OK,以及一些警告。由于存在缺失文件和错误字节,这条命令预期会失败。$? 保存上一条命令的原生退出状态;必须立即读取,否则后续命令会覆盖它。printf 用于格式化输出,\n 用于结束一行。

printf 'Check exit: %s\n' "$?"

应看到 Check exit: 1。零字节的致谢文件是正确的,因为它的摘要匹配。“非空”要求针对的是校验清单,而不是每一个资源文件。只看到一行 OK,不能说明整个资源包都正确。

逐行读取时保留完整名称

组织者需要一份紧凑的状态清单:按校验清单顺序,每行包含一个状态和一个完整文件名。你将创建 inventory.sh,用 $1 接收校验清单,用 $2 接收资源包目录。它是一个独立程序,仅依赖这些参数和已安装命令,不依赖旁边的辅助文件。

编写之前,先理解它使用的 shell 语法:

  • while IFS= read -r line 每次读取一整行。空的 IFS 防止首尾空格被去掉;-r 禁止反斜杠处理。done < "$manifest" 将清单重定向给循环,而不是从终端等待输入。
  • ${line:0:64} 提取字符位置 0 到 63 的摘要。${line:66} 提取两个分隔空格后面的全部内容,不会把文件名拆成多个单词。
  • [ -f "$target" ] 判断支持范围内的目标是否为普通文件。在执行校验命令前,程序就会先识别缺失成员。
  • actual=$(sha256sum -- "$target") 捕获命令输出。${actual%% *} 删除末尾匹配“空格后跟任意内容”的最长部分,只留下摘要。在前面限定的文件名格式下,这种处理是安全的。
  • printf '%s\t%s\n' 输出两个值,中间用制表符分隔,末尾换行。引号保证每个完整文件名都是一个参数。

下面的 here-document 将字面文本保存到脚本中。cat > inventory.sh 会替换该脚本文件;<<'BASH' 读取文本,直到遇到只有 BASH 的一行。给分隔符加引号,可以防止当前 shell 在保存脚本时提前展开其中的变量。第一行标明解释器为 Bash;这里会用 bash 显式运行文件。

cat > inventory.sh <<'BASH'
#!/usr/bin/env bash
manifest=$1
pack=$2
while IFS= read -r line; do
    expected=${line:0:64}
    name=${line:66}
    target=$pack/$name
    status=MISSING
    if [ -f "$target" ]; then
        actual=$(sha256sum -- "$target")
        actual=${actual%% *}
        status=MISMATCH
        if [ "$actual" = "$expected" ]; then
            status=MATCH
        fi
    fi
    printf '%s\t%s\n' "$status" "$name"
done < "$manifest"
BASH

使用提供的参考清单和本地资源包运行脚本。> 将标准输出重定向到 inventory.tsv,每次运行都会替换这份报告。TSV 表示用制表符分隔的值。这是供组织者使用的诊断成果,不会修复资源。

bash inventory.sh /opt/labex/download-pack-state/expected.sha256 pack > inventory.tsv
cat inventory.tsv

应看到下面这些行,状态与文件名之间用一个制表符分隔:

MATCH Welcome guide.pdf
MISMATCH Workshop card.txt
MISSING Map notes.txt
MATCH Optional credits.txt

即使报告中含有 MISSING,脚本也可以返回零退出状态:它负责分类,并不负责宣称资源包完整。报告是否正确,取决于它与实际文件及可信参考清单之间的关系。检查还会将你的脚本用于另一套文件名不同、包含空文件的独立资源包,不要求固定采用这里的循环写法。

如果脚本某行输错了,请重新执行完整的 here-document,只替换 inventory.sh,再运行生成报告的命令。如果误在 pack 中运行命令,请用上面的绝对路径 cd 命令返回。保持 pack、可信参考清单和 notes.txt 不变。再次运行状态清单命令应得到相同报告,因为它只读取资源包。下一项实验会用这些分类结果决定应下载什么。

总结

你打开了真实资源,将资源包与独立的 SHA256 参考清单进行比较,并用 Bash 建立了保留完整行的文件状态清单。你区分了缺失文件、字节不匹配的文件和正确的空文件,同时保持原资源包不变。这份状态清单为后续每一次修复提供了依据。