从tar档案中删除重复项

4

我试图创建多个文本文件的存档。有时,这些文件会被更新,当这些文件被更新时,我使用tar--update选项将这些文件附加到归档中。

假设我们有两个文件:test1.txttest2.txt。这些文件被添加到归档test.tar中。

使用tar -tf test.tar检查tar文件:

我得到了预期的结果:

test1.txt
test2.txt

现在,如果我更新 test2.txt 文件,并使用 tar -f test.tar -u test2.txt 命令将其附加到归档中。

我期望运行tar -tf test.tar 命令的输出为:

test1.txt
test2.txt

但是我却得到:

test1.txt
test2.txt
test2.txt

那么我该如何摆脱这个tar包,以删除旧的test2.txt文件呢?我知道在解压缩存档后,我将只获得两个文件中最新的更改,因此这个问题在这个演示中似乎很容易解决,但实际上我正在对数千个5000行的文件进行归档,所以重复运行后,归档文件大小会变得非常大。

目前我的做法是将文件解压缩到临时目录中,然后每次运行脚本时重新进行归档。这显然非常低效。我希望有一个tar选项我没有发现。


1
据我所知,在tar命令中没有替换文件的选项。我建议在添加新文件之前删除现有文件。如果您正在运行GNU tar,请使用--delete - Digvijay S
1
你也可以考虑使用不同于tar的归档格式。 - Shawn
这样想,--update 更新的是 TAR 文件,而不是 TAR 文件中的文件。因此它按设计工作。 - Nic3500
我可以接受其他格式,通常在更新后我会对tar文件进行gzip压缩。那么还有哪些支持覆盖旧文件的归档/压缩格式呢? - Osama Adam
那么,是否有其他支持覆盖旧文件的归档/压缩格式呢?zip可以做到。 - Fravadona
2个回答

2
TAR 简单地将原始文件内容与一些元数据混合在一起进行连接。正如您所注意到的那样,更新文件只是将文件附加到 TAR 的末尾,并且按照惯例,出现在 TAR 中的最后一个文件会“胜出”。TAR 不仅仅是更新文件,因为这可能意味着所有更新后的文件内容都必须向后移动一些字节来为更大的新文件版本腾出空间。
实际上,这里没有提到的有一个适合您用例的 TAR 选项:--occurrence=[NUMBER]。使用此选项,您可以指定要提取或删除的具有相同名称/路径的多个文件版本中的哪一个。它将在您的简单示例中正常工作。这是我设置它的方式:
echo foo > test1.txt
echo foo > test2.txt
tar -cf updated.tar test1.txt test2.txt
sleep 1s
echo barbara > test2.txt
tar --update -f updated.tar test1.txt test2.txt
sleep 1s
echo foobar > test2.txt
tar --update -f updated.tar test1.txt test2.txt
tar tvlf updated.tar
    -rwx------ user/group   4 2022-03-29 19:00 test1.txt
    -rwx------ user/group   4 2022-03-29 19:00 test2.txt
    -rwx------ user/group   8 2022-03-29 19:01 test2.txt
    -rwx------ user/group   7 2022-03-29 19:01 test2.txt

请注意,tar --update 只会检查时间戳而不是内容,时间戳只有1秒的精度!因此,我们需要等待1秒钟以确保时间戳至少比现有文件晚1秒,否则 tar 将不会将其添加到存档中。当复制粘贴此代码时,这一点尤为重要。
简单地调用 --delete 将删除所有版本
tar --delete -f updated.tar test2.txt
tar tvlf updated.tar
    -rwx------ user/group   4 2022-03-29 19:00 test1.txt

当指定 --occurrence=1 时,只会删除第一次出现的,即最的版本:

tar --delete -f updated.tar test2.txt
tar tvlf updated.tar
    -rwx------ user/group   4 2022-03-29 19:00 test1.txt
    -rwx------ user/group   8 2022-03-29 19:01 test2.txt
    -rwx------ user/group   7 2022-03-29 19:01 test2.txt

很遗憾,对于--delete选项,您只能删除一个文件版本。因此,您需要重复删除最旧的版本,直到只剩下最新的版本为止。虽然可以在bash中完成此操作,这至少比将其提取到临时文件夹中更加节省空间,但可能会更慢,因为它必须多次扫描归档文件,每次扫描归档文件时基本上会完全重写归档文件。
我建议使用我编写的ratarmount代替。它将挂载存档文件(而不是实际提取),并公开一个文件夹视图,显示每个文件的最新版本。使用此方法,您可以创建新的精简存档文件:
python3 -m pip install --user ratarmount
ratarmount updated.tar
ls -lA updated/
    -rwx------ 1 user group 4 Mar 29 19:14 test1.txt
    -rwx------ 1 user group 7 Mar 29 19:14 test2.txt
tar -c -f most-recent.tar -C updated/ .
tar tvlf updated.tar
    drwxrwxrwx user/group   0 2022-03-29 19:00 ./
    -rwx------ user/group   4 2022-03-29 19:00 ./test1.txt
    -rwx------ user/group   7 2022-03-29 19:01 ./test2.txt

这就是全部内容。使用 -C 并指定归档.文件夹,导致tar tvlf的输出与前面的点略有不同。通常情况下,这没有问题,但您可以通过以下任何一种稍微更具问题性的选项来避免这种情况:

tar -c -f most-recent.tar -C updated/ test1.txt test2.txt
tar -c -f most-recent.tar -C updated/ $( cd updated && find . -mindepth 1 -maxdepth 1 )
( cd updated/ && tar -c -f ../most-recent.tar {[^.],.[!.],..?}*; )

如果您在使用ratarmount时遇到问题,请在这里提交问题。 请注意,ratarmount甚至会在特殊的隐藏文件夹中公开那些较旧的版本:

ratarmount updated.tar
ls -lA updated/test2.txt.versions/
    -rwx------ 1 user group 4 Mar 29 20:10 1
    -rwx------ 1 user group 8 Mar 29 20:10 2
    -rwx------ 1 user group 7 Mar 29 20:10 3

特殊的.versions文件夹中的文件名与给定的--occurrence参数匹配。


在bash中,上述提到的使用--occurrence的版本如下:

function deleteAllButMostRecentInTar()
{
    local archive=$1
    local filesToDelete=$( mktemp )

    while true; do
        tar --list --file "$archive" | sort | uniq -c |
            sed -n -E '/^[ \t]*1 /d; s|^[ \t]*[0-9]+ ||p' > "$filesToDelete"
        if [[ -s "$filesToDelete" ]]; then
            local fileCount=$( cat -- "$filesToDelete" | wc -l )
            echo -n "Found $fileCount files with more than version. Deleting ..."
            tar --delete --occurrence=1 --files-from="$filesToDelete" \
                --file "$archive"
            echo " OK"
        else
            break
        fi
    done
    rm -- "$filesToDelete"
    echo
}

deleteAllButMostRecentInTar updated.tar
tar tvlf updated.tar
    -rwx------ user/group   4 2022-03-29 19:00 test1.txt
    -rwx------ user/group   7 2022-03-29 19:01 test2.txt

0

如果你希望 tar 总是在原地操作,你可以使用像这样的包装器(受到 mxmlnkn 令人惊叹的回答的启发)。

你可以将这个添加到你的 ~/.bashrc 文件中。

# source: https://dev59.com/kL_qa4cB1Zd3GeqPSe_Y#71666950
function deleteAllButMostRecentInTar()
{
    local archive=$1
    local filesToDelete=$(mktemp)
    local shouldPrintMessage=true

    while true; do
       tar --list --file "$archive" | sort | uniq -c |
           sed -n -E '/^[ \t]*1 /d; s|^[ \t]*[0-9]+ ||p' >| "$filesToDelete"
        if [[ -s "$filesToDelete" ]]; then
            if [[ $shouldPrintMessage == true ]]; then
                echo "Found files in archive with multiple versions:"
                cat -- "$filesToDelete"
                shouldPrintMessage=false
                echo "Deleting all but most recent version..."
            fi
            local fileCount=$( cat -- "$filesToDelete" | wc -l )
            tar --delete --occurrence=1 --files-from="$filesToDelete" \
                --file "$archive"
        else
            break
        fi
    done
    if [[ -s "$filesToDelete" ]]; then
        echo "Done."
    fi
    rm -- "$filesToDelete" >/dev/null 2>&1
    echo
}

# tari, tar "in-place"
# - an inconvenient feature of tar is that it never overwrites members with the
#   same name, meaning that operations like --append and --update will not
#   replace an existing member with a file of the same name.
# - tar "in-place" is a wrapper around tar that calls
#   deleteAllButMostRecentInTar after every tar operation
function tari () {
  tar "$@"

  # First we must figure out the archive name. Possible formats are:
  # -XXXf <archive>
  # --file=<archive>
  # Note: Old-style tar option (tar -cf <archive>) is not supported because it
  # is bonkers.
  local archive_name
  if [[ "$@" =~ -[a-zA-Z0-9]*f[[:space:]]+([^[:space:]]+) ]]; then
    archive_name="${BASH_REMATCH[1]}"
  elif [[ "$@" =~ --file=([^[:space:]]+) ]]; then
    archive_name="${BASH_REMATCH[1]}"
  fi

  # If the archive is compressed, do nothing. (the tar operations that modify
  # archives don't work on compressed archives)
  if [[ "$archive_name" =~ \.(gz|bz2|xz|Z)$ ]]; then
    return
  fi

  if [[ -n "$archive_name" ]]; then
    deleteAllButMostRecentInTar "$archive_name"
  fi
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接