212 lines
5.1 KiB
Bash
Executable File
212 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
|
|
## ## ## ## ## ## ## ## ## ## ## ## ## ##
|
|
## ##
|
|
## btrfs-snapshots.sh ##
|
|
## ##
|
|
## Take read-only snapshots of BTRFS ##
|
|
## subvolumes ##
|
|
## ##
|
|
## ## ## ## ## ## ## ## ## ## ## ## ## ##
|
|
|
|
|
|
##
|
|
## Information
|
|
## (don't touch unless you are me)
|
|
##
|
|
|
|
NAME="btrfs-snapshots.sh"
|
|
VERSION="0.2.0"
|
|
AUTHOR="david@socialnerds.org"
|
|
LICENSE="MIT"
|
|
DESCRIPTION="Take read-only snapshots of BTRFS subvolumes."
|
|
WEBSITE="https://git.socialnerds.org/david/scripts"
|
|
CHANGELOG=("[2023-10-26][v0.2.0] Complete rewrite"
|
|
"[2021-01-01][v0.1.0] Initial version")
|
|
|
|
|
|
##
|
|
## Configuration
|
|
##
|
|
|
|
EXECUTABLE="$(basename $0)"
|
|
LIBRARIES="lib.sh"
|
|
DEPENDENCIES="basename dirname readlink ln mkdir rm ls date btrfs"
|
|
REQUIRE_ROOT=1
|
|
|
|
SNAP_FOLDER=".snapshots"
|
|
SNAP_PREFIX="snapshot-"
|
|
SNAP_TIMESTAMP=$(date +%Y%m%d%H%M) #add %S if you want sub-minute snapshots
|
|
SNAP_NAME="$SNAP_PREFIX$SNAP_TIMESTAMP"
|
|
|
|
# Build the Btrfs command
|
|
BTRFS_BINARY="$(command -v btrfs)"
|
|
BTRFS_OPTIONS="-q"
|
|
BTRFS_COMMAND="$BTRFS_BINARY $BTRFS_OPTIONS"
|
|
|
|
# How many snapshots should be kept?
|
|
# Can be overridden with an option flag (-s)
|
|
SNAPSHOTS=128
|
|
|
|
|
|
##
|
|
## Functions
|
|
##
|
|
|
|
# Print help information
|
|
function print_help() {
|
|
printf "%s\n\n%s\n%b\n\n%s\n %-21s %s\n %-21s %s\n %-21s %s\n %-21s %s\n %-21s %s\n" \
|
|
"$DESCRIPTION" "Usage:" "$LIB_BOLD$EXECUTABLE <options> <subvolume>$LIB_CLEAR" "Options:" \
|
|
"-s, --snapshots <int>" "Override how many snapshots to keep (default: 128)" \
|
|
"-h, --help" "Print help screen and exit" \
|
|
"-i, --info" "Print script information and exit" \
|
|
"-v, --verbose" "More verbose output" \
|
|
"-q, --quiet" "No output except errors (overrides -v)"
|
|
}
|
|
|
|
# Verify that input is a Btrfs subvolume
|
|
function is_subvolume() {
|
|
if ! $($BTRFS_COMMAND subvolume show $1 >/dev/null 2>&1); then
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
##
|
|
## Preflight
|
|
##
|
|
|
|
# Load BASH libraries
|
|
SCRIPT_PATH=$(readlink -f "$0")
|
|
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
|
for LIBRARY in $LIBRARIES; do
|
|
if [[ -r "$SCRIPT_DIR/$LIBRARY" ]]; then
|
|
#echo "Loading library file [$SCRIPT_DIR/$LIBRARY]"
|
|
source "$SCRIPT_DIR/$LIBRARY"
|
|
else
|
|
echo "Error: Cannot load library file [$SCRIPT_DIR/$LIBRARY]"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Check for root privileges
|
|
if ! lib_amiroot && [[ $REQUIRE_ROOT -eq 1 ]]; then
|
|
lib_print "!You need to have root privileges"
|
|
exit 1
|
|
fi
|
|
|
|
# Check for dependencies
|
|
MISSING_COMMANDS=$(lib_missing_commands $DEPENDENCIES)
|
|
if [[ -n "$MISSING_COMMANDS" ]]; then
|
|
lib_print "!One or more commands missing [$MISSING_COMMANDS]"
|
|
lib_print "Try installing them with your package manager"
|
|
exit 1
|
|
fi
|
|
|
|
|
|
##
|
|
## Liftoff
|
|
##
|
|
|
|
while [[ "$1" =~ ^- && ! "$1" == "--" ]]; do
|
|
case $1 in
|
|
-s|--snapshots)
|
|
shift
|
|
if lib_is_int $1; then
|
|
SNAPSHOTS=$1
|
|
else
|
|
lib_print "!Input for --snapshots must be an intager"
|
|
exit 1
|
|
fi
|
|
;;
|
|
-h|--help)
|
|
H=1
|
|
;;
|
|
-i|--info)
|
|
I=1
|
|
;;
|
|
-v|--verbose)
|
|
V=1
|
|
;;
|
|
-q|--quiet)
|
|
Q=1
|
|
;;
|
|
*)
|
|
lib_print "!Unknown option [$1]"
|
|
lib_print "Try --help or -h for available options"
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ "$1" == '--' ]]; then
|
|
shift
|
|
fi
|
|
|
|
if [[ $I -eq 1 ]]; then
|
|
lib_print_info
|
|
exit 0
|
|
elif [[ $H -eq 1 ]]; then
|
|
print_help
|
|
exit 0
|
|
fi
|
|
|
|
if [[ -z "$1" ]]; then
|
|
print_help
|
|
exit 0
|
|
else
|
|
if is_subvolume "$1"; then
|
|
SNAP_FOLDER="${1%/}/$SNAP_FOLDER"
|
|
# Create $SNAP_FOLDER if it does not exist
|
|
if [[ ! -d "$SNAP_FOLDER" ]]; then
|
|
lib_print "?Creating snapshot folder because it does not yet exist [$SNAP_FOLDER]"
|
|
mkdir "$SNAP_FOLDER"
|
|
fi
|
|
if [[ -d "$SNAP_FOLDER/$SNAP_NAME" ]]; then
|
|
lib_print "?Skipping snapshot because it already exists. [$SNAP_FOLDER/$SNAP_NAME]"
|
|
else
|
|
# Take the snapshot
|
|
#TODO: handle failed snapshot creations
|
|
if $($BTRFS_COMMAND subvolume snapshot -r "$1" "$SNAP_FOLDER/$SNAP_NAME"); then
|
|
lib_print "Created new snapshot [$SNAP_FOLDER/$SNAP_NAME]"
|
|
else
|
|
lib_print "!Error occured while creating new snapshot [$SNAP_FOLDER/$SNAP_NAME]"
|
|
exit 1
|
|
fi
|
|
if [ -h "$SNAP_FOLDER/latest" ]; then
|
|
rm "$SNAP_FOLDER/latest"
|
|
fi
|
|
ln -sf "$SNAP_FOLDER/$SNAP_NAME" "$SNAP_FOLDER/latest"
|
|
lib_print "?Relinked latest to new snapshot [$SNAP_FOLDER/$SNAP_NAME]"
|
|
# Delete old snapshots
|
|
SNAPS=($(ls -r $SNAP_FOLDER | grep $SNAP_PREFIX))
|
|
lib_print "?Snapshot retention is set to: $SNAPSHOTS"
|
|
lib_print "?Existing snapshots found: ${#SNAPS[@]}"
|
|
i=0
|
|
for SNAP in ${SNAPS[@]}; do
|
|
if [[ $i -ge $SNAPSHOTS ]]; then
|
|
if $($BTRFS_COMMAND subvolume delete "$SNAP_FOLDER/$SNAP"); then
|
|
lib_print "Deleted old snapshot [$SNAP_FOLDER/$SNAP]"
|
|
else
|
|
lib_print "!Error occured while deleting old snapshot [$SNAP_FOLDER/$SNAP]"
|
|
exit 1
|
|
fi
|
|
fi
|
|
i=$((i+1))
|
|
done
|
|
fi
|
|
else
|
|
lib_print "!Input does not appear to be a Btrfs subvolume [$1]"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
|
|
##
|
|
## Here be dragons
|
|
##
|
|
|
|
exit 0
|