Last active
September 20, 2023 14:30
-
-
Save neon12345/4d3b1249196c56434336269af96178ce to your computer and use it in GitHub Desktop.
Manage a Btrfs snapshot history chain as visible files for SnapRAID
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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