假设我有以下结构:
$ 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
,然后触发删除rm
(rm
不删除,仅删除目录条目。删除由引用计数垃圾收集器发生)。
例如
#!/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.
,则无法删除该文件..
。- 如果您不知道文件在那里,则无法删除该文件。就像当它是一个目录时,您具有写入和搜索权限,但没有读取权限。
- 如果文件位于只读文件系统上,则无法删除文件
- 诸如
apparmor
、selinux
和其他强制访问控制框架之类的东西可能会妨碍,以及诸如用户命名空间、uid 命名空间之类的东西...... - 任何文件系统驱动程序都可以添加自己的约束。例如,有些人喜欢
zfs
或nfs
具有无法删除的特殊文件/目录。
其中一些您无法通过脚本轻松检查,除非尝试一下。
但对于上述大多数更常见的情况,还是有办法的。例如:
[ -w dir ]
GNUfind -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