#!/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 $LIB_CLEAR" "Options:" \ "-s, --snapshots " "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