在不写入磁盘的情况下提取 lzma 压缩 tar 存档成员

Extract lzma compressed tar archive members without writing to disk

我有一个嵌套的 tar 文件 2 tar 深。最外面的 tar 是 gpg 加密的,没有压缩。里面的tar是lzma。使用磁盘最里面的 tar 我没有任何问题。将最里面的 tar.xz 文件直接传递给 with tarfile.open() as get_lzma 是可行的。该行后面的代码执行时没有错误。我可以提取 tar 成员和 json.load() 数据。

这是一个小文件,数据很敏感。当我使用它时,它必须位于磁盘上,所以我不想解密它并将最里面的 tar 提取到磁盘。所以我想访问内存中的成员。我可以解密到 gpg 文件和 for member in get_lzma.getmembers(): returns 我期望的 tarinfo 对象,所以该成员似乎在那里,我只是不能用它做任何事情。当我 运行 extractfile() 我不能 .read() 结果是 returns <ExFileObject name=None>

在这一点上,我只是好奇为什么这不起作用。

如果文件结构不清楚,这就是它在磁盘上的位置:

file.tar.gpg <- is a tar file
 file.tar.xz <- is a compressed tar file
   member1
   memberN
   json.load(file_o)
  File "/home/user/.pyenv/versions/3.8.2/lib/python3.8/json/__init__.py", line 293, in load
    return loads(fp.read(),
  File "/home/user/.pyenv/versions/3.8.2/lib/python3.8/tarfile.py", line 681, in read
    self.fileobj.seek(offset + (self.position - start))
  File "/home/user/.pyenv/versions/3.8.2/lib/python3.8/lzma.py", line 253, in seek
    return self._buffer.seek(offset, whence)
  File "/home/user/.pyenv/versions/3.8.2/lib/python3.8/_compression.py", line 143, in seek
    data = self.read(min(io.DEFAULT_BUFFER_SIZE, offset))
  File "/home/user/.pyenv/versions/3.8.2/lib/python3.8/_compression.py", line 103, in read
    data = self._decompressor.decompress(rawblock, size)
_lzma.LZMAError: Input format not supported by decoder
   with open(gpg_encrypted_tar_archive, 'rb') as f: 
        try:
            decrypted_data = gpg.decrypt_file(f, passphrase=passph)
            assert decrypted_data.ok
        except AssertionError:
            print(f"Decryption failed with message '{decrypted_data.status}' and status '{decrypted_data.ok}'")

        io_bytes_file_like_object = io.BytesIO(decrypted_data.data)

        # untar the parent archive
        tarfile.open(fileobj=io_bytes_file_like_object, mode='r')

        with tarfile.open(fileobj=io_bytes_file_like_object, mode='r:xz', debug=3, errorlevel=2) as get_lzma:

            for member in get_lzma.getmembers():

                if member.isfile():
                    file_o = get_lzma.extractfile(member)
                    json.load(file_o)

离开这个问题的一天似乎已经弄清了我对这个问题的理解。我将解释我的想法和我的发现。

tldr 我把我的 tarinfo 和 tarfile 和 exfileobjects 都搞混了。

根据 OP,传递给 open() 的文件在幕后表示为 <_io.BufferedReader name='myfile.tar.xz'>。所以让代码与未加密的文件和 open() 一起工作,实际上只能证明文件没有损坏。所以没有问题,我不会再提了。

按照 OP 中的代码示例,从 decrypted_data 对象调用 decrypted_data.data returns 原始字节字符串。这些原始字节是我的 2 tar 个文件。解压缩 tar 和压缩 tar 嵌套结构。字符串 starts b'myfile.tar.xz\x00\x00\x00\...... 等。我们将其包装为一个字节对象 io.BytesIO(decrypted_data.data) 以便我们有一个可以使用的接口,因此它可以传递给 tarfile.open()最初。到目前为止一切顺利,但这正是事情tar注定会出错的地方。

我决定在以下代码中使用 2 个上下文管理器。在 OP 中我调用了 tarfile.open() 两次,我想我一定假设有一个提取操作以及 mode='r'。您可以看到我现在在每个上下文管理器的 member 上对 extractfile() 进行了 2 次调用。第一个 extractfile() 从未压缩的 tar 中提取 tar.xz。这是我最终作为 mode=r:xz 传递给下一个上下文管理器的 ExFileObject "file"。在 OP 中,它是一个 TarInfo 对象,而不是提取的数据,这是错误的。

extractfile() 的第二次调用是在第二个上下文管理器的 member 上完成的,以便从 myfile.tar.xz.

获取 readable_members_value
with tarfile.open(fileobj=io_bytes_file_like_object, mode='r') as uncompressed_tar_file:

    # uncompressed_tar_file is tarfile.TarFile object
    for member in uncompressed_tar_file.getmembers():

      # member is TarInfo object
        tar_file_object = uncompressed_tar_file.extractfile(member)

        # tar_file_object is ExFileObject
        with tarfile.open(fileobj=tar_file_object, mode='r:xz', debug=3, errorlevel=2) as lzma_compressed_tar_file:
            for member in lzma_compressed_tar_file.getmembers():
                if member.isfile():
                    readable_members_value = lzma_compressed_tar_file.extractfile(member)

                    # now works where it failed before equivalent to something like file_o.read() in OP
                    print(readable_members_value.read())

                    decoded_readable_member_value = readable_member_value.read().decode("utf-8")
                    json_data = json.loads(decoded_readable_member_value)
                    print(json_data)

除最后几行外,其余代码几乎相同。您可以通过 OP 中的变量名称看到我期望 file_o 是一个文件对象。 json.load(file_o) 会针对文件指针工作,但在这种情况下 readable_member.read() 返回字节文字 b'' 所以实际上 json.loads() 我不需要 json.load().