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

213 directory hashsum checksum

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

War*_*ung 253

正确的方法取决于你问的确切原因:

选项 1:仅比较数据

如果您只需要树文件内容的散列,这将起作用:

$ find -s somedir -type f -exec md5sum {} \; | md5sum
Run Code Online (Sandbox Code Playgroud)

这首先以可预测的顺序单独汇总所有文件内容,然后传递该文件名列表和 MD5 散列值以自行散列,给出一个值,该值仅在树中一个文件的内容更改时更改。

不幸的是,find -s只适用于 BSD find(1),在 macOS、FreeBSD、NetBSD 和 OpenBSD 中使用。要在具有 GNU 或 SUS find(1) 的系统上获得类似的东西,您需要更丑陋的东西:

$ find somedir -type f -exec md5sum {} \; | sort -k 2 | md5sum
Run Code Online (Sandbox Code Playgroud)

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

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

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

您很可能会问上面-k 2的 GNUsort命令中的位是否是必需的。鉴于文件数据的哈希值是文件名的适当代理,只要内容没有改变,如果我们删除此选项,我们将不会得到误报,允许我们对 GNU 和 BSD 使用相同的命令sort。但是,要意识到文件名的确切顺序有很小的可能性(MD5 为1:2 128)与不匹配的部分顺序不匹配,-k 2如果发生哈希冲突,则不匹配。但是请记住,如果这种不匹配的小概率对您的应用程序很重要,那么整个方法对您来说可能是不可能的。

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

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

选项 2:比较数据元数据

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

$ tar -cf - somedir | md5sum
Run Code Online (Sandbox Code Playgroud)

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

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

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

如果您发现自己收到误报,我建议您find | cpio选择Gilles 的回答中的选项。

  • 我认为最好导航到正在比较的目录并使用 `find .` 而不是 `find somedir`。这样,当提供不同的路径规范来查找时,文件名是相同的;这可能很棘手:-) (11认同)
  • 我需要在最后一个 `md5sum` 之前添加另一个管道:` | 切-f 1 -d''`。也就是截断文件路径,否则`md5sum`对路径前缀进行哈希处理,这当然是不同的。 (4认同)

Gil*_*il' 45

校验和需要是文件作为字符串的确定性和明确表示。确定性意味着如果您将相同的文件放在相同的位置,您将获得相同的结果。明确意味着两组不同的文件具有不同的表示。

数据和元数据

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

一种解决方案是在存档之前对文件名进行排序。如果您的文件名不包含换行符,您可以运行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
Run Code Online (Sandbox Code Playgroud)

只有名称和内容,低技术的方式

如果您只想考虑文件数据而不考虑元数据,您可以制作一个仅包含文件内容的存档,但没有标准工具可以做到这一点。您可以包含文件的哈希值,而不是包含文件内容。如果文件名不包含换行符,并且只有常规文件和目录(没有符号链接或特殊文件),这很容易,但您需要注意以下几点:

{ 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
Run Code Online (Sandbox Code Playgroud)

除了校验和列表之外,我们还包括一个目录列表,否则空目录将不可见。文件列表已排序(在特定的、可重现的语言环境中——感谢 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
Run Code Online (Sandbox Code Playgroud)

更稳健的方法

这是一个经过最少测试的 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()
Run Code Online (Sandbox Code Playgroud)


Dee*_*tal 17

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

尝试这个:

diff -qr dir1 dir2
Run Code Online (Sandbox Code Playgroud)


小智 15

看看md5deep。您可能会感兴趣的 md5deep 的一些功能:

递归操作 - md5deep 能够递归检查整个目录树。也就是说,为目录中的每个文件和每个子目录中的每个文件计算 MD5。

比较模式 - md5deep 可以接受已知哈希列表并将它们与一组输入文件进行比较。该程序可以显示那些与已知散列列表匹配或不匹配的输入文件。

...

  • md5deep 本身并不能解决 OP 的问题,因为它不打印合并的 md5sum,它只是打印目录中每个文件的 md5sum。也就是说,您可以对 md5deep 的输出进行 md5sum - 不是 OP 想要的_相当_,但很接近!例如对于当前目录:`md5deep -r -l -j0。| md5sum`(其中`-r` 是递归的,`-l` 表示“使用相对路径”,以便在尝试比较两个目录的内容时文件的绝对路径不会干扰,而`-j0` 表示使用1 个线程,以防止由于以不同顺序返回的各个 md5sum 导致的不确定性)。 (3认同)

Dmi*_*nov 10

使用checksumdir

$ pip install checksumdir
$ checksumdir -a md5 assets/js
981ac0bc890de594a9f2f40e00f13872
$ checksumdir -a sha1 assets/js
88cd20f115e31a1e1ae381f7291d0c8cd3b92fad
Run Code Online (Sandbox Code Playgroud)

更快更容易比其他的bash解决方案。


Pav*_*sov 7

您可以递归地散列每个文件,然后散列结果文本:

> md5deep -r -l . | sort | md5sum
d43417958e47758c6405b5098f151074 *-
Run Code Online (Sandbox Code Playgroud)

md5deep是必需的。

  • 我尝试过 hashdeep。它不仅输出哈希值,还输出一些标头,包括“## Invoked from: /home/myuser/dev/”(这是您当前的路径)和“## $ hashdeep -s -r -l ~/folder/”。这必须进行排序,因此如果您更改当前文件夹或命令行,最终的哈希值将会有所不同。 (2认同)

小智 5

nix-hash来自Nix 包管理器

命令 nix-hash 计算每个路径内容的加密散列并将其打印在标准输出上。默认情况下,它计算 MD5 哈希,但也可以使用其他哈希算法。哈希以十六进制打印。

散列是在每个路径的序列化上计算的:以路径为根的文件系统树的转储。这允许对目录和符号链接以及常规文件进行哈希处理。转储采用 nix-store --dump 生成的 NAR 格式。因此, nix-hash path 产生与 nix-store --dump path | 相同的加密哈希。md5sum。