介绍
归档中可能有名称不同却完全相同的图片,也可能有大小相同却内容不同的图片。你将实现一个可复用重复文件查找器的字节读取部分,并在不改动原件的情况下保存完整清单。
你应已了解 Java 类、方法、列表、异常及基本文件读取。本实验介绍二进制输入、自动关闭资源和 SHA-256。目录适配器和命令启动器已准备好;摘要计算由你实现。这是只读练习,不是磁盘清理工具。作者估计需要 50 分钟,包含阅读和常见错误修正。
读取字节,而不是文件名
在这一步中,你将先比较一个正常图片样例,再实现恰好读取每个字节一次的摘要。打开 WebIDE,选择 Terminal > New Terminal,后续始终使用这个集成终端。每个实验独立开始,拥有自己的文件。
cd 改变当前终端的工作目录。准备好的 run.sh 在全新的临时构建目录中编译 src/*.java,再运行指定的 Java 类。编译器使用 -encoding UTF-8,避免源文本依赖终端的区域设置。只有编译器和 Java 子进程使用 C.UTF-8;Java 子进程也默认使用 UTF-8 文本编码,因此能处理支持范围内的 UTF-8 文件名,而不改动你的 shell 设置。
cd /home/labex/project/duplicates
./run.sh Sample
辅助程序解码真实的 PNG 文件并比较其字节。它会输出 red = renamed: true 和 red = blue: false。三张图片的尺寸和字节长度都相同。名称或长度能缩小搜索范围,却不能证明内容相等。也可以在 WebIDE Explorer 中展开 duplicates/data/gallery,点击 red.png、blue.png 和 copies/renamed.png 查看图片。这是可选展示;必需的比较是上面 Sample 的解码与字节结果。这些是本课程原创的合成图片,无需外部素材。
公开的 Digester 接口约定了一个方法:digest(Path) 返回十六进制字符串,读取失败时抛出 IOException。Path 标识文件;InputStream 按二进制字节读取,不把它们当作文本。SHA-256 将这些字节浓缩为固定长度的候选指纹。后续单元仍会比较实际内容,才会声明文件重复。
替换初始实现之前,先阅读这个小接口和初始代码:
cat src/Digester.java src/Sha256.java
程序重复使用一个 1024 字节缓冲区。read(buffer) 返回本次有效字节数,到达末尾时返回 -1。最后一次读取可能只填满部分缓冲区,其余字节还保留着之前的值。因此 sha.update(buffer, 0, count) 必须使用本次实际数量。空文件不进行任何更新,也仍有合法摘要。
try (InputStream ... ) 是 try-with-resources:代码块结束时,Java 会关闭流,包括读取抛出异常的情况。关闭操作释放文件资源,并不保证立即进行垃圾回收。MessageDigest.getInstance 可能抛出受检的算法异常,因此外层 catch 将它转换为同一种文件操作错误约定。十六进制用两个字符表示一个字节;& 255 将有符号 Java 字节视为无符号值。
下一个命令写入完整替换内容。> 替换这个源文件。带引号的 here-document 标记 <<'JAVA' 将中间文本原样传给 cat;末尾的 JAVA 必须单独占一行,不能缩进。你也可以在 WebIDE 中编辑并保存同一文件。如果粘贴后终端仍停在未结束的 here-document 提示符,按 Ctrl+C 取消,再完整粘贴替换命令,包括最后的标记。
cat > src/Sha256.java <<'JAVA'
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Sha256 implements Digester {
public String digest(Path path) throws IOException {
try {
MessageDigest sha = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[1024];
try (InputStream input = Files.newInputStream(path)) {
int count;
while ((count = input.read(buffer)) != -1) {
sha.update(buffer, 0, count);
}
}
StringBuilder hex = new StringBuilder();
for (byte value : sha.digest()) {
hex.append(String.format("%02x", value & 255));
}
return hex.toString();
} catch (NoSuchAlgorithmException error) {
throw new IOException("SHA-256 unavailable", error);
}
}
}
JAVA
重新编译并运行同一个样例。编译会检查新类,而样例对内容相等性的判断应保持不变。
./run.sh Sample
如果 Java 报告行号,重新打开 src/Sha256.java,对照替换内容检查花括号和拼写,保存后重跑。如果路径错误,用 pwd 查看当前目录,再执行上面的绝对路径 cd。不需要重置 VM。
保存每条路径及其指纹
在这一步中,你将使用自己实现的摘要,创建完整的 UTF-8 清单。继续在同一个 WebIDE 集成终端中操作,工作目录为 /home/labex/project/duplicates。
准备好的 Inputs.files 适配器仅遍历指定目录,并关闭目录流。它会拒绝符号链接、特殊文件及含制表符或换行符的路径,而不是静默跟随或跳过。仅支持稳定的普通文件和目录。扫描期间不要改动输入。属性检查能发现部分变化,但不会冻结目录,也不提供并发快照保证。
准备好的 Inventory 在全部读取成功后保存每条相对路径、大小和摘要。其 TSV 格式用制表符分隔各列。报告适配器先向新的临时文件写入 UTF-8,再替换报告路径,因此即使已有报告是硬链接,也不会覆盖原件的字节。这不保证崩溃后的持久性。输出必须位于扫描根目录之外。reports 已与 data 并列存在,所以报告不会意外成为自己的输入。命令参数顺序是 Inventory ROOT REPORT:第一个参数是待扫描目录,第二个是输出文件路径,不要颠倒。
./run.sh Inventory data/gallery reports/inventory.tsv
预期输出 Inventory complete: 11 files。用 cat 显示保存的文本:
cat reports/inventory.tsv
表头为 PATH、SIZE、SHA256。比较 red.png、copies/renamed.png 和 copies/third.png 的行:不同路径共享同一摘要。blue.png 大小相同,摘要却不同。两个空文件大小为零,具有相同的合法摘要。tail.bin 最后一次读取只有部分缓冲区有效;它的匹配副本仍应得到相同摘要。同名的 north/readme.txt 和 south/readme.txt 仍分别占一行。
现在练习一次常见路径错误。下一个命令会故意失败,退出状态为 2,并显示 Scan failed;它不能生成新报告。失败不等于成功生成空清单。任何旧报告都不能证明这次失败的运行完成了任务。
./run.sh Inventory data/missing reports/missing.tsv
修正输入路径,创建新报告:
./run.sh Inventory data/gallery reports/recovered.tsv
读取失败遵循相同规则:显示错误并返回失败,绝不能静默遗漏无法读取的文件。检查会在隔离的空文件、不满缓冲区和不可读文件夹具上运行你当前的源码,并读取真实清单。所有原件必须保留路径、字节、权限、归属和修改时间;不要删除或替换原件。普通读取可能更新访问时间。你现在获得了候选事实;下个实验会对相同指纹的候选进行真实字节比较。
总结
你使用 try-with-resources 读取二进制文件,按实际读取数量更新 SHA-256,并保存了完整的 UTF-8 清单。名称、大小和摘要都是候选事实;实际字节比较将确认重复文件组。



