Only detect subvolume nesting
This commit is contained in:
parent
814e18cf12
commit
5ed6bd51fb
1 changed files with 21 additions and 84 deletions
105
src/snapshot.sh
105
src/snapshot.sh
|
|
@ -1,106 +1,43 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Read mountinfo from stdin and fill the caller's associative array. Kernel
|
||||
# escapes protect whitespace and backslashes in the mount-point field.
|
||||
snapshot_mounted_subvolumes() {
|
||||
local source=${1%/} mount remainder _ignored
|
||||
local -n discovered=$2
|
||||
while IFS=' ' read -r _ignored _ignored _ignored _ignored mount remainder; do
|
||||
[[ $remainder == *' - btrfs '* ]] || continue
|
||||
printf -v mount '%b' "$mount"
|
||||
if [[ $mount == "$source/"* && $mount != "$source/" ]]; then
|
||||
# The nameref fills the caller's array.
|
||||
# shellcheck disable=SC2034
|
||||
discovered["${mount#"$source/"}"]=1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
snapshot_create() (
|
||||
local source target parent name directory child relative inode original
|
||||
local -a pending
|
||||
local -A mounts=()
|
||||
unset GLOBIGNORE
|
||||
shopt -s dotglob nullglob
|
||||
local source target=$2 children mount remainder inode _ignored
|
||||
IFS= read -r -d '' source < <(realpath -ez -- "$1") || return 1
|
||||
target=$2
|
||||
while [[ $target == */ && $target != / ]]; do target=${target%/}; done
|
||||
name=${target##*/}
|
||||
parent=${target%/*}
|
||||
[[ $target == */* ]] || parent=.
|
||||
IFS= read -r -d '' parent < <(realpath -ez -- "${parent:-/}") || return 1
|
||||
target=${parent%/}/$name
|
||||
if [[ -e $target || -L $target ]]; then
|
||||
printf 'Snapshot destination already exists: %s\n' "$target" >&2
|
||||
return 1
|
||||
fi
|
||||
snapshot_mounted_subvolumes "$source" mounts < /proc/self/mountinfo || return 1
|
||||
btrfs subvolume snapshot "$source" "$target" || return 1
|
||||
pending=("$target")
|
||||
while (( ${#pending[@]} )); do
|
||||
directory=${pending[-1]}
|
||||
unset 'pending[-1]'
|
||||
if [[ ! -r $directory || ! -x $directory ]]; then
|
||||
printf 'Cannot traverse snapshot directory: %s\n' "$directory" >&2
|
||||
|
||||
children=$(btrfs subvolume list -o "$source") || return 1
|
||||
if [[ -n $children ]]; then
|
||||
printf 'Nested subvolumes are not supported under %s:\n%s\n' "$source" "$children" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Mounted subvolumes may live elsewhere and are absent from the list above.
|
||||
while IFS=' ' read -r _ignored _ignored _ignored _ignored mount remainder; do
|
||||
[[ $remainder == *' - btrfs '* ]] || continue
|
||||
printf -v mount '%b' "$mount"
|
||||
[[ $mount == "${source%/}/"* && $mount != "$source" ]] || continue
|
||||
inode=$(stat -c %i -- "$mount") || return 1
|
||||
if [[ $inode == 256 && -d $mount ]]; then
|
||||
printf 'Nested subvolumes are not supported: %s\n' "$mount" >&2
|
||||
return 1
|
||||
fi
|
||||
for child in "$directory"/*; do
|
||||
[[ -d $child && ! -L $child ]] || continue
|
||||
relative=${child#"$target/"}
|
||||
inode=$(stat -c %i -- "$child") || return 1
|
||||
# Unsnapshotted children have inode 2. A mounted subvolume may
|
||||
# instead hide an ordinary directory in its parent's snapshot.
|
||||
if [[ $inode == 2 || ${mounts[$relative]:-} == 1 ]]; then
|
||||
original=${source%/}/$relative
|
||||
if [[ -L $original ]]; then
|
||||
printf 'Subvolume changed into a symlink: %s\n' "$original" >&2
|
||||
return 1
|
||||
fi
|
||||
# Never recurse into the scratch snapshot, even below a child.
|
||||
if [[ $original -ef $target ]]; then
|
||||
rmdir -- "$child" || return 1
|
||||
continue
|
||||
fi
|
||||
# Refuse to discard data hidden beneath a mounted subvolume.
|
||||
rmdir -- "$child" || return 1
|
||||
btrfs subvolume snapshot "$original" "$child" || return 1
|
||||
fi
|
||||
pending+=("$child")
|
||||
done
|
||||
done
|
||||
done < /proc/self/mountinfo || return 1
|
||||
|
||||
btrfs subvolume snapshot "$source" "$target"
|
||||
)
|
||||
|
||||
snapshot_delete() (
|
||||
local target=$1 directory child inode index
|
||||
local -a pending subvolumes
|
||||
unset GLOBIGNORE
|
||||
shopt -s dotglob nullglob
|
||||
local target=$1
|
||||
while [[ $target == */ && $target != / ]]; do target=${target%/}; done
|
||||
if [[ -L $target ]]; then
|
||||
printf 'Refusing to delete a symlink as a snapshot: %s\n' "$target" >&2
|
||||
return 1
|
||||
fi
|
||||
btrfs subvolume show "$target" > /dev/null || return 1
|
||||
pending=("$target")
|
||||
subvolumes=("$target")
|
||||
while (( ${#pending[@]} )); do
|
||||
directory=${pending[-1]}
|
||||
unset 'pending[-1]'
|
||||
if [[ ! -r $directory || ! -x $directory ]]; then
|
||||
printf 'Cannot traverse snapshot directory: %s\n' "$directory" >&2
|
||||
return 1
|
||||
fi
|
||||
for child in "$directory"/*; do
|
||||
[[ -d $child && ! -L $child ]] || continue
|
||||
inode=$(stat -c %i -- "$child") || return 1
|
||||
[[ $inode != 256 ]] || subvolumes+=("$child")
|
||||
pending+=("$child")
|
||||
done
|
||||
done
|
||||
# Reverse discovery order puts every child before its parent.
|
||||
for ((index = ${#subvolumes[@]} - 1; index >= 0; index--)); do
|
||||
btrfs subvolume delete "${subvolumes[index]}" || return 1
|
||||
done
|
||||
btrfs subvolume delete "$target"
|
||||
)
|
||||
|
||||
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
||||
|
|
|
|||
Loading…
Reference in a new issue