Skip to content

Instantly share code, notes, and snippets.

@neon12345
Last active September 20, 2023 14:30
Show Gist options
  • Save neon12345/4d3b1249196c56434336269af96178ce to your computer and use it in GitHub Desktop.
Save neon12345/4d3b1249196c56434336269af96178ce to your computer and use it in GitHub Desktop.
Manage a Btrfs snapshot history chain as visible files for SnapRAID
#!/bin/bash
# usage: ./backup.sh btrfs_root_path
# requirements:
# 1. $BACKUP_DIR must not exist in the btrfs_root_path
# 2. current, new, top, old must not exist in the btrfs_root_path
# how it works:
# The script creates a single btrfs snapshot "new" per invocation and btrfs sends it
# to the $BACKUP_DIR with a "current" snapshot as parent.
# Thus the btrfs send is incremental with either an initial "current" snapshot
# or a "current" snapshot from the previous invocation.
# Once the send is done, the "current" snapshot is removed and "new" is
# renamed into "current". This means only the latest "current" snapshot
# remains and the btrfs streams stored in the $BACKUP_DIR can be used
# to walk down the snapshot history with brtfs receive.
# This history walk is also used to remove a snapshot file in the middle of the chain.
# (This can be improved if there is a way to merge stream files)
# The snapshot cleanup algorithm keeps a user defined number of snapshots roughly
# at the boundary of day, week, month and year distance to the next invocation.
# why?:
# This could be used with SnapRAID to make btrfs snapshots visible and protected.
# (SnapRAID is not able to read subvolumes/snapshots and if this is added in the future,
# the snapshots are probably identified as additional files/"disk")
# state:
# experimental
set -o pipefail
BTRFS_TARGET=$1
# user options
BACKUP_DIR="backup_snapshots"
KEEP_DAY=0
KEEP_WEEK=2
KEEP_MONTH=2
KEEP_YEAR=2
SECONDS_YEAR=31536000
SECONDS_MONTH=2629800
SECONDS_WEEK=604800
SECONDS_DAY=86400
CURRENT_HOUR=$(expr $(date "+%s") + 0)
CURRENT_DAY=$(expr $(date "+%d") + 0)
CURRENT_WEEK=$(expr $(date "+%V") + 0)
CURRENT_MONTH=$(expr $(date "+%m") + 0)
CURRENT_YEAR=$(expr $(date "+%Y") + 0)
FILE="$CURRENT_HOUR-$CURRENT_DAY-$CURRENT_MONTH-$CURRENT_YEAR-$CURRENT_WEEK.inc.backup"
err() { echo "$@" 1>&2; }
sscanf() {
local str="$1"
if [[ "$str" =~ ^([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+).inc.backup$ ]]; then
HOUR=${BASH_REMATCH[1]}
DAY=${BASH_REMATCH[2]}
MONTH=${BASH_REMATCH[3]}
YEAR=${BASH_REMATCH[4]}
WEEK=${BASH_REMATCH[5]}
return
fi
false
}
NUM_DAY=0
NUM_WEEK=0
NUM_MONTH=0
NUM_YEAR=0
NUM_FILES=0
CURRENT="$BTRFS_TARGET/current"
NEW="$BTRFS_TARGET/new"
TOP="$BTRFS_TARGET/top"
OLD="$BTRFS_TARGET/old"
OUT_FILE="$BTRFS_TARGET/$BACKUP_DIR/$FILE"
rem_file() {
local RFILE="$1"
local TARGET=""
local THE_FILE=""
echo "remove: $RFILE"
mv "$CURRENT" "$TOP"
while IFS= read -r -d $'\0' file; do
THE_FILE=$(basename $file)
if sscanf "$THE_FILE"; then
cat "$BTRFS_TARGET/$BACKUP_DIR/$THE_FILE" | btrfs -q receive "$BTRFS_TARGET"
if [[ ! $? -eq 0 ]]; then
err "btrfs receive failed"
break
fi
if [[ -n "$TARGET" ]]; then
break
fi
if [[ "$THE_FILE" == "$RFILE" ]]; then
if [[ -d "$OLD" ]]; then
TARGET="$OLD"
else
TARGET="$TOP" # it is the first file in the chain
fi
mv "$CURRENT" "$NEW"
else
if [[ -d "$OLD" ]]; then
btrfs -q subvolume delete "$OLD"
fi
mv "$CURRENT" "$OLD"
fi
fi
done < <(find "$BTRFS_TARGET/$BACKUP_DIR" -maxdepth 1 -type f -print0 | sort -rVz)
handle() {
local TARGET="$1"
local THE_FILE="$2"
if [[ -n "$TARGET" ]]; then
btrfs -q subvolume delete "$NEW" 2>/dev/null
# current is the first file after the file to delete or none if it is the last
# target is the file before or top if it is the first
if [[ -d "$CURRENT" ]]; then # else it is the last file in the chain
btrfs -q send -p "$TARGET" "$CURRENT" > "$BTRFS_TARGET/$BACKUP_DIR/$THE_FILE.new"
if [[ ! $? -eq 0 ]]; then
err "btrfs send failed"
rm -f "$BTRFS_TARGET/$BACKUP_DIR/$THE_FILE.new"
return
fi
cp "$BTRFS_TARGET/$BACKUP_DIR/$THE_FILE.new" "$BTRFS_TARGET/$BACKUP_DIR/$THE_FILE"
rm "$BTRFS_TARGET/$BACKUP_DIR/$THE_FILE.new"
fi
rm "$BTRFS_TARGET/$BACKUP_DIR/$RFILE"
fi
}
handle "$TARGET" "$THE_FILE"
btrfs -q subvolume delete "$OLD" 2>/dev/null
btrfs -q subvolume delete "$CURRENT" 2>/dev/null
mv "$TOP" "$CURRENT"
}
if [[ -f "$OUT_FILE" ]]; then
err "$FILE exists, try later."
exit 1
fi
if [[ ! -d "$CURRENT" ]]; then
btrfs -q subvolume snapshot "$BTRFS_TARGET" "$CURRENT" || exit 1
rm -rf "$CURRENT/$BACKUP_DIR"
btrfs -q property set -ts "$CURRENT" ro true
fi
btrfs -q subvolume snapshot "$BTRFS_TARGET" "$NEW" || exit 1
mkdir -p "$BTRFS_TARGET/$BACKUP_DIR"
rm -rf "$NEW/$BACKUP_DIR"
btrfs -q property set -ts "$NEW" ro true
btrfs -q send -p "$NEW" "$CURRENT" > "$OUT_FILE"
if [[ ! $? -eq 0 ]]; then
btrfs -q subvolume delete "$NEW"
err "btrfs send failed"
rm -f "$OUT_FILE"
exit 1
fi
btrfs -q subvolume delete "$CURRENT" || exit 1
mv "$NEW" "$CURRENT" || exit 1
# FIXME: with the debian package python3-btrfs and set_received_uuid.py from
# https://github.com/knorrie/python-btrfs/blob/master/examples/set_received_uuid.py
# it is possible to set the received UUID of a new subvolume to the UUID of the
# top current snapshot as a starting point for recovery. This will prevent the sanity
# check and recreation for the current top snapshot on the same disk seems to work.
# Need to test this on a clean disk.
btrfs -q subvolume show "$CURRENT" > "$BTRFS_TARGET/$BACKUP_DIR/current.backup"
while true; do
# we start with the oldest
find "$BTRFS_TARGET/$BACKUP_DIR" -maxdepth 1 -type f -print0 | sort -Vz | while IFS= read -r -d $'\0' file; do
THE_FILE=$(basename $file)
if sscanf "$THE_FILE"; then
NUM_FILES=$(( NUM_FILES + 1 ))
DIST=$(( CURRENT_HOUR - HOUR ))
#echo "dist: $(( DIST / SECONDS_DAY )) $THE_FILE"
if (( DIST < SECONDS_YEAR * 2 )); then
if (( DIST >= SECONDS_YEAR )) && (( KEEP_YEAR != 0)); then
if(( NUM_YEAR < KEEP_YEAR )); then
NUM_YEAR=$(( NUM_YEAR + 1 ))
continue
fi
elif (( DIST >= SECONDS_MONTH)) && (( KEEP_MONTH != 0)); then
if (( NUM_MONTH < KEEP_MONTH )); then
NUM_MONTH=$(( NUM_MONTH + 1 ))
continue
fi
elif (( DIST >= SECONDS_WEEK )) && (( KEEP_WEEK != 0)); then
if (( NUM_WEEK < KEEP_WEEK )); then
NUM_WEEK=$(( NUM_WEEK + 1 ))
continue
fi
elif (( DIST >= SECONDS_DAY )) && (( KEEP_DAY != 0)); then
if (( NUM_DAY < KEEP_DAY )); then
NUM_DAY=$(( NUM_DAY + 1 ))
continue
fi
else
if(( NUM_YEAR < KEEP_YEAR )); then
NUM_YEAR=$(( NUM_YEAR + 1 ))
continue
fi
if (( NUM_MONTH < KEEP_MONTH )); then
NUM_MONTH=$(( NUM_MONTH + 1 ))
continue
fi
if (( NUM_WEEK < KEEP_WEEK )); then
NUM_WEEK=$(( NUM_WEEK + 1 ))
continue
fi
if (( NUM_DAY < KEEP_DAY )); then
NUM_DAY=$(( NUM_DAY + 1 ))
continue
fi
fi
fi
NUM_FILES=$(( NUM_FILES - 1 ))
rem_file "$THE_FILE"
break
fi
done
if (( NUM_FILES <= KEEP_YEAR + KEEP_MONTH + KEEP_WEEK + KEEP_DAY )); then
break
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment