как создать архив, в котором сохраняется один и тот же хэш md5 для идентичного контента в python?

Как объясняется в этой статье https://medium.com/@mpreziuso/is-gzip-deterministic-26c81bfd0a49 md5 двух файлов .tar.gz, которые представляют собой сжатие одного и того же набора файлов, могут быть разными. Это связано с тем, что он, например, включает отметку времени в заголовке сжатого файла.

В статье предложено 3 решения, и в идеале я хотел бы использовать первое из них:

Мы можем использовать флаг -n в gzip, который заставит gzip опустить метку времени и имя файла из заголовка файла;

И это решение работает хорошо:

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

Тем не менее, я понятия не имею, что будет хорошим способом сделать это на питоне. Есть ли способ сделать это с помощью tarfile(https://docs.python.org/2/library/tarfile.html)?


person Sophie Jacquin    schedule 11.07.2017    source источник
comment
Есть ли какая-то причина, по которой вы не можете запустить команды, которые вы только что написали, как внешний процесс? os.system("tar -c ./bin |gzip -n >one.tar.gz")   -  person Martin Drozdik    schedule 11.07.2017
comment
Что не так с использованием явного аргумента mtime для gzip.GzipFile()?   -  person Ignacio Vazquez-Abrams    schedule 11.07.2017


Ответы (4)


В качестве обходного пути вы можете использовать сжатие 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
person Martin Drozdik    schedule 11.07.2017

Ответ Мартина правильный, но в моем случае я также хотел игнорировать дату последнего изменения каждого файла в tar, так что даже если файл был «изменен», но без реальных изменений, он все равно имеет тот же хэш.

При создании 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 https://stackoverflow.com/questions/34688392/paxheaders-in-tarball
    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

Вы можете настроить, какие параметры и как будут изменены в вашей функции фильтра в зависимости от вашего варианта использования.

person Patrick Fay    schedule 15.09.2019
comment
Боюсь, это не сработало для меня. Я использую сжатие bz2 и атрибуты tarinfo с жестким кодированием точно в соответствии с ответом. Я добавляю в tar-файл несколько файлов, а не один, поэтому я предполагаю, что это может быть проблема с сортировкой (то есть порядком, в котором я добавляю файлы). - person Sean McCarthy; 27.05.2021

Мне нужно было заархивировать много файлов в один файл 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
person Sean McCarthy    schedule 27.05.2021

Конечно, вы можете удалить даты и другую информацию, не относящуюся к файлам, в заголовках tar и gzip и использовать ту же версию того же компрессора с теми же настройками, чтобы получить точно такие же байты архива.

Однако все это наводит меня на мысль, что вы решаете не ту проблему, и что вы столкнетесь с проблемами, если кто-то изменит версию компрессора под вас, с сигнатурами, не совпадающими до и после изменения версии.

Вместо этого я бы порекомендовал вам создавать свои подписи, используя объединение несжатого файла content. Тогда ваша подпись будет естественно независима от всех вещей, которые вам в настоящее время приходится делать для обнуления, а также будет независима от изменений в коде сжатия. Тогда все, что вам нужно сделать, это позаботиться о сохранении порядка файлов в архиве.

person Mark Adler    schedule 27.05.2021