Linux: 为给定的文件夹和内容计算一个散列?

肯定有什么方法可以轻而易举地做到这一点!

我试过 Linux 命令行应用程序,比如 sha1summd5sum,但它们似乎只能计算单个文件的哈希值,并输出一个哈希值列表,每个文件一个哈希值。

我需要为文件夹的整个内容(而不仅仅是文件名)生成一个散列。

我想做一些像

sha1sum /folder/of/stuff > singlehashvalue

编辑: 澄清一下,我的文件位于目录树的多个级别,它们并不都位于同一个根文件夹中。

91028 次浏览

如果您只想散列文件的内容,忽略文件名,那么您可以使用

cat $FILES | md5sum

在计算散列时,请确保文件的顺序相同:

cat $(echo $FILES | sort) | md5sum

但是不能在文件列表中包含目录。

试着分两步做:

  1. 为文件夹中的所有文件创建带哈希的文件
  2. 散列这个文件

像这样:

# for FILE in `find /folder/of/stuff -type f | sort`; do sha1sum $FILE >> hashes; done
# sha1sum hashes

或者一次性完成:

# cat `find /folder/of/stuff -type f | sort` | sha1sum

您可以使用 sha1sum生成散列值列表,然后再使用 sha1sum生成该列表,这取决于您想要完成的具体内容。

我将通过 sort将单个文件的结果通过管道传送到 md5sumsha1sum,无论您选择哪个。

一种可能的方法是:

Sha1sum 路径/to/file/* | sha1sum

如果有一个完整的目录树,那么最好使用 find 和 xargs

find path/to/folder -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum

最后,如果您还需要考虑权限和空目录:

(find path/to/folder -type f -print0  | sort -z | xargs -0 sha1sum;
find path/to/folder \( -type f -o -type d \) -print0 | sort -z | \
xargs -0 stat -c '%n %a') \
| sha1sum

stat的参数将导致它打印文件的名称,然后打印其八进制权限。这两个查找将一个接一个地运行,导致磁盘 IO 数量加倍,第一个查找所有文件名并对内容进行校验和,第二个查找所有文件和目录名,打印名称和模式。“文件名和校验和”列表,以及后面的“具有权限的名称和目录”将进行校验和,以获得较小的校验和。

  • 使用像 助手这样的文件系统入侵检测工具。

  • 散列目录中的一个焦油球:

    tar cvf - /path/to/folder | sha1sum

  • 自己编写代码,比如 Vatine 的画笔:

    find /path/to/folder -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum

你可以做 tar -c /path/to/folder | sha1sum

有一个 Python 脚本可以解决这个问题:

Http://code.activestate.com/recipes/576973-getting-the-sha-1-or-md5-hash-of-a-directory/

如果你更改了文件的名称而没有更改它们的字母顺序,散列脚本将不会检测到它。但是,如果您更改了文件的顺序或任何文件的内容,那么运行该脚本将产生与以前不同的散列。

实现这一目标的另一个工具是:

Http://md5deep.sourceforge.net/

正如听起来一样: 像 md5sum 但也是递归的,加上其他特性。

md5deep -r {direcotory}

我为此编写了一个 Groovy 脚本:

import java.security.MessageDigest


public static String generateDigest(File file, String digest, int paddedLength){
MessageDigest md = MessageDigest.getInstance(digest)
md.reset()
def files = []
def directories = []


if(file.isDirectory()){
file.eachFileRecurse(){sf ->
if(sf.isFile()){
files.add(sf)
}
else{
directories.add(file.toURI().relativize(sf.toURI()).toString())
}
}
}
else if(file.isFile()){
files.add(file)
}


files.sort({a, b -> return a.getAbsolutePath() <=> b.getAbsolutePath()})
directories.sort()


files.each(){f ->
println file.toURI().relativize(f.toURI()).toString()
f.withInputStream(){is ->
byte[] buffer = new byte[8192]
int read = 0
while((read = is.read(buffer)) > 0){
md.update(buffer, 0, read)
}
}
}


directories.each(){d ->
println d
md.update(d.getBytes())
}


byte[] digestBytes = md.digest()
BigInteger bigInt = new BigInteger(1, digestBytes)
return bigInt.toString(16).padLeft(paddedLength, '0')
}


println "\n${generateDigest(new File(args[0]), 'SHA-256', 64)}"

您可以自定义用法,以避免打印每个文件、更改消息摘要、删除目录散列等。我已经用 NIST 的测试数据对它进行了测试,结果和预期的一样。http://www.nsrl.nist.gov/testdata/

gary-macbook:Scripts garypaduana$ groovy dirHash.groovy /Users/garypaduana/.config
.DS_Store
configstore/bower-github.yml
configstore/insight-bower.json
configstore/update-notifier-bower.json
filezilla/filezilla.xml
filezilla/layout.xml
filezilla/lockfile
filezilla/queue.sqlite3
filezilla/recentservers.xml
filezilla/sitemanager.xml
gtk-2.0/gtkfilechooser.ini
a/
configstore/
filezilla/
gtk-2.0/
lftp/
menus/
menus/applications-merged/


79de5e583734ca40ff651a3d9a54d106b52e94f1f8c2cd7133ca3bbddc0c6758

如果你只是想检查文件夹中的内容是否发生了变化,我推荐这个:

ls -alR --full-time /folder/of/stuff | sha1sum

它只会给你 ls 输出的散列,包含文件夹、子文件夹、它们的文件、它们的时间戳、大小和权限。几乎所有的东西,你需要确定,如果有什么改变。

请注意,此命令不会为每个文件生成散列,但这就是为什么它应该比使用 find 更快的原因。

一个稳健而干净的方法

  • 首先,不要占用可用的内存! 对文件进行散列,而不是填充整个文件。
  • 针对不同需求/目的的不同方法(以下所有方法或者选择适用的方法) :
    • 只散列目录树中所有条目的条目名称
    • 散列所有条目的文件内容(保留元数据类型、 inode 编号、 ctime、 time、 mtime、 size 等,您就明白了)
    • 对于一个符号链接,它的内容是指向的名称。散列它或者选择跳过
    • 在散列条目的内容时,是否遵循(已解析的名称)符号链接
    • 如果它是一个目录,它的内容只是目录条目。在递归遍历时,它们最终将被散列,但是是否应该对该级别的目录条目名称进行散列以标记该目录?在需要散列来快速识别更改而不必深入遍历以散列内容的用例中非常有用。一个例子就是文件名的改变,但是其余的内容保持不变,而且它们都是相当大的文件
    • 很好地处理大文件(同样要注意 RAM)
    • 处理非常深的目录树(注意打开的文件描述符)
    • 处理非标准文件名
    • 如何进行文件是套接字,管道/FIFO,块设备,字符设备? 必须散列他们以及?
    • 遍历时不要更新任何条目的访问时间,因为这将是一个副作用和适得其反(直观?)对于某些用例。

这是我头顶上的东西,任何一个花了一些时间在这上面的人实际上已经抓住了其他的陷阱和角落案件。

这里有一个工具 ,内存非常少,可以处理大多数情况,可能有点粗糙,但是非常有用。

dtreetrawl的使用和输出示例。

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


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


Application Options:
-t, --terse               Produce a terse output; parsable.
-j, --json                Output as JSON
-d, --delim=:             Character or string delimiter/separator for terse output(default ':')
-l, --max-level=N         Do not traverse tree beyond N level(s)
--hash                    Enable hashing(default is MD5).
-c, --checksum=md5        Valid hashing algorithms: md5, sha1, sha256, sha512.
-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
-s, --hash-symlink        Include symbolic links' referent name while calculating the root checksum
-e, --hash-dirent         Include hash of directory entries while calculating root checksum

一个人类友好输出的片段:

...
... //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

我必须检查整个目录中的文件更改。

但不包括时间戳、目录所有权。

目标是得到一个相同的总和在任何地方,如果文件是相同的。

包括托管到其他机器,不管任何东西,但文件,或改变到他们。

md5sum * | md5sum | cut -d' ' -f1

它按文件生成一个哈希列表,然后将这些哈希连接为一个哈希列表。

这比 tar 方法快多了。

对于散列中的 更强的隐私,我们可以在相同的配方中使用 Sha512sum

sha512sum * | sha512sum | cut -d' ' -f1

散列在任何地方使用 Sha512sum都是相同的,但是没有已知的方法来反转它。

下面是 Python 3中一个简单、简短的变体,适用于小型文件(比如源代码树或者其他什么东西,每个文件都可以很容易地放入 RAM 中) ,忽略空目录,基于其他解决方案的想法:

import os, hashlib


def hash_for_directory(path, hashfunc=hashlib.sha1):
filenames = sorted(os.path.join(dp, fn) for dp, _, fns in os.walk(path) for fn in fns)
index = '\n'.join('{}={}'.format(os.path.relpath(fn, path), hashfunc(open(fn, 'rb').read()).hexdigest()) for fn in filenames)
return hashfunc(index.encode('utf-8')).hexdigest()

工作原理是这样的:

  1. 递归地查找目录中的所有文件并按名称对它们进行排序
  2. 计算每个文件的 hash (默认值: SHA-1)(将整个文件读入内存)
  3. 使用“ filename = hash”行创建一个文本索引
  4. 将该索引重新编码为 UTF-8字节字符串并对其进行哈希

如果 SHA-1不是你的菜,你可以把 不同的散列函数作为第二个参数。

如果这是一个 git 回购,你想忽略 .gitignore中的任何文件,你可能想使用这个:

git ls-files <your_directory> | xargs sha256sum | cut -d" " -f1 | sha256sum | cut -d" " -f1

这对我很有效。

到目前为止,最快的方法仍然是使用 tar。并且通过一些额外的参数,我们还可以消除元数据造成的差异。

要使用 tar 对目录进行散列,需要在 tar 期间使用 一定要把路径分类,否则它总是不同的。

tar -C <root-dir> -cf - --sort=name <dir> | sha256sum

无视时间

如果您不关心访问时间或修改时间也使用类似 --mtime='UTC 2019-01-01' 的东西,以确保所有的时间戳是相同的。

无视所有权

通常我们需要添加 --group=0 --owner=0 --numeric-owner来统一所有者元数据。

忽略一些文件

使用 --exclude=PATTERN

Kvantour 的回答添加多处理和进度条

大约快30倍(取决于 CPU)

100%|██████████████████████████████████| 31378/31378 [03:03<00:00, 171.43file/s]
# to hash without permissions
find . -type f -print0 | sort -z | xargs -P $(nproc --all) -0 sha1sum | tqdm --unit file --total $(find . -type f | wc -l) | sort | awk '{ print $1 }' | sha1sum
# to hash permissions
(find . -type f -print0  | sort -z | xargs -P $(nproc --all) -0 sha1sum | sort | awk '{ print $1 }';
find . \( -type f -o -type d \) -print0 | sort -z | xargs -P $(nproc --all) -0 stat -c '%n %a') | \
sort | sha1sum | awk '{ print $1 }'

确保安装了 TQDMpip install tqdm或检查文档

awk将删除文件路径,这样如果父目录或路径不同,就不会影响散列

您可以尝试使用 Hashdir,它是为此目的编写的开放源码命令行工具。

hashdir /folder/of/stuff

它有几个有用的标志,允许您指定哈希算法,打印所有子哈希,以及保存和验证哈希。

hashdir:
A command-line utility to checksum directories and files.


Usage:
hashdir [options] [<item>...] [command]


Arguments:
<item>    Directory or file to hash/check


Options:
-t, --tree                                         Print directory tree
-s, --save                                         Save the checksum to a file
-i, --include-hidden-files                         Include hidden files
-e, --skip-empty-dir                               Skip empty directories
-a, --algorithm <md5|sha1|sha256|sha384|sha512>    The hash function to use [default: sha1]
--version                                          Show version information
-?, -h, --help                                     Show help and usage information


Commands:
check <item>    Verify that the specified hash file is valid.

快速摘要: 如何散列整个文件夹的内容,或比较两个文件夹是否相等

# 1. How to get a sha256 hash over all file contents in a folder, including
# hashing over the relative file paths within that folder to check the
# filenames themselves (get this bash function below).
sha256sum_dir "path/to/folder"


# 2. How to quickly compare two folders (get the `diff_dir` bash function below)
diff_dir "path/to/folder1" "path/to/folder2"
# OR:
diff -r -q "path/to/folder1" "path/to/folder2"

“一句话”

这样做 而不是主要答案,以获得一个单一的散列所有 非目录文件内容在一个完整的文件夹,无论该文件夹位于:

这是一个“一行”命令。复制并粘贴整个程序,以便一次运行所有程序:

# This one works, but don't use it, because its hash output does NOT
# match that of my `sha256sum_dir` function. I recommend you use
# the "1-liner" just below, therefore, instead.


time ( \
starting_dir="$(pwd)" \
&& target_dir="path/to/folder" \
&& cd "$target_dir" \
&& find . -not -type d -print0 | sort -zV \
| xargs -0 sha256sum | sha256sum; \
cd "$starting_dir"
)

但是,它产生的散列值与我的 sha256sum_dir bash 函数略有不同,我将在下面介绍该函数。因此,要使输出散列与 sha256sum_dir函数的输出匹配,改为:

# Use this one, as its output matches that of my `sha256sum_dir`
# function exactly.


all_hashes_str="$( \
starting_dir="$(pwd)" \
&& target_dir="path/to/folder" \
&& cd "$target_dir" \
&& find . -not -type d -print0 | sort -zV | xargs -0 sha256sum \
)"; \
cd "$starting_dir"; \
printf "%s" "$all_hashes_str" | sha256sum

有关为什么主要答案不为不同位置的相同文件夹产生相同散列的更多信息,请参见下面的进一步说明。

[我的首选方法]下面是我编写的一些 bash 函数: sha256sum_dirdiff_dir

~/.bashrc文件或 ~/.bash_aliases文件中放置以下函数,假设您的 ~/.bashrc文件的 ~/.bash_aliases文件源如下所示:

if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi

你可以在我个人的 ERCaGuy _ dotfiles回购文件中找到以下两个函数。

下面是 sha256sum_dir函数,它获得目录中所有文件的总“目录”散列:

# Take the sha256sum of all files in an entire dir, and then sha256sum that
# entire output to obtain a _single_ sha256sum which represents the _entire_
# dir.
# See:
# 1. [my answer] https://stackoverflow.com/a/72070772/4561887
sha256sum_dir() {
return_code="$RETURN_CODE_SUCCESS"
if [ "$#" -eq 0 ]; then
echo "ERROR: too few arguments."
return_code="$RETURN_CODE_ERROR"
fi
# Print help string if requested
if [ "$#" -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
# Help string
echo "Obtain a sha256sum of all files in a directory."
echo "Usage:  ${FUNCNAME[0]} [-h|--help] <dir>"
return "$return_code"
fi


starting_dir="$(pwd)"
target_dir="$1"
cd "$target_dir"


# See my answer: https://stackoverflow.com/a/72070772/4561887
filenames="$(find . -not -type d | sort -V)"
IFS=$'\n' read -r -d '' -a filenames_array <<< "$filenames"
time all_hashes_str="$(sha256sum "${filenames_array[@]}")"
cd "$starting_dir"


echo ""
echo "Note: you may now call:"
echo "1. 'printf \"%s\n\" \"\$all_hashes_str\"' to view the individual" \
"hashes of each file in the dir. Or:"
echo "2. 'printf \"%s\" \"\$all_hashes_str\" | sha256sum' to see that" \
"the hash of that output is what we are using as the final hash" \
"for the entire dir."
echo ""
printf "%s" "$all_hashes_str" | sha256sum | awk '{ print $1 }'
return "$?"
}
# Note: I prefix this with my initials to find my custom functions easier
alias gs_sha256sum_dir="sha256sum_dir"

假设您只是想比较两个目录是否相等,那么可以使用 diff -r -q "dir1" "dir2",我将它包装在这个 diff_dir命令中。我在这里学习了用于比较整个文件夹的 diff命令: 如何检查两个文件夹在 linux 中是否相同

# Compare dir1 against dir2 to see if they are equal or if they differ.
# See:
# 1. How to `diff` two dirs: https://stackoverflow.com/a/16404554/4561887
diff_dir() {
return_code="$RETURN_CODE_SUCCESS"
if [ "$#" -eq 0 ]; then
echo "ERROR: too few arguments."
return_code="$RETURN_CODE_ERROR"
fi
# Print help string if requested
if [ "$#" -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
echo "Compare (diff) two directories to see if dir1 contains the same" \
"content as dir2."
echo "NB: the output will be **empty** if both directories match!"
echo "Usage:  ${FUNCNAME[0]} [-h|--help] <dir1> <dir2>"
return "$return_code"
fi


dir1="$1"
dir2="$2"
time diff -r -q "$dir1" "$dir2"
return_code="$?"
if [ "$return_code" -eq 0 ]; then
echo -e "\nDirectories match!"
fi


# echo "$return_code"
return "$return_code"
}
# Note: I prefix this with my initials to find my custom functions easier
alias gs_diff_dir="diff_dir"

下面是我的 ~/temp2目录中的 sha256sum_dir命令的输出(我在下面描述了这个目录,以便您可以重现它并自己测试它)。在这个示例中,您可以看到总文件夹散列为 b86c66bcf2b033f65451e8c225425f315e618be961351992b7c7681c3822f6a3:

$ gs_sha256sum_dir ~/temp2


real    0m0.007s
user    0m0.000s
sys 0m0.007s


Note: you may now call:
1. 'printf "%s\n" "$all_hashes_str"' to view the individual hashes of each
file in the dir. Or:
2. 'printf "%s" "$all_hashes_str" | sha256sum' to see that the hash of that
output is what we are using as the final hash for the entire dir.


b86c66bcf2b033f65451e8c225425f315e618be961351992b7c7681c3822f6a3

下面是 cmd 和 diff_dir的输出,用于比较两个 dir 是否相等。这是检查复制整个目录到我的 SD 卡刚才正常工作。每当出现这种情况时,我都将输出指示为 Directories match!!:

$ gs_diff_dir "path/to/sd/card/tempdir" "/home/gabriel/tempdir"


real    0m0.113s
user    0m0.037s
sys 0m0.077s


Directories match!

为什么 主要答案不为不同位置的相同文件夹生成相同的散列

我试过 最受欢迎的答案,但是它不能正常工作。需要调整一下。它不能很好地工作,因为散列根据感兴趣的文件夹的基本路径进行更改!这意味着即使两个文件夹是 完美匹配并且包含 完全一样的内容!某个文件夹的相同副本的文件夹也比它复制的文件夹的文件夹的文件夹的文件夹的文件夹的文件夹的文件夹文件夹的文件夹文件夹的文件夹文件夹文件夹的文件夹文件夹文件夹的文件夹文件夹文件夹文件夹的文件夹文件夹文件夹!让我解释一下:

假设我在 ~/temp2有一个名为 temp2的文件夹。它包含 file1.txtfile2.txtfile3.txtfile1.txt包含字母 a后面跟着一个返回,file2.txt包含字母 b后面跟着一个返回,file3.txt包含字母 ~/temp20后面跟着一个返回。

如果我运行 find /home/gabriel/temp2,我得到:

$ find /home/gabriel/temp2
/home/gabriel/temp2
/home/gabriel/temp2/file3.txt
/home/gabriel/temp2/file1.txt
/home/gabriel/temp2/file2.txt

如果我将它转发给 sha256sum(代替 sha1sum) ,其模式与主要答案状态相同,就会得到这个结果。注意,每个散列后面都有一个 全路径,也就是我们想要的 没有:

$ find /home/gabriel/temp2 -type f -print0 | sort -z | xargs -0 sha256sum
87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7  /home/gabriel/temp2/file1.txt
0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f  /home/gabriel/temp2/file2.txt
a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478  /home/gabriel/temp2/file3.txt

如果您然后管道输出字符串以上的 sha256sum再次,它散列文件的 完整的文件路径,这是我们想要的 没有!文件散列可能在一个文件夹和该文件夹 没错的一个副本中匹配,但是在 绝对路径不完全匹配中匹配,因此它们将产生不同的最终散列,因为我们正在将整个文件路径作为我们的单个最终散列的一部分进行散列!

相反,我们需要的是每个散列旁边的 亲戚文件路径。为此,您必须首先将 cd放入感兴趣的文件夹,然后 那么对其中的所有文件运行 hash 命令,如下所示:

cd "/home/gabriel/temp2" && find . -type f -print0 | sort -z | xargs -0 sha256sum

现在,我得到了这个。注意,现在文件路径都是 亲戚,这就是我想要的! :

$ cd "/home/gabriel/temp2" && find . -type f -print0 | sort -z | xargs -0 sha256sum
87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7  ./file1.txt
0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f  ./file2.txt
a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478  ./file3.txt

很好。现在,如果我散列整个输出字符串,因为文件路径都是相对的,最终散列将匹配文件夹及其副本的 没错!通过这种方式,我们对文件 内容 还有中感兴趣的目录中的文件 名字进行散列,以获得一个给定文件夹的不同散列,如果文件内容不同 或者文件名不同,或者都不同。