连接 tar 个文件,这样生成的 tar 可以在没有 -i 选项的情况下打开

Concatenate tar files so that the resulting tar can be opened without the -i option

我得到了 tar 个包含很多非常小的 JSON 文件的档案。每天我都会收到一个新的 tar 存档。现在我想将每日 tar 存档合并为每年 tar 存档并压缩。我使用以下 bash 脚本来做到这一点:

tar -cf "/mnt/archive/archive - 2020.tar" --files-from /dev/null
for f in /mnt/data/logs/2020/logs-main-2020-??-??.tar
do
    tar -n --concatenate --file="/mnt/archive/archive - 2020.tar" $f
done

pxz -T6 -c "/mnt/archive/archive - 2020.tar" > "/mnt/archive/archive - 2020.tar.xz"
rm "/mnt/archive/archive - 2020.tar"

这行得通,但是 tar 文件的串联速度越慢,主 tar 文件越大。

我可以使用 cat 指令简单地将所有 tar 加在一起,但生成的存档随后包含原始 tar 的所有存档结束空标记秒。因此,结果 tar 必须使用 -i 选项打开,这不是使用结果 tar.

的系统的选项

如何在不需要缓慢 tar 连接的情况下连接 tar 文件,并且仍然创建有效的 tar 而中间没有空值?我可以做一些 cat,un-tar,re-tar,compress pipe 吗?

一些基于 perl 的方法:

首先,使用核心 Archive::Tar 模块读取现有 tar 文件并创建新文件的脚本(由于模块的限制,它必须保存组合目标的数据tar 文件在写入之前一次性全部存入内存;可能是大量数据的问题):

#!/usr/bin/env perl
use warnings;
use strict;
use feature qw/say/;
use Archive::Tar;

# First argument is the new tar file to create, rest are ones to
# copy files from.

die "Usage: [=10=] DESTFILE SOURCEFILE ...\n" unless @ARGV >= 2;

my $destfile = shift;
my $dest = Archive::Tar->new;

foreach my $file (@ARGV) {
  my $src = Archive::Tar->iter($file) or exit 1;
  say "Adding contents of $file";
  while (my $file = $src->() ) {
    my $name = $file->full_path;
    say "\t$name";
    $dest->add_data($name, $file->get_content,
                    { mtime => $file->mtime,
                      size => $file->size,
                      mode => $file->mode,
                      uid => $file->uid,
                      gid => $file->gid,
                      type => $file->type,
                      devmajor => $file->devmajor,
                      devminor => $file->devminor,
                      linkname => $file->linkname
                    })
      or exit 1;
  }
}

$dest->write($destfile) or exit 1;
say "Wrote $destfile";

用法:

perl tarcat.pl "/mnt/archive/archive - 2020.tar" /mnt/data/logs/2020/logs-main-2020-??-??.tar

或者使用 Archive::Tar::Merge 的单行代码(通过 OS 包管理器安装,如果提供的话,或者最喜欢的 CPAN 客户端;不确定它的内存限制):

perl -MArchive::Tar::Merge -e '
    Archive::Tar::Merge->new(dest_tarball => $ARGV[0],
                             source_tarballs => [ @ARGV[1..$#ARGV] ])->merge
' "/mnt/archive/archive - 2020.tar" /mnt/data/logs/2020/logs-main-2020-??-??.tar

without the nulls in-between

这是主要问题。我们需要确定到底需要多少个零来切断结尾。然后,我们可以简单地使用 cat 来连接剩余的数据。

不幸的是,如果不从头读取 TAR 存档,则无法确定实际的 TAR 文件数据结束。但是对于 TAR 中的每个文件,如果我们知道大小就足够了,这样我们就可以简单地跳过它。这大大加快了存档的处理速度!这是一些简短的 python 代码,是我从我的宠物项目 ratarmount 中提取的。有许多不同的 TAR 格式风格,但这应该适用于大多数格式。为了更加通用,还必须支持 base-256 格式。

import io
import sys

with open(sys.argv[1], 'rb') as file:
    while True:
        blockContents = file.read(512)
        if len(blockContents) < 512:
            sys.exit(1)

        # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_01
        # > At the end of the archive file there shall be two 512-byte blocks filled with binary zeros,
        # > interpreted as an end-of-archive indicator.
        if blockContents == b"[=10=]" * 512:
            blockContents = file.read(512)
            if blockContents == b"[=10=]" * 512:
                print(file.tell() - 2 * 512)
                sys.exit(0)
            sys.exit(1)

        rawSize = blockContents[124 : 124 + 12].strip(b"[=10=]")
        # TODO This might fail for non-POSIX GNU tar base 256 encoded sizes
        #      https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions
        size = int(rawSize, 8) if rawSize else 0
        file.seek(size if size % 512 == 0 else size + ( 512 - size % 512 ), io.SEEK_CUR)

此函数将 return TAR 存档的大小,不包括 zero-byte 块。我们可以使用这个值来截断 TAR.

function tarcat()
{
    local FIND_TAR_FILE_END_SCRIPT
    read -r -d '' FIND_TAR_FILE_END_SCRIPT <<'EOF'
<COPY PASTE THE ABOVE PYTHON SCRIPT HERE!>
EOF

    local realDataSize
    while [[ "$#" -gt 0 ]]; do
        if [[ "$#" -gt 1 ]]; then
            realDataSize=$( python3 -c "$FIND_TAR_FILE_END_SCRIPT" "" )
            if [[ $? -eq 0 ]]; then
                head -c "$realDataSize" -- ""
            fi
        else
            cat -- ""
        fi
        shift
    done
}

这个bash函数可以这样使用:

for i in $( seq 3 ); do
    echo "foo$i" > "bar$i"
    tar -cf "tar$i.tar" "bar$i"
done

ls -l tar[0-9].tar
# -rwx------ 1 user group 10240 Mar 30 00:17 tar1.tar
# -rwx------ 1 user group 10240 Mar 30 00:17 tar2.tar
# -rwx------ 1 user group 10240 Mar 30 00:17 tar3.tar
tar tvlf tar3.tar
# -rwx------ user/group   5 2022-03-30 00:16 bar3

tarcat tar1.tar tar2.tar tar3.tar > concatenated-without-zeros.tar

ls -l concatenated-without-zeros.tar
# -rwx------ 1 user group 12288 Mar 30 00:18 concatenated-without-zeros.tar
tar tvlf concatenated-without-zeros.tar
# -rwx------ user/group   5 2022-03-30 00:16 bar1
# -rwx------ user/group   5 2022-03-30 00:16 bar2
# -rwx------ user/group   5 2022-03-30 00:16 bar3

可以看出,即使没有指定 -i 并且存档大小 (12 KiB) 小于串联档案的总和 (30 KiB),因为从前两个档案中删除了尾随零块(不是从最后一个中删除,因为它们充当 EOF 指示器)。

请注意,此代码尚未经过广泛测试。您可能还可以使 tarcat 成为 Python-only 脚本,但需要做更多的工作。