选项1

选项1

假设我有以下结构:

$ mkdir d1
$ mkdir d1/d2
$ touch d1/f1
$ touch d1/d2/f2

$ chmod u-w d1/d2

如果我尝试删除d1,则无法删除,因为我没有 的写入权限d1/d2。但它仍然删除d1/f1

$ rm -rf d1
rm: cannot remove 'd1/d2/f2': Permission denied

$ ls d1
d2          # f1 has been deleted

有没有办法实现原子rm工具?例如,如果它不能删除所有内容,那么它什么也不删除。

答案1

我会以这样的方式编写这个程序,它创建树的硬链接备份。然后,如果操作失败,它会从备份中恢复已删除的文件。如果操作成功,则会删除硬链接备份。

当然,这只是逻辑上的原子性,并不是“我们可以随时拔掉电源线”意义上的原子性;尽管这也可以通过额外的逻辑和一些在启动时运行的钩子来安排。

进行两次传递(一次用于检查权限,另一次用于删除)是很棘手的。逻辑必须详尽才能检查所有权限,包括扩展属性。例如,如果我们这样做sudo chattr +i file,那么file即使常规的 Unix 权限看起来不错:目录是可写的,那么就变得不可删除。 “我们可以删除这个文件吗”的最佳试金石就是实际尝试一下。

这是一个经过一定测试的概念原型,作为概念证明,用于rsync基于硬链接的备份、恢复。该脚本称为atomic-rm.sh

#!/bin/sh

set -eu

if [ $# -ne 1 ] ; then
  echo "specify directory to remove"
  exit 1
fi

ar_src=$(realpath "$1")
ar_tmp=$(mktemp -d "$(dirname "$ar_src")/tmp-XXXXXX")

if [ $? -ne 0 ] ; then
  echo "unable to create temporary directory"
  exit 1
fi

cleanup()
{
  find "$ar_tmp" -type d -print0 | xargs -0 chmod +w

  if ! rm -rf "$ar_tmp" ; then
    echo "removal of temporary directory $ar_tmp failed"
    exit 2 # 2 indicates dirty failure
  fi
}

trap cleanup EXIT

if ! rsync -ar --link-dest="$ar_src" "$ar_src"/ "$ar_tmp"/ ; then
  echo "unable to create hard-linked backup of $ar_src in $ar_tmp"
  exit 1
fi

if ! rm -rf "$ar_src" ; then
  if ! rsync -ar --link-dest="$ar_tmp" "$ar_tmp"/ "$ar_src"/ ; then
    echo "removal of $ar_src failed; unfortunately, so did the rollback"
    exit 2 # 2 indicates dirty failure
  fi
  exit 1
fi

exit 0

通过一些额外的努力,它可以将一些信息存储在某个地方,引导时恢复脚本可以使用这些信息来清理松散的临时目录。临时目录被创建为要删除的目录的同级目录,以确保它们位于同一文件系统上;我们不能使用/tmp.

如果删除失败并执行回滚,它不会绝对恢复树的确切状态,因为目录被弄乱了,因此它们的修改时间戳被更改了。

请注意,在 中cleanup,我们必须遍历find目录以使它们可写。原因是,如果由于目录不可写而导致删除失败,那么它在备份副本中也会以同样的方式失败,因为rsync将复制这些目录权限。

答案2

选项1

一种方法是将树移动到一个trash目录,然后用于rm垃圾收集。使用 删除文件mv,然后触发删除rmrm不删除,仅删除目录条目。删除由引用计数垃圾收集器发生)。

例如

#!/bin/bash -e
directory_tree_to_remove="$1"
#only works if trash directory is on same file-system (no checks done),
#  if not a very expensive copy will be done, followed by an `rm`
#  :todo: add checks
#uses gnu `mv`: uses safety feature
#does not do full input error checking

trash_dir="…/trash"

#Atomicly remove tree or file, form its current location
mv -t "$trash_dir" "$directory_tree_to_remove"

#Garbage collect 
rm -rf "${trash_dir}/${directory_tree_to_remove}" || echo "error: could not garbage collect. There may be garbage left in "$trash_dir"

选项2

可能有适合您正在做的事情的事务性文件系统。

答案3

在这里,试图回答这个问题:“如何提前知道一个目录及其全部内容是否可以被删除”。

可以防止 Linux 上的普通用户删除文件的事情(我能想到的):

  • 非空目录无法删除,所以显然必须先删除其中的文件
  • 如果您没有目录的写入访问权限或搜索访问权限,则无法删除文件(取消其与其父目录的链接)。
  • t如果您既不拥有该文件,也不拥有该目录,则无法取消该文件与设置了该位的目录的链接。
  • a如果文件或其链接到的目录具有( FS_APPEND_FL) 或i( FS_IMMUTABLE_FL) 标志,则无法删除该文件。
  • /如果文件是挂载点nornornor .,则无法删除该文件..
  • 如果您不知道文件在那里,则无法删除该文件。就像当它是一个目录时,您具有写入和搜索权限,但没有读取权限。
  • 如果文件位于只读文件系统上,则无法删除文件
  • 诸如apparmorselinux和其他强制访问控制框架之类的东西可能会妨碍,以及诸如用户命名空间、uid 命名空间之类的东西......
  • 任何文件系统驱动程序都可以添加自己的约束。例如,有些人喜欢zfsnfs具有无法删除的特殊文件/目录。

其中一些您无法通过脚本轻松检查,除非尝试一下。

但对于上述大多数更常见的情况,还是有办法的。例如:

  • [ -w dir ]GNU find -writable,检查目录是否可写。还应该照顾只读文件系统。类似的可读/可搜索。
  • ls -nd(或各种不兼容的实现stat),find -user/-uid检查文件所有者。
  • lsattr检索文件标志。
  • mountpoint -q检查文件是否为挂载点。另请参阅findmnt

不过,将它们全部放在一个 shell 脚本中并可靠地执行将是相当棘手的。这是使用的尝试zsh

#! /bin/zsh -
file=${1?}
export LC_ALL=C

name=$file:t parent=$file:h

die() {
  print -ru2 -- "$@"
  exit 1
}

# can't delete / . .. ""
[[ $name = (/|.|..|) ]] && die "Can't delete $file"

[[ -e $file || -L $file ]] || die "Can't tell whether $file exists"

[[ -w $parent ]] || die "Parent dir not writable"
[[ -x $parent ]] || die "Parent dir not searchable"

[[ -k $parent && ! -O $parent && ! -O $file ]] &&
  die "Parent has t bit, is not ours, nor is $file"

[[ $(lsattr -d -- $parent 2> /dev/null) = ????(i|?a)* ]] &&
  die "parent has a or i file flag"

mountpoint -q -- $file &&
  die "$file is a mount point"

# at this point, we should be able to delete non-directory files
[[ -L $file || ! -d $file ]] && exit 0

# same for empty readable dirs
[[ -r $file ]] && ()((! $#)) $file(NF) && exit 0

case $file in
  (/*) cd -P -- $file;;
  (*) cd -P -- ./$file;; # workaround for -, -1, +1, CDPATH...
esac 2> /dev/null || die "Can't cd into $file"

lsattr -Ra .//. 2> /dev/null |
  grep -Eq '^.{4}(i|.a).*//' &&
  die "Files with a and/or i file flags seen in $file"

zmodload zsh/system
{
  ERRNO=0
  non_empty_dirs=({.,**/*}(NDFoN))
  # error encountered while getting that list. Takes care of 
  # non-readable dirs or non-lstat()able mountpoints...
  (( ERRNO )) && {
    syserror -p "Error encountered while traversing $file: " $ERRNO
    exit 1
  }
}

for dir ($non_empty_dirs) {
  [[ -w $dir ]] || die "non-writable non-empty directory found"

  [[ -k $dir && ! -O $dir ]] && ()(($#)) $dir/*(ND^OY1) &&
    die "directory with t bit I don't own found with files I don't own inside"
}

mountpoints=(${(f)"$(findmnt -nlo target --submount -T .)"})
set -o extendedglob
for mount ($mountpoints) {
  # unescape the \xXX parts:
  mount=${mount//(#b)\\(x??)/${(#):-0$match[1]}}

  [[ $mount = $PWD/* ]] &&
    die "Mount point found under $file"
}

# probably deletable.
exit 0

用作:

if that-script "$some_dir_or_file"; then
  rm -rf -- "$some_dir_or_file"
  # and hope for the best
else
  echo probably not fully deletable
fi

在某些极端情况下,它仍然可能给出误报或漏报。当然,在检查时间和致电时间之间rm,许多事情可能会发生变化。

答案4

卡兹答案的修改版本。制作原始树的副本(cp -a),尝试删除该副本,如果删除成功,则删除原始树。

  • -- :根据树的大小,可能需要更长的时间和更多的磁盘空间
  • ++ :如果无法原子删除,则原始树保持不变
#!/bin/sh

if [ $# -ne 1 ] ; then
  echo "specify directory to remove"
  exit 1
fi

ar_src=$(realpath "$1")
ar_tmp=$(mktemp -d "$(dirname "$ar_src")/tmp-XXXXXX")

if [ $? -ne 0 ] ; then
  echo "unable to create temporary directory"
  exit 1
fi

if ! cp -a "$1" "$ar_tmp"; then
    echo "unable to copy"
    exit 1
fi

if rm -rf "$ar_tmp"; then
    rm -rf "$ar_src";
fi

exit 0

执行 :

$ ./atomic-rm2.sh d1/
rm: cannot remove '/tmp/testRMDIR/tmp-4sOBNS/d1/d2/f2': Permission denied

$ tree d1
d1
├── d2
│   └── f2
└── f1

相关内容