옵션 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

나는 트리의 하드 링크 백업을 생성하는 방식으로 이 프로그램을 작성할 것입니다. 그런 다음 작업이 실패하면 삭제된 파일을 백업에서 복원합니다. 작업이 성공하면 하드 링크 백업이 삭제됩니다.

물론 이것은 "언제든지 전원 코드를 뽑을 수 있다"는 의미에서 원자적이지 않고 논리적으로 원자적일 뿐입니다. 물론 추가 논리와 시작 시 실행되는 일부 후크를 사용하여 정렬할 수도 있습니다.

두 번의 패스(하나는 권한 확인용, 다른 하나는 삭제용)를 만드는 것은 까다롭습니다. 확장된 속성을 포함하여 모든 권한을 확인하려면 논리가 철저해야 합니다. 예를 들어 이렇게 하면 일반 Unix 권한이 괜찮아 보이 sudo chattr +i file더라도 file쓰기 가능한 디렉터리는 삭제할 수 없게 됩니다. "이 파일을 삭제할 수 있습니까?"에 대한 가장 좋은 리트머스 테스트는 실제로 시도해 보는 것입니다.

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 ]GNU는 find -writable디렉토리가 쓰기 가능한지 확인합니다. 읽기 전용 파일 시스템도 관리해야 합니다. 유사한 읽기/검색 가능.
  • ls -nd(또는 다양한 호환되지 않는 구현 stat) find -user/-uid파일 소유자를 확인하세요.
  • lsattr파일 플래그를 검색합니다.
  • mountpoint -q파일이 마운트 지점인지 확인하십시오. 또한보십시오 findmnt.

그러나 이를 모두 쉘 스크립트에 넣고 안정적으로 실행하는 것은 매우 까다로울 수 있습니다. 다음은 다음을 사용한 시도입니다 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

Katz 답변의 수정된 버전입니다. 원본 나무의 복사본을 만들고( 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

관련 정보