'' usage() { echo "Usage: $0 []" >&2; exit 1; } [[ $# -ge 2 && $# -le 3 && ( $# -ne 3 || $(("$3" + 0)) = "$3" ) ]] || usage [[ $UID -eq 0 ]] || { echo "$0: requires root privileges" >&2; exit 1; } SOURCE="$1" TARGET="$2" [ $# -ge 3 ] && UP_TO="$3" || UP_TO= get_snapshots() { FILTER="\$1 > 0" [[ -n "$UP_TO" ]] && FILTER="\$1 > 0 && \$1 <= $UP_TO" find "$1" -follow -maxdepth 1 -printf '%f\n' \ | sed 's@/$@@g' | awk "$FILTER { print }" | sort -n } clone() { if [[ -n "$2" ]]; then echo "Cloning $1 (incremental from $2)..." >&2 PARENT_ARG="-p$SOURCE/$2/snapshot" else echo "Cloning $1 (non-incremental)..." >&2 PARENT_ARG="--" fi FROM="$SOURCE/$1" INTO="$TARGET/$1" [[ -d "$INTO" ]] || mkdir "$INTO" btrfs send "$PARENT_ARG" "$FROM/snapshot" | pv | btrfs receive "$INTO" cp -a "$FROM/info.xml" "$INTO/" } cleanup() { echo "Removing $1..." >&2 AT="$TARGET/$1" [[ -d "$AT/snapshot" ]] && btrfs subvolume delete "$AT/snapshot" rm -f "$AT/info.xml" rmdir "$AT" } while true; do ls -- "$SOURCE/" "$TARGET/" >/dev/null mapfile -t SNAPSHOTS < <(get_snapshots "$SOURCE") [[ ''${#SNAPSHOTS} -eq 0 ]] && { echo "$0: nothing to do" >&2; exit; } mapfile -t EXISTENT < <(get_snapshots "$TARGET") RESET=0 PARENT= ALL_ARE_BAD=0 for SNAPSHOT in "''${SNAPSHOTS[@]}"; do if [[ ! -e "$SOURCE/$SNAPSHOT" ]]; then echo "Notice: resetting listings due to time lapse" >&2 RESET=1 break elif [[ ! -s "$SOURCE/$SNAPSHOT/info.xml" ]]; then echo "Warning: skipping source $SNAPSHOT due to invalid info.xml" >&2 continue fi CLONE=0 if [[ ! -e "$TARGET/$SNAPSHOT" ]]; then CLONE=1 elif [[ $ALL_ARE_BAD -eq 1 || ! -f "$TARGET/$SNAPSHOT/info.xml" ]]; then echo -n "Warning: bad copy $SNAPSHOT (" >&2 [[ $ALL_ARE_BAD -eq 1 ]] && echo -n "bad chain" >&2 || echo -n "incomplete" >&2 echo "), recreating..." >&2 cleanup "$SNAPSHOT" CLONE=1 ALL_ARE_BAD=1 fi if [[ $CLONE -eq 1 ]]; then clone "$SNAPSHOT" "$PARENT" elif [[ -z "$PARENT" ]]; then echo "Initial snapshot present, all copies will be incremental" >&2 fi chmod 750 "$TARGET/$SNAPSHOT" PARENT="$SNAPSHOT" done [[ $RESET -eq 1 ]] && continue echo "Copies updated, cleaning up..." >&2 for SNAPSHOT in "''${EXISTENT[@]}"; do FOUND="" for KEEP in "''${SNAPSHOTS[@]}"; do if [ "$KEEP" = "$SNAPSHOT" ]; then FOUND=1 break fi done [ -n "$FOUND" ] || cleanup "$SNAPSHOT" done break done echo "Done" >&2 ''