从 tar 存档中删除重复项

问题描述 投票:0回答:2

我正在尝试创建多个文本文件的存档。有时这些文件会更新,当这些文件更新时,我使用

tar
中的 --update 选项将这些文件附加到存档中。

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

使用

tar -tf test.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 选项。

bash tar archive
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秒的粒度!因此,我们需要等待 1s 以确保时间戳至少晚一秒,否则
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

现在就完成了。

tar tvlf
的输出看起来与前面的点有点不同,因为我们使用了
-C
并指定存档
.
文件夹。通常,这不会造成伤害,但您可以使用以下任何稍微有问题的替代方案来规避此问题:

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://stackoverflow.com/a/71666950/21567639
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 [[ -n "$archive_name" ]]; then
    deleteAllButMostRecentInTar "$archive_name"
  fi
}
© www.soinside.com 2019 - 2024. All rights reserved.