如何将目录内容的 MD5 和作为一个和?

如何将目录内容的 MD5 和作为一个和?

md5sum 程序不提供目录的校验和。我想要获取目录全部内容(包括子目录中的文件)的单个 MD5 校验和。也就是说,由所有文件组成的一个组合校验和。有没有办法做到这一点?

答案1

正确的方法取决于您提出问题的确切原因:

选项 1:仅比较数据

如果您只需要树的文件内容的哈希值,则可以这样做:

$ find -s somedir -type f -exec md5sum {} \; | md5sum

首先以可预测的顺序单独汇总所有文件内容,然后传递文件名列表和 MD5 哈希值以对其进行哈希处理,从而给出一个值,该值仅在树中某个文件的内容发生更改时才会发生变化。

不幸的是,find -s仅适用于 BSD find(1),用于 macOS、FreeBSD、NetBSD 和 OpenBSD。为了在 GNU 或 SUS find(1) 系统上获得类似的东西,你需要一些更丑陋的东西:

$ find somedir -type f -exec md5sum {} \; | sort -k 2 | md5sum

我们find -s通过添加对sort.该-k 2位告诉它跳过 MD5 散列,因此它只对文件名进行排序,这些文件名位于字段 2 到行尾的sort计算中。

这个版本的命令有一个弱点,如果你有任何带有换行符的文件名,它很容易变得混乱,因为它看起来像多行调用sort。该find -s变体没有这个问题,因为树遍历和排序发生在同一个程序中find

无论哪种情况,排序都是必要的,以避免误报:最常见的 Unix/Linux 文件系统不会以稳定、可预测的顺序维护目录列表。您可能没有通过使用ls等方式意识到这一点,它会默默地为您对目录内容进行排序。不以某种方式对其输出进行排序的调用find将导致输出中的行顺序与底层文件系统返回它们的任何顺序相匹配,如果作为输入提供给它的文件顺序发生变化,这将导致该命令给出更改的哈希值,即使数据保持相同。

您可能会问上面的-k 2GNUsort命令中的位是否是必要的。鉴于只要内容未更改,文件数据的哈希值就可以充分代表文件名,因此如果删除此选项,我们将不会得到误报,从而允许我们在 GNU 和 BSD 中使用相同的命令sort。但是,请注意,如果存在哈希冲突,文件名的确切顺序与不执行该操作可能给出的部分顺序不匹配的可能性很小(MD5 为 1:2 128-k 2 )。但请记住,如果如此小的不匹配机会对您的应用程序很重要,那么整个方法对您来说可能是不可能的。

您可能需要将md5sum命令更改为md5或其他一些哈希函数。如果您选择另一个哈希函数并且您的系统需要第二种形式的命令,则可能需要sort相应地调整命令。另一个陷阱是一些数据求和程序根本不写出文件名,一个典型的例子是旧的 Unixsum程序。

此方法效率有些低,需要调用md5sumN+1 次,其中 N 是树中的文件数量,但这是避免散列文件和目录元数据的必要成本。

选项 2:比较数据元数据

如果您需要能够检测到这一点任何事物树中的内容已更改,而不仅仅是文件内容,请要求tar为您打包目录内容,然后将其发送到md5sum

$ tar -cf - somedir | md5sum

因为tar还可以看到文件权限、所有权等,所以这还将检测这些内容的更改,而不仅仅是文件内容的更改。

这种方法要快得多,因为它只对树进行一次遍历,并且只运行一次哈希程序。

find上面的基于方法一样,tar将按照底层文件系统返回文件名的顺序处理文件名。很可能在您的应用程序中,您可以确定不会导致这种情况发生。我可以想到至少三种不同的使用模式,其中可能存在这种情况。 (我不会列出它们,因为我们正在进入未指定的行为领域。这里的每个文件系统都可能不同,甚至从一个版本的操作系统到下一个版本也是如此。)

如果您发现自己出现误报,我建议您使用以下find | cpio选项吉尔斯的回答

答案2

校验和必须是文件的确定性且明确的字符串表示形式。确定性意味着如果将相同的文件放在相同的位置,您将得到相同的结果。明确意味着两组不同的文件具有不同的表示形式。

数据和元数据

制作包含这些文件的存档是一个好的开始。这是一个明确的表示(显然,因为您可以通过提取存档来恢复文件)。它可能包括文件元数据,例如日期和所有权。然而,这还不太正确:存档是不明确的,因为它的表示取决于文件存储的顺序,以及压缩(如果适用)。

解决方案是在归档之前对文件名进行排序。如果您的文件名不包含换行符,您可以运行find | sort列出它们,并按此顺序将它们添加到存档中。请注意告诉归档程序不要递归到目录中。以下是 POSIX pax、GNU tar 和 cpio 的示例:

find | LC_ALL=C sort | pax -w -d | md5sum
find | LC_ALL=C sort | tar -cf - -T - --no-recursion | md5sum
find | LC_ALL=C sort | cpio -o | md5sum

仅名称和内容,低技术含量的方式

如果您只想考虑文件数据而不考虑元数据,则可以制作仅包含文件内容的存档,但没有标准工具可以实现这一点。您可以包含文件的哈希值,而不是包含文件内容。如果文件名不包含换行符,并且只有常规文件和目录(没有符号链接或特殊文件),则这相当简单,但您确实需要注意一些事项:

{ export LC_ALL=C;
  find -type f -exec wc -c {} \; | sort; echo;
  find -type f -exec md5sum {} + | sort; echo;
  find . -type d | sort; find . -type d | sort | md5sum;
} | md5sum

除了校验和列表之外,我们还包括一个目录列表,否则空目录将不可见。文件列表已排序(在特定的、可重现的区域设置中 - 感谢 Peter.O 提醒我这一点)。echo将这两部分分开(如果没有这个,您可以创建一些空目录,其名称看起来像md5sum也可以传递给普通文件的输出)。我们还列出了文件大小,以避免长度扩展攻击

顺便说一下,MD5 已被弃用。如果可用,请考虑使用 SHA-2,或至少使用 SHA-1。

名称和数据,支持名称中的换行符

下面是上面代码的一个变体,它依赖于 GNU 工具来用空字节分隔文件名。这允许文件名包含换行符。 GNU 摘要实用程序在其输出中引用特殊字符,因此不会出现不明确的换行符。

{ export LC_ALL=C;
  du -0ab | sort -z; # file lengths, including directories (with length 0)
  echo | tr '\n' '\000'; # separator
  find -type f -exec sha256sum {} + | sort -z; # file hashes
  echo | tr '\n' '\000'; # separator
  echo "End of hashed data."; # End of input marker
} | sha256sum

更稳健的方法

这是一个经过最低限度测试的 Python 脚本,它构建一个描述文件层次结构的哈希值。它将目录和文件内容记入帐户并忽略符号链接和其他文件,如果无法读取任何文件,则返回致命错误。

#! /usr/bin/env python
import hashlib, hmac, os, stat, sys
## Return the hash of the contents of the specified file, as a hex string
def file_hash(name):
    f = open(name)
    h = hashlib.sha256()
    while True:
        buf = f.read(16384)
        if len(buf) == 0: break
        h.update(buf)
    f.close()
    return h.hexdigest()
## Traverse the specified path and update the hash with a description of its
## name and contents
def traverse(h, path):
    rs = os.lstat(path)
    quoted_name = repr(path)
    if stat.S_ISDIR(rs.st_mode):
        h.update('dir ' + quoted_name + '\n')
        for entry in sorted(os.listdir(path)):
            traverse(h, os.path.join(path, entry))
    elif stat.S_ISREG(rs.st_mode):
        h.update('reg ' + quoted_name + ' ')
        h.update(str(rs.st_size) + ' ')
        h.update(file_hash(path) + '\n')
    else: pass # silently symlinks and other special files
h = hashlib.sha256()
for root in sys.argv[1:]: traverse(h, root)
h.update('end\n')
print h.hexdigest()

答案3

如果您的目标只是查找两个目录之间的差异,请考虑使用 diff。

尝试这个:

diff -qr dir1 dir2

答案4

使用checksumdir:

$ pip install checksumdir
$ checksumdir -a md5 assets/js
981ac0bc890de594a9f2f40e00f13872
$ checksumdir -a sha1 assets/js
88cd20f115e31a1e1ae381f7291d0c8cd3b92fad

快点更轻松比其他 bash 解决方案。

相关内容