如何创建存档,其对 python 中的相同内容保持相同的 md5 散列?

how to create archive whose keep same md5 hash for identical content in python?

如本文所述https://medium.com/@mpreziuso/is-gzip-deterministic-26c81bfd0a49 两个 .tar.gz 文件的 md5 是完全相同的一组文件的压缩文件,它们可能不同。这是因为,例如,它在压缩文件的 header 中包含时间戳。

文章中提出了 3 种解决方案,我最好使用第一种:

We can use the -n flag in gzip which will make gzip omit the timestamp and the file name from the file header;

这个解决方案效果很好:

tar -c ./bin |gzip -n >one.tar.gz
tar -c ./bin |gzip -n >two.tar.gz
md5sum one.tgz two.tgz

尽管如此,我不知道在 python 中做这件事的好方法是什么。 有没有办法用 tarfile(https://docs.python.org/2/library/tarfile.html)?

作为解决方法,您可以改用 bzip2 压缩。好像没有这个问题:

import tarfile

tar1 = tarfile.open("one.tar.bz2", "w:bz2")
tar1.add("bin")
tar1.close()

tar2 = tarfile.open("two.tar.bz2", "w:bz2")
tar2.add("bin")
tar2.close()

运行 md5 给出:

martin@martin-UX305UA:~/test$ md5sum one.tar.bz2 two.tar.bz2 
e9ec2fd4fbdfae465d43b2f5ecaecd2f  one.tar.bz2
e9ec2fd4fbdfae465d43b2f5ecaecd2f  two.tar.bz2

是正确的,但就我而言,我也想忽略 tar 中每个文件的最后修改日期,这样即使文件是 "modified"但没有实际更改,它仍然具有相同的哈希值。

创建 tar 时,我可以覆盖我不关心的值,因此它们始终相同。

在这个例子中,我展示了仅使用正常的 tar.bz2,如果我使用新的创建时间戳重新创建源文件,散列将改变(1 和 2 相同,之后重新创建,4 会有所不同)。但是,如果我将时间设置为 Unix Epoch 0(或任何其他任意时间),我的文件将全部哈希相同(3、5 和 6)

为此,您需要将 filter 函数传递给 tar.add(DIR, filter=tarInfoStripFileAttrs) 以删除所需的字段,如下例所示

import tarfile, time, os

def createTestFile():
    with open(DIR + "/someFile.txt", "w") as file:
        file.write("test file")

# Takes in a TarInfo and returns the modified TarInfo:
# https://docs.python.org/3/library/tarfile.html#tarinfo-objects
# intented to be passed as a filter to tarfile.add
# https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.add
def tarInfoStripFileAttrs(tarInfo):
    # set time to epoch timestamp 0, aka 00:00:00 UTC on 1 January 1970
    # note that when extracting this tarfile, this time will be shown as the modified date
    tarInfo.mtime = 0

    # file permissions, probably don't want to remove this, but for some use cases you could
    # tarInfo.mode = 0

    # user/group info
    tarInfo.uid= 0
    tarInfo.uname = ''
    tarInfo.gid= 0
    tarInfo.gname = ''

    # stripping paxheaders may not be required
    # see 
    tarInfo.pax_headers = {}

    return tarInfo


# COMPRESSION_TYPE = "gz" # does not work even with filter
COMPRESSION_TYPE = "bz2"
DIR = "toTar"
if not os.path.exists(DIR):
    os.mkdir(DIR)

createTestFile()

tar1 = tarfile.open("one.tar." + COMPRESSION_TYPE, "w:" + COMPRESSION_TYPE)
tar1.add(DIR)
tar1.close()

tar2 = tarfile.open("two.tar." + COMPRESSION_TYPE, "w:" + COMPRESSION_TYPE)
tar2.add(DIR)
tar2.close()

tar3 = tarfile.open("three.tar." + COMPRESSION_TYPE, "w:" + COMPRESSION_TYPE)
tar3.add(DIR, filter=tarInfoStripFileAttrs)
tar3.close()

# Overwrite the file with the same content, but an updated time
time.sleep(1)
createTestFile()

tar4 = tarfile.open("four.tar." + COMPRESSION_TYPE, "w:" + COMPRESSION_TYPE)
tar4.add(DIR)
tar4.close()


tar5 = tarfile.open("five.tar." + COMPRESSION_TYPE, "w:" + COMPRESSION_TYPE)
tar5.add(DIR, filter=tarInfoStripFileAttrs)
tar5.close()

tar6 = tarfile.open("six.tar." + COMPRESSION_TYPE, "w:" + COMPRESSION_TYPE)
tar6.add(DIR, filter=tarInfoStripFileAttrs)
tar6.close()
$ md5sum one.tar.bz2 two.tar.bz2 three.tar.bz2 four.tar.bz2 five.tar.bz2 six.tar.bz2
0e51c97a8810e45b78baeb1677c3f946  one.tar.bz2      # same as 2
0e51c97a8810e45b78baeb1677c3f946  two.tar.bz2      # same as 1
54a38d35d48d4aa1bd68e12cf7aee511  three.tar.bz2    # same as 5/6
22cf1161897377eefaa5ba89e3fa6acd  four.tar.bz2     # would be same as 1/2, but timestamp has changed
54a38d35d48d4aa1bd68e12cf7aee511  five.tar.bz2     # same as 3, even though timestamp has changed
54a38d35d48d4aa1bd68e12cf7aee511  six.tar.bz2      # same as 3, even though timestamp has changed

您可能希望根据您的用例调整修改哪些参数以及在过滤器函数中的修改方式。

我需要将许多文件存档在一个 tar 文件中(不只是一个),以上答案对我不起作用。相反,我将 Linux tar 命令与 Python 的 subprocess 模块一起使用:

import subprocess
import shlex 

def make_tarfile_linux(folder_path, filename):
    """
    Make idempotent tarfile for an identical checksum each time.
    However, this method does not filter out unwanted files like Python can...
    """
    tarfile_to_create_path_and_filename = f"/home/user/{filename}"
    tar_command = "tar --sort=name --owner=root:0 --group=root:0 --mtime='UTC 1970-01-01' -cjf"
    command_list = shlex.split(f"{tar_command} {tarfile_to_create_path_and_filename} {folder_path}")
    cp = subprocess.run(command_list)

    return None

当然,您可以删除 tar 和 gzip headers 中的日期和其他 non-file 信息,并使用具有相同设置的相同压缩器的相同版本,全部在为了获得完全相同的存档字节。

然而,所有这一切让我认为你正在解决错误的问题,如果有人更改你下面的压缩器版本,你将 运行 陷入问题,签名与之前和之前不匹配版本变更后

我建议您使用 未压缩 文件 内容 的串联来生成您的签名。然后你的签名将自然地独立于你当前必须竭尽全力归零的所有事情,并且也将独立于压缩代码的变化。然后您需要做的就是注意保持存档中文件的顺序。