连接 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 吗?
- 我在输入 tars
的 JSON 文件名中没有任何空格字符,例如换行符
- 我在 CentOS 7 上使用 GNU tar v1.26
- 每个输入 tar 大约 1GB,所以将它们保存在内存中是不可能的选择
- 无需检查输出 tar 是否有重复条目。输入 tar 的创建方式确保它们没有重复的 JSON 文件
一些基于 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 脚本,但需要做更多的工作。
我得到了 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 吗?
- 我在输入 tars 的 JSON 文件名中没有任何空格字符,例如换行符
- 我在 CentOS 7 上使用 GNU tar v1.26
- 每个输入 tar 大约 1GB,所以将它们保存在内存中是不可能的选择
- 无需检查输出 tar 是否有重复条目。输入 tar 的创建方式确保它们没有重复的 JSON 文件
一些基于 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 脚本,但需要做更多的工作。