#!/bin/bash export BTRFS_BIN=$(which btrfs) module_load confirm module_load helpers module_load path # NOTE: set this for sudo # ALL ALL = (root) NOPASSWD:/bin/btrfs is_subv () { if [[ $1 == "-r" ]]; then return $(btrfs subvolume show "$2" 2> /dev/null | grep readonly &> /dev/null) else return $(btrfs subvolume show "$1" &> /dev/null) fi } is_subvv () { if is_subv "$@"; then echo is a $([[ $1 == "-r" ]] && echo "read only") subvolume else echo is not a $([[ $1 == "-r" ]] && echo "read only") subvolume return 1 fi } # TODO: convert these into subcommands subv_snap () { local usesudo; local ro [[ $1 == "-r" ]] && ro=true && shift [[ $# -eq 0 ]] && echo no subvolume name passed && return 1 [[ $# -eq 1 ]] && echo no snapshot path/name passed && return 2 [[ $EUID -ne 0 ]] && usesudo=sudo if is_subv "$1"; then if [[ -v PS1 ]]; then confirm create snapshot of subvolume $(realpath "$1") to $(realpath "$2") || return 1 $usesudo "$BTRFS_BIN" subvolume snapshot $ro "$1" "$2" else $usesudo "$BTRFS_BIN" subvolume snapshot $ro -q "$1" "$2" fi else echo $1 not a subvolume, can not make a snapshot return 3 fi } subv_make() { local usesudo local uid local gid [[ $EUID -ne 0 ]] && usesudo=sudo uid=${2:-$USER} gid=${3:-$uid} mkdir -p $(dirname "$1") &> /dev/null echo $usesudo "$BTRFS_BIN" subvolume create "$1" $usesudo "$BTRFS_BIN" subvolume create "$1" echo $usesudo chown "$uid":"$gid" "$1" $usesudo chown "$uid":"$gid" "$1" } subv_del() { local usesudo; local delete [[ $1 == "-d" ]] && delete=true && shift [[ $EUID -ne 0 ]] && usesudo=sudo if $usesudo "$BTRFS_BIN" subvolume show "$1" &> /dev/null; then if [[ -v PS1 && ! $delete ]]; then confirm "DELETE the subvolume $(realpath "$1") (pass -d to avoid confirm)" || return 1 fi $usesudo "$BTRFS_BIN" subvolume delete "$1" else echo "$1 not a btrfs subvolume, nothing to delete" fi } # make_base_subvols() { # svols="shell admin opt data docker images temp" # BTRFSDATAROOT=${BTRFSDATAROOT:-/mnt/data} # # cd $BTRFSDATAROOT || exit # for svol in $svols; do # subv_make $BTRFSDATAROOT/$svol $@ # done # } #ssudo mount -o subvol=opt /dev/sda4 /test # FIXME: use subv_xfer # snapshot_restore () { # local src=$1 # local dest=$2 # local name=${3:-$(echo "$src"| rev | cut -f 2- -d '.' | rev)} # local usesudo # echo copying "$src" at "$PWD" to "$2" then renaming to "$name" # # TODO check for snapshot, strip volume from source path, remove extra snapshot when done # [[ $EUID -ne 0 ]] && usesudo=sudo # $usesudo "$BTRFS_BIN" send "$src" | $usesudo "$BTRFS_BIN" -q receive "$dest" # # cd $2 || return # $usesudo "$BTRFS_BIN" subvolume snapshot "$dest"/"$src" "$dest"/"$name" # # todo check for snapshot then delete transfered one # } subv_size () { local all; [[ $1 == "-a" ]] && all=true if [[ $all ]]; then sudo "$BTRFS_BIN" qgroup show "${2:-$PWD}" --kbytes | tail -n +3 str=$(sudo "$BTRFS_BIN" qgroup show "${2:-$PWD}" --kbytes | tail -n +3); else sudo "$BTRFS_BIN" qgroup show "${2:-$PWD}" --kbytes | grep "$1" str=$(sudo "$BTRFS_BIN" qgroup show "${2:-$PWD}" --kbytes | grep "$1") fi # echo $str subvolumeFolderSize=0; while read line; do FIELDS=( $line ) thisLineKb="${FIELDS[2]/'.00KiB'/''}"; # echo $thisLineKb subvolumeFolderSize=$((subvolumeFolderSize+thisLineKb)); done <<< "$str" [[ $all ]] && echo "size of entire filesystem at ${2:-$PWD}" || echo size of subvolumes/snapshots "$1" at "${2:-$PWD}" echo $subvolumeFolderSize KB echo "~ $((subvolumeFolderSize/1024)) MB" echo "~ $((subvolumeFolderSize/1024/1024)) GB" } # https://unix.stackexchange.com/questions/93324/how-does-this-find-command-using-find-exec-sh-c-sh-work subv_find() { local dir=${1:-.} local max="-maxdepth ${2:-1}" [[ "$2" == "max" ]] && max="" sudo /bin/find "$dir" -mindepth 1 $max -type d -exec sudo -E bash -c ' for d do lvl=$(/bin/btrfs subvolume show "$d" 2>/dev/null | grep "Top level ID:") [[ ${lvl##* } -gt 0 ]] && printf "%s\n" "$d" done' find-sh {} + } folder_snapshot() { if [[ $1 == "-r" ]];then readonly=-r shift 1 else readonly="" fi for sv in $(find_subvolumes "$1" 1); do if [[ -d $2/$sv ]]; then echo snapshot "$sv" already exists in "$2". You must manually delete target snapshots! else if sudo mkdir -p "$2" > /dev/null; then sudo "$BTRFS_BIN" sub snap $readonly "$1/$sv" "$2/$sv" else echo unable to make directory "$2" so can not make subvolumes therein fi fi done } subv_xfer() { # Show usage and exit with status help () { echo 'usage: subv_xfer -h,-r ' } if [[ $1 == "-h" ]]; then help; return; fi local dr="echo " if [[ $1 == "-r" ]]; then unset dr; shift; else echo this will be a dry run, use -r to actually run the command fi if [[ $# -lt 2 ]]; then help; return 1; fi # Check for directories if ! is_subv "$1"; then echo source "$1" not a subvolume help return 2 fi if [[ ! -d "$2" ]]; then echo destination "$2" not a valid directory help; return 3 fi # Get paths local src=$(abs_path "${1%/}") local psrc="$(dirname "$src")" local dest=$(abs_path "${2%/}") local snap=$(basename "$src") local ext="$(echo "$snap" | cut -s -f 2 -d '.')" local name=$(echo "$snap" | cut -f 1 -d '.') [[ $ext ]] && ext=".$ext" [[ $3 ]] && ext=".$3" local tsnap if ! is_subv -r "$src"; then tsnap=$psrc/$name.tmp $dr sudo "$BTRFS_BIN" sub snap -r "$src" "$tsnap" fi if [[ $dr ]]; then echo "sudo $BTRFS_BIN send $([[ $tsnap ]] && echo "$tsnap" || echo "$src") | btrfs receive $dest" echo "[[ ""$tsnap"" ]] && sudo $BTRFS_BIN sub del $tsnap" echo "sudo ""$BTRFS_BIN"" sub snap $dest/$name.tmp $dest/$name$ext" echo "sudo ""$BTRFS_BIN"" sub del $dest/$name.tmp" else # TODO: add mbuffer, show progress if is_subv "$dest"/"$name".tmp; then echo destination temporary subvolume "$dest"/"$name".tmp exists. Deleting so xfer can proceed subv_del "$dest"/"$name".tmp fi sudo "$BTRFS_BIN" send $([[ $tsnap ]] && echo "$tsnap" || echo "$src") | sudo "$BTRFS_BIN" receive "$dest" if is_subv -r "$dest"/"$name".tmp; then echo transfer of "$name" to "$name""$ext" is complete, deleting source temporary snap "$tsnap" [[ "$tsnap" ]] && sudo "$BTRFS_BIN" sub del "$tsnap" if is_subv "$dest"/"$name""$ext"; then if ! confirm -s destination subvolume "$dest"/"$name""$ext" already exists do you want to overwrite it; then sudo "$BTRFS_BIN" sub del "$dest"/"$name".tmp return 1 fi fi sudo "$BTRFS_BIN" sub snap "$dest"/"$name".tmp "$dest"/"$name""$ext" sudo "$BTRFS_BIN" sub del "$dest"/"$name".tmp ls -la "$dest" ls -la "$dest"/"$name""$ext" else echo error receiving "$dest"/"$name".tmp, deleting source temporary snap "$tsnap" [[ "$tsnap" ]] && sudo "$BTRFS_BIN" sub del "$tsnap" return 1 fi fi } subv_mount () { # echo sudo mount $1 -o subvol=$2 $3 if mountpoint "$3" &> /dev/null; then echo "$3" already a mountpoint, aborting subvolume mount else mkdir -p "$3" &> /dev/null if ! sudo mount "$1" -o subvol="$2" "$3" > /dev/null ; then echo "failed to mount $2"; return 1; fi fi } named_snapshot () { local usage; local subvol; local target; local run; local usesudo [[ $1 == "-r" ]] && run=true && shift [[ $EUID -ne 0 ]] && usesudo=sudo subvol=$(realpath "$1") usage="usage: named_snapshot <-r> " if sudo "$BTRFS_BIN" subvolume show "$1" &> /dev/null; then # if [[ -v PS1 && ! $delete ]]; then [[ ! $2 ]] && echo no extension name for named snapshot && echo "$usage" && return 1 if [[ $3 ]]; then [[ ! -d $3 ]] && echo "target directory $3 does not exist, can not make snapshot" && echo "$usage" && return 2 target=$(realpath "$3")/$(basename "$(rm_ext "$subvol")").$2 else target="$(rm_ext "$subvol").$2" fi [[ -v PS1 && ! $run ]] && confirm "make snapshot of $subvol to $target (pass -r to avoid confirm )" || return 4 $usesudo "$BTRFS_BIN" sub snap "$subvol" "$target" else echo "$subvol not a btrfs subvolume, can't make named snapshot" echo "$usage" return 3 fi } # finds device and subvolume of a mountpoint (or a mountpoint above the given path) subv_mp () { module_load filesystem local mp; local subvol mp=$(find_mountpoint "$1") path=${1//"$mp"/} dev=$(mount | grep "$mp " | cut -f 1 -d ' ') IFS=, read -r -a input <<< $(findmnt -nt btrfs | grep "$mp ") for x in "${input[@]}"; do eval $(echo "$x" | grep subvol=) [[ $subvol ]] && echo "$dev" "$subvol""$path" done } btrfs_clone () { # btrfs_clone # if [[ $# == "3" ]]; then module_load filesystem local src; local dest; local dr; local usesudo [[ $EUID -ne 0 ]] && usesudo=sudo dr="--dry-run" [[ $1 == "-r" ]] && dr="" && shift subvol=$1 src=$2 dest=$3 $usesudo umount /tmp/source &> /dev/null sudo mount "$src" /tmp/source # $usesudo umount /tmp/dest &> /dev/null # if subv_mount $src $subvol /tmp/source; then if ! subv_make "$dest"/$(basename "$subvol") &> /dev/null; then echo unable to make subvolume "$1" at "$dest"; return 2; fi # $usesudo btrfs subvolume show $dest/$(basename $subvol) # if subv_mount $(subv_mp $dest/$(basename $subvol)) /tmp/dest; then # echo cloning now # # echo $usesudo btrfs-clone --toplevel $dr -v /tmp/source /tmp/dest cmd="$usesudo btrbk $dr archive /tmp/source/$1 $dest/$1" echo "command: $cmd" if eval "$cmd"; then echo "######### $dest/$1 #########" ls -la /tmp/dest echo "#################################" else echo clone failed echo "$cmd" fi # else # echo unable to mount a destination subvolume $1 at $3 # fi # else # echo unable to mount source subvolume $1 from $2 # return 1 # fi # confirm press any key to unmount temporary mountpoints /tmp/source, /tmp/dest echo unmounting source $usesudo umount /tmp/source &> /dev/null # $usesudo umount /tmp/dest &> /dev/null } # alias alias btvl="sudo $BTRFS_BIN subvolume list" alias btrfs="sudo $BTRFS_BIN" alias btsub="sudo $BTRFS_BIN subvolume" dir2subv () { # Directory to convert into BTRFS subvolume local dirPath; local dirSub; local perms dirPath="$(realpath "$1")" if [[ ! -d $dirPath ]]; then echo no directory at "$dirPath"; return 1; fi [[ $2 ]] && dirSub="$(realpath "$2")" perms=/tmp/perms_$(basename "$dirPath") if is_subv "$dirPath"; then echo "$dirPath" is already a subvolume, exiting; return 0; fi if ! confirm transform "$dirPath" into a subvolume at "${dirSub:-$dirPath}"; then return 1; fi pushd "$dirPath" &> /dev/null || return echo saving permissions at "$PWD" to "$perms" sudo bash -c 'getfacl -R . > '"$perms"'' popd &> /dev/null || return 0 if [[ ! $dirSub ]]; then echo moving "$dirPath" to "${dirPath}_temp" sudo mv "${dirPath}" "${dirPath}_temp" fi echo creating subvolume at "${dirSub:-$dirPath}" sudo "$BTRFS_BIN" subvolume create "${dirSub:-$dirPath}" echo copying contents to new subvolume if [[ $dirSub ]]; then # if confirm copying "$dirPath" to "$dirSub"; then sudo /bin/cp --archive --one-file-system --reflink=always "${dirPath}/." "${dirSub}" # fi else # if confirm copying "${dirPath}"_temp back to subvolume at "$dirPath"; then sudo /bin/cp --archive --one-file-system --reflink=always "${dirPath}_temp/." "${dirPath}" # fi fi pushd "${dirSub:-$dirPath}" &> /dev/null || return # cat $perms echo restoring saved permissions using "$perms" on $PWD if sudo setfacl --restore="$perms"; then echo permissions restored, deleting $perms sudo rm -f "$perms" else echo FATAL: permissions were not resorted fi popd &> /dev/null || return 0 echo "$dirPath" has been converted into a subvolume at "${dirSub:-$dirPath}" if [[ -d ${dirPath}_temp ]];then echo $dirPath was converted but now check it is ok before deleting temporary copy if confirm -s do you want to now delete the copy at "${dirPath}"_temp; then sudo rm -rf --one-file-system "${dirPath}_temp" fi fi } # sudo find / -type d -exec sh -c ' # for d do # btrfs subvolume show "$d" >/dev/null 2>&1 && printf "%s\n" "$d" # done' find-sh {} + # You may want to exclude paths that are beyond suspicion. The following code excludes /proc, /sys and /dev: # sudo find / -type d \( \ # \( -path /proc -prune \) -o \ # \( -path /sys -prune \) -o \ # \( -path /dev -prune \) -o \ # \( -exec sh -c ' # for d do # btrfs subvolume show "$d" >/dev/null 2>&1 && printf "%s\n" "$d" # done # ' find-sh {} + \) \)