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


171

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

Answers:


186

正确的方法完全取决于您要问的原因:

选项1:仅比较数据

如果您只需要哈希树文件内容的哈希值,那么就可以做到这一点:

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

这首先以可预测的顺序分别总结了所有文件内容,然后传递该文件名列表和MD5哈希值本身进行哈希处理,从而给出一个仅当树中文件之一的内容更改时才会更改的单个值。

不幸的是,find -s仅适用于macOS,FreeBSD,NetBSD和OpenBSD中使用的BSD find(1)。为了在具有GNU或SUS find(1)的系统上获得可比的东西,您需要一些难看的东西:

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

我们已将find -s呼叫替换为sort。该-k 2位告诉它跳过MD5哈希值,因此它仅通过sort估算来对文件名进行排序,这些文件名通过行尾在字段2中。

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

在这两种情况下,都必须进行排序以避免误报:最常见的Unix / Linux文件系统并不以稳定,可预测的顺序维护目录列表。您可能不会通过使用ls诸如此类等来实现这一点,后者会为您静默地对目录内容进行排序。find没有-ssort调用将以基础文件系统返回文件的顺序打印文件,如果作为输入提供给它的文件顺序发生更改,这将导致此命令给出更改的哈希值。

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

该方法效率不高,调用md5sumN + 1次,其中N是树中文件的数量,但这是避免哈希文件和目录元数据的必要成本。

选项2:比较数据元数据

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

$ tar -cf - somedir | md5sum

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

该方法相当快,因为​​它只对树进行一次传递,并且仅运行一次哈希程序。

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

如果您发现自己误报了,建议您find | cpio选择Gilles的答案中的选项。


7
我认为最好导航到要比较的目录并使用find .而不是find somedir。这样,在提供不同的路径规范时,文件名是相同的。这可能很棘手:-)
Abbafei

我们也应该对文件排序吗?
CMCDragonkai '16

@CMCDragonkai:你是什么意思?在第一种情况下,我们会对文件名列表进行排序。在第二种情况下,我们故意不这样做,因为第一句中强调的部分内容是目录中文件的顺序已更改,因此您不希望对任何内容进行排序。
沃伦·杨

@WarrenYoung您能否更彻底地解释为什么选项2并不总是更好?它似乎更快,更简单并且更跨平台。在哪种情况下不应该是选项1?
罗宾·温斯洛

选项1替代:find somedir -type f -exec sh -c "openssl dgst -sha1 -binary {} | xxd -p" \; | sort | openssl dgst -sha1忽略所有文件名(应使用换行符)
windm

38

校验和必须是文件的确定性和明确表示形式。确定性意味着,如果将相同的文件放在相同的位置,则会得到相同的结果。明确意味着两个不同的文件集具有不同的表示形式。

数据和元数据

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

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

好的,这有效,谢谢。但是有没有不包含任何元数据的方法吗?现在我只需要实际内容。

如何LC_ALL=C sort从不同的环境中进行检查...(+ 1 btw)
Peter.O 2012年

您为此编写了一个完整的Python程序?谢谢!这确实超出了我的预期。:-)无论如何,我将检查这些方法以及Warren的新选项1。

好答案。LC_ALL=C如果要在多台计算机和OS上运行,则设置排序顺序至关重要。
Davor Cubranic '16

什么cpio -o -意思 cpio默认不使用stdin / out吗?GNU cpio 2.12产生cpio: Too many arguments
2016年

12

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

递归操作-md5deep能够递归检查整个目录树。即,为目录中的每个文件以及每个子目录中的每个文件计算MD5。

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

...


很好,但是不能正常工作,它说.../foo: Is a directory,有什么用?
卡米洛·马丁

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

如何忽略路径中的某些文件/目录?
Sandeepan Nath

9

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

尝试这个:

diff -qr dir1 dir2

是的,这也很有用。我认为您在该命令中指的是dir1 dir2。

1
当可以避免使用GUI时,我通常不使用GUI,但是对于目录差异而言,kdiff3很棒,并且可以在许多平台上使用。
sinelaw

该命令也会报告不同的文件。
Serge Stroobandt 2014年

7

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

> md5deep -r -l . | sort | md5sum
d43417958e47758c6405b5098f151074 *-

md5deep是必需的。


1
而不是在ubuntu 16.04 上md5deep使用hashdeep,因为md5deep软件包只是hashdeep的过渡虚拟对象。
palik

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

3

文件内容,不包括文件名

我需要一个仅检查文件名的版本,因为内容位于不同的目录中。

这个版本(Warren Young的答案)很有帮助,但是我的md5sum输出版本是文件名(相对于我运行命令的路径),并且文件夹名称是不同的,因此即使单个文件的校验和匹配,最终的校验和也没有没错

为了解决这个问题,就我而言,我只需要从find输出的每一行剥离文件名(使用,仅选择第一个单词,用空格隔开cut):

find -s somedir -type f -exec md5sum {} \; | cut -d" " -f1 | md5sum

您可能还需要对校验和进行排序以获得可重复的列表。
eckes


3

nix-hash来自Nix包管理器

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

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


2

我将此代码段用于中等音量

find . -xdev -type f -print0 | LC_COLLATE=C sort -z | xargs -0 cat | md5sum -

这是XXXL的

find . -xdev -type f -print0 | LC_COLLATE=C sort -z | xargs -0 tail -qc100 | md5sum -


什么是-xdev标志吗?
czerasz

它要求您键入:man find并阅读该精美手册;)
poige

好点子 :-)。-xdev Don't descend directories on other filesystems.
czerasz

1
请注意,这将忽略新的空文件(例如,如果您触摸文件)。
罗恩·约翰

在许多情况下,这将产生具有完全不同的文件和目录结构的相同md5sum。如果重命名文件和目录不会改变文件的排序顺序,则根本不会改变。所以我不推荐这种方法。
汉斯·彼得·斯托尔

2

一个好的树校验和就是Git的tree-id。

不幸的是,没有可用的独立工具可以做到这一点(至少我不知道),但是如果您有Git方便的话,您就可以假装建立一个新的存储库并将想要检查的文件添加到索引中。

这使您可以生成(可重复的)树形哈希-仅包含内容,文件名和某些简化的文件模式(可执行)。


2

作为此出色答案的补充,如果您发现自己想加快大型目录的校验和的计算,请尝试使用GNU Parallel

find -s somedir -type f | parallel -k -n 100 md5 {} | md5

(这是使用带有的Mac md5,根据需要替换。)

-k标志很重要,它指示parallel维护顺序,否则即使文件都相同,总和也可以更改运行。-n 100表示要md5使用100个参数运行每个实例,这是您可以调整的参数,以获得最佳运行时间。另请参阅的-X标志parallel(尽管在我个人的情况下会导致错误。)


1

这个脚本经过了严格的测试,并支持许多操作,包括查找重复项,对数据和元数据进行比较,显示添加以及更改和删除,您可能会喜欢Fingerprint

指纹现在不会为目录生成单个校验和,而是一个脚本文件,其中包含该目录中所有文件的校验和。

fingerprint analyze

这将index.fingerprint在当前目录中生成,其中包括校验和,文件名和文件大小。默认情况下,它同时使用MD5SHA1.256

将来,我希望将对Merkle树的支持添加到Fingerprint中,这将为您提供单个顶级校验和。现在,您需要保留该文件以进行验证。


1

我既不想新的可执行文件,也不想笨拙的解决方案,所以这是我的看法:

#!/bin/sh
# md5dir.sh by Camilo Martin, 2014-10-01.
# Give this a parameter and it will calculate an md5 of the directory's contents.
# It only takes into account file contents and paths relative to the directory's root.
# This means that two dirs with different names and locations can hash equally.

if [[ ! -d "$1" ]]; then
    echo "Usage: md5dir.sh <dir_name>"
    exit
fi

d="$(tr '\\' / <<< "$1" | tr -s / | sed 's-/$--')"
c=$((${#d} + 35))
find "$d" -type f -exec md5sum {} \; | cut -c 1-33,$c- | sort | md5sum | cut -c 1-32

0

强大而干净的方法

  • 首先,不要浪费可用内存!散列文件而不是整个文件。
  • 针对不同需求/目的的不同方法(以下全部内容或选择适用的方法):
    • 仅散列目录树中所有条目的条目名称
    • 散列所有条目的文件内容(保留元数据,如inode编号,ctime,atime,mtime,size等,您便会明白)
    • 对于符号链接,其内容为引用名称。散列或选择跳过
    • 哈希条目内容时遵循或不遵循(解析名称)符号链接
    • 如果是目录,则其内容仅是目录条目。在递归遍历时,它们最终将被散列,但是是否应该对该级别的目录条目名称进行散列以标记该目录?在需要散列以快速识别更改而不必深入遍历以散列内容的用例中很有用。一个例子是文件的名称更改,但其余内容保持不变,并且都是相当大的文件
    • 妥善处理大文件(再次注意RAM)
    • 处理非常深的目录树(注意打开的文件描述符)
    • 处理非标准文件名
    • 如何处理套接字,管道/ FIFO,块设备,char设备等文件?还必须对它们进行哈希处理吗?
    • 遍历时不要更新任何条目的访问时间,因为这在某些用例中会产生副作用并且会适得其反(直观?)。

这就是我的头等大事,任何花了一些时间从事这一工作的人实际上都会抓到其他陷阱和死角。

这是一个工具(免责声明:我是它的贡献者)dtreetrawl,对内存的要求很低,可以解决大多数情况,可能有些麻烦,但是很有帮助。

Usage:
  dtreetrawl [OPTION...] "/trawl/me" [path2,...]

Help Options:
  -h, --help                Show help options

Application Options:
  -t, --terse               Produce a terse output; parsable.
  -d, --delim=:             Character or string delimiter/separator for terse output(default ':')
  -l, --max-level=N         Do not traverse tree beyond N level(s)
  --hash                    Hash the files to produce checksums(default is MD5).
  -c, --checksum=md5        Valid hashing algorithms: md5, sha1, sha256, sha512.
  -s, --hash-symlink        Include symbolic links' referent name while calculating the root checksum
  -R, --only-root-hash      Output only the root hash. Blank line if --hash is not set
  -N, --no-name-hash        Exclude path name while calculating the root checksum
  -F, --no-content-hash     Do not hash the contents of the file

人类友好输出示例:

...
... //clipped
...
/home/lab/linux-4.14-rc8/CREDITS
        Base name                    : CREDITS
        Level                        : 1
        Type                         : regular file
        Referent name                :
        File size                    : 98443 bytes
        I-node number                : 290850
        No. directory entries        : 0
        Permission (octal)           : 0644
        Link count                   : 1
        Ownership                    : UID=0, GID=0
        Preferred I/O block size     : 4096 bytes
        Blocks allocated             : 200
        Last status change           : Tue, 21 Nov 17 21:28:18 +0530
        Last file access             : Thu, 28 Dec 17 00:53:27 +0530
        Last file modification       : Tue, 21 Nov 17 21:28:18 +0530
        Hash                         : 9f0312d130016d103aa5fc9d16a2437e

Stats for /home/lab/linux-4.14-rc8:
        Elapsed time     : 1.305767 s
        Start time       : Sun, 07 Jan 18 03:42:39 +0530
        Root hash        : 434e93111ad6f9335bb4954bc8f4eca4
        Hash type        : md5
        Depth            : 8
        Total,
                size           : 66850916 bytes
                entries        : 12484
                directories    : 763
                regular files  : 11715
                symlinks       : 6
                block devices  : 0
                char devices   : 0
                sockets        : 0
                FIFOs/pipes    : 0

总是欢迎提供一般性建议,但是最好的答案是具体的,并在适当的地方提供代码。如果您有使用该工具的经验,请把它包括在内。
bu5hman

@ bu5hman当然!自从我参与了它的开发以来,我还不太满意地说(高兴吗?)更多关于它的工作情况。
六ķ

0

单独处理每个目录中的所有文件。

# Calculating
find dir1 | xargs md5sum > dir1.md5
find dir2 | xargs md5sum > dir2.md5
# Comparing (and showing the difference)
paste <(sort -k2 dir1.md5) <(sort -k2 dir2.md5) | awk '$1 != $3'

0

迁移到POSIX存档格式会影响基于GNU Tar的校验和

这个答案是对使用Tar输出对目录的内容进行哈希处理的方法的补充更新,这是沃伦·扬吉尔斯在一段时间前的出色回答中提出的。

从那时起,至少openSUSE(从其版本12.2开始)将其默认GNU Tar格式从“ GNU tar 1.13.x格式”更改为(略)高级的“ POSIX 1003.1-2001(pax)格式”。也上游(GNU焦油的开发者),他们讨论来执行相同的迁移,例如参见上最后一个段落当前页的的GNU焦油手册

GNU tar的默认格式是在编译时定义的。您可以通过运行tar --help并检查其输出的最后几行来对其进行检查。通常,将GNU tar配置为以gnu格式创建档案,但是,将来的版本将切换到posix

(此页面还对GNU Tar可用的不同存档格式进行了很好的回顾。)

在我们的示例中,如果我们将目录的内容作为tar并散列结果,并且不采取特定措施,那么从GNU更改为POSIX格式将产生以下结果:

  • 尽管目录内容相同,但生成的校验和将有所不同。

  • 尽管目录内容相同,但如果使用默认的pax标头,则每次运行时产生的校验和将有所不同。

后者来自以下事实:POSIX(pax)格式包括扩展的pax标头,这些标头由默认为%d/PaxHeaders.%p/%fGNU Tar中的格式字符串确定。在该字符串中,说明符%p被生成的Tar进程的进程ID替换,当然,这在运行之间是不同的。有关详细信息,请参见GNU Tar手册的这一部分,尤其是本部分

刚刚从2019-03-28开始,上游接受了一项提交,以缓解此问题。

因此,为了能够在给定的用例中继续使用GNU Tar,我可以推荐以下替代选项:

  • 使用Tar选项--format=gnu明确指示Tar以“旧”格式生成档案。这对于验证“旧”校验和是必需的。

  • 使用更新的POSIX格式,但显式指定合适的pax标头,例如通过--pax-option="exthdr.name=%d/PaxHeaders/%f"。但是,这破坏了与“旧”校验和的向后兼容性。

这是一个Bash代码片段,我经常使用它来计算目录内容(包括元数据)的校验和:

( export LC_ALL=C
  find <paths> ! -type s -print0 |
  sort -z |
  tar cp --format=gnu --numeric-owner \
         --atime-preserve \
         --no-recursion --null --files-from - |
  md5sum --binary; )

在此,<paths>用校验和覆盖的所有目录的路径的列表以空格分隔。在其他答案中已经充分讨论了使用C语言环境,文件名的空字节分隔以及使用查找和排序来获取档案中文件的文件系统独立顺序的目的。

周围的括号将LC_ALL设置保留在子外壳中。

此外,我使用表达式! -type swith find来避免如果套接字文件是目录内容的一部分而引起Tar发出的警告:GNU Tar不会存档套接字。如果您希望收到有关套接字跳过的通知,请不要使用该表达式。

--numeric-owner与Tar一起使用,以便以后甚至在并非所有文件所有者都知道的系统上也可以校验校验和。

--atime-preserve如果<paths>在只读安装的设备上有任何谎言,则最好省略Tar 的选项。否则,将警告您访问时间戳记Tar无法还原的每个文件。对于启用写<paths>,我很好地使用此选项将访问时间戳保留在哈希目录中。

Tar选项--no-recursion已经在Gilles提案中使用,可以防止Tar递归地下降到目录中,而是对从排序find输出中得到的任何内容逐个文件地进行操作。

最后,我使用的不是真的md5sum:我实际上使用sha256sum


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.