diff --git a/lib.sh b/lib.sh new file mode 100644 index 0000000..c16fc32 --- /dev/null +++ b/lib.sh @@ -0,0 +1,134 @@ +# This file is meant to be sourced by another script, +# it contains only BASH functions and global variables. + + + ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## + ## ## + ## lib.sh ## + ## ## + ## Library for my personal BASH scripts ## + ## ## + ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## + + +## +## Information +## + +LIB_NAME="lib.sh" +LIB_DESCRIPTION="Library for my personal BASH scripts" +LIB_VERSION="0.1.0" +LIB_AUTHOR="david@socialnerds.org" +LIB_LICENSE="MIT" +LIB_WEBSITE="https://git.socialnerds.org/david/scripts" + + +## +## Variables +## + +# Reset text formatting +LIB_CLEAR="\e[0m" + +# Text settings +LIB_BOLD="\e[1m" +LIB_UNDERLINE="\e[4m" + +# Text color +LIB_RED="\e[31m" +LIB_GREEN="\e[32m" +LIB_YELLOW="\e[33m" +LIB_BLUE="\e[34m" +LIB_MAGENTA="\e[35m" +LIB_CYAN="\e[36m" +LIB_LIGHTGREY="\e[37m" + +# Text color with bold font +LIB_RED_BOLD="\e[1;31m" +LIB_GREEN_BOLD="\e[1;32m" +LIB_YELLOW_BOLD="\e[1;33m" +LIB_BLUE_BOLD="\e[1;34m" +LIB_MAGENTA_BOLD="\e[1;35m" +LIB_CYAN_BOLD="\e[1;36m" +LIB_LIGHTGREY_BOLD="\e[1;37m" + +# Background color +LIB_RED_BG="\e[41m" +LIB_GREEN_BG="\e[42m" +LIB_YELLOW_BG="\e[43m" +LIB_BLUE_BG="\e[44m" +LIB_MAGENTA_BG="\e[45m" +LIB_CYAN_BG="\e[46m" +LIB_LIGHTGREY_BG="\e[47m" + +# Background color with bold font +LIB_RED_BG_BOLD="\e[1;41m" +LIB_GREEN_BG_BOLD="\e[1;42m" +LIB_YELLOW_BG_BOLD="\e[1;43m" +LIB_BLUE_BG_BOLD="\e[1;44m" +LIB_MAGENTA_BG_BOLD="\e[1;45m" +LIB_CYAN_BG_BOLD="\e[1;46m" +LIB_LIGHTGREY_BG_BOLD="\e[1;47m" + + +## +## Functions +## + +# Print text message +function lib_print() { + local LIB_Q=${Q:-0} + local LIB_V=${V:-0} + if [[ "$1" =~ ^\! ]]; then + printf "$LIB_RED_BG_BOLD%b$LIB_CLEAR\n" "${1#\!}" + elif [[ "$1" =~ ^\? ]]; then + if [ $LIB_V -eq 1 ] && [ $LIB_Q -ne 1 ]; then + printf "$LIB_LIGHTGREY%b$LIB_CLEAR\n" "${1#\?}" + fi + elif [[ $LIB_Q -ne 1 ]]; then + printf "%b\n" "$1" + fi +} + +# Print script information +# including the last 5 changelog entries +function lib_print_info() { + printf "%12s %b\n%12s %s\n%12s %s\n%12s %s\n%12s %s\n%12s %s\n\n%12s %s\n" \ + "Name:" "$LIB_BOLD$NAME$LIB_CLEAR" "Version:" "$VERSION" \ + "Author:" "$AUTHOR" "License:" "$LICENSE" "Web:" "$WEBSITE" \ + "Description:" "$DESCRIPTION" "Changelog:" "${CHANGELOG[0]}" + for i in "${CHANGELOG[@]:1:4}"; do + printf "%12s %s\n" "" "$i" + done +} + +# Print version information +function lib_print_version() { + printf "%s\n" "$VERSION" +} + +# Check for privileges +# Returns 0 if the current user is root and 1 if not +function lib_amiroot() { + if [[ $(whoami) != "root" ]]; then + return 1 + fi +} + +# Check for command availability +# Takes one or a list of space seperated commands. Returns a list of missing commands. +function lib_missing_commands() { + local MISSING_COMMANDS="" + for COMMAND in $@; do + if ! $(command -v $COMMAND &>/dev/null); then + MISSING_COMMANDS="$MISSING_COMMANDS $COMMAND" + fi + done + echo "${MISSING_COMMANDS# }" +} + +# Generate a random alphanumeric string +# Optional: Supply length as intager. Default is 16. +function lib_gen_string() { + LC_ALL=C tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c ${1:-16}; echo +} diff --git a/restic-backup.sh b/restic-backup.sh new file mode 100755 index 0000000..00cce4e --- /dev/null +++ b/restic-backup.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash + + + ## ## ## ## ## ## ## ## ## ## ## ## ## ## + ## ## + ## restic-backup.sh ## + ## ## + ## Simple script to help setup and run ## + ## periodic Restic backup jobs ## + ## ## + ## ## ## ## ## ## ## ## ## ## ## ## ## ## + + +## +## Information +## + +NAME="restic-backup.sh" +VERSION="0.2.0" +AUTHOR="david@socialnerds.org" +LICENSE="MIT" +DESCRIPTION="A simple script to help setup and run periodic Restic backup jobs." +WEBSITE="https://git.socialnerds.org/david/scripts" +CHANGELOG=("[2023-10-26][v0.2.0] Complete rewrite" + "[2022-01-01][v0.1.0] Initial version") + +## +## Configuration +## + +EXECUTABLE="$(basename $0)" +LIBRARIES="lib.sh" +DEPENDENCIES="basename dirname readlink tr head chmod curl restic" +REQUIRE_ROOT=0 + +# Files +PASSWORD="$HOME/.restic-password" +REPOSITORY="$HOME/.restic-repository" +HEALTHCHECKS="$HOME/.restic-healthchecks" + +# Build the Restic command +RESTIC_BINARY="$(command -v restic)" +RESTIC_OPTIONS="-q" +RESTIC_COMMAND="$RESTIC_BINARY --password-file $PASSWORD --repository-file $REPOSITORY $RESTIC_OPTIONS" + +# How many snapshots should be kept? +SNAPSHOTS=32 + + +## +## Functions +## + +# Print help information +function print_help() { + printf "%s\n\n%s\n%b\n\n%s\n %-15s %s\n %-15s %s\n %-15s %s\n %-15s %s\n %-15s %s\n" \ + "$DESCRIPTION" "Usage:" "$LIB_BOLD$EXECUTABLE $LIB_CLEAR" "Options:" \ + "-c, --checks" "Enable Healthchecks" \ + "-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)" +} + + +## +## 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 + -c|--checks) + C=1 + ;; + -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 + # Generate a new password file if it is missing or empty + if [[ ! -s $PASSWORD ]]; then + lib_gen_string 32 > $PASSWORD + chmod 600 $PASSWORD + lib_print "New password file generated since it did not exist or was empty [$PASSWORD]" + fi + + # Generate a new repo file if it is missing or empty + if [[ ! -s $REPOSITORY ]]; then + echo -n "Repository URL: "; read -r INPUT + echo $INPUT > $REPOSITORY + chmod 600 $REPOSITORY + lib_print "New repository file generated since it did not exist or was empty [$REPOSITORY]" + fi + + # Generate a new healthchecks file if it is missing or empty + if [[ ! -s $HEALTHCHECKS ]] && [[ $C -eq 1 ]]; then + echo -n "Healthchecks URL: "; read -r INPUT + if [[ -n "$INPUT" ]]; then + echo "$INPUT" > $HEALTHCHECKS + chmod 600 $HEALTHCHECKS + lib_print "New healthchecks file generated [$HEALTHCHECKS]" + fi + fi + + # Initialize a new repo if the URL has no config file + if ! $($RESTIC_COMMAND snapshots >/dev/null 2>&1); then + if ! $($RESTIC_COMMAND init >/dev/null 2>&1); then + lib_print "!A problem occured while initializing a new repository" + exit 1 + fi + lib_print "New repository initialized" + fi + + # Run the Restic repository check + if ! $($RESTIC_COMMAND check >/dev/null 2>&1); then + lib_print "!Repository check failed" + exit 1 + fi + + if [[ "$1" =~ ^/ && -r "$1" ]]; then + # Signal Healthchecks start if enabled + if [[ "$C" -eq 1 ]]; then + HEALTHCHECKS_URL=$(cat $HEALTHCHECKS) + curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECKS_URL/start + fi + # Run the actual backup + if ! $($RESTIC_COMMAND backup --one-file-system ${@:2} "$1" >/dev/null 2>&1); then + lib_print "!Something went wrong while running backup [$1]" + # Signal Healthchecks fail if enabled + if [[ "$C" -eq 1 ]]; then + curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECKS_URL/fail >/dev/null 2>&1 + fi + exit 1 + fi + # Delete old snapshots + if ! $($RESTIC_COMMAND forget --keep-last $SNAPSHOTS --path "$1" --prune >/dev/null 2>&1); then + lib_print "!Something went wrong while deleting old snapshots [$1]" + # Signal Healthchecks fail if enabled + if [[ "$C" -eq 1 ]]; then + curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECKS_URL/fail >/dev/null 2>&1 + fi + exit 1 + fi + # Signal Healthchecks success if enabled + if [[ "$C" -eq 1 ]]; then + curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECKS_URL >/dev/null 2>&1 + fi + elif [[ "$1" =~ ^(backup|cache|cat|check|copy|diff|dump|find|forget|generate|help|init|key|list|ls|migrate|mount|prune|recover|repair|restore|rewrite|snapshots|stats|tag|unlock|version)$ ]]; then + # Wrapper mode + $RESTIC_COMMAND $@ + else + lib_print "!The given path must be absolute and readable [$1]" + lib_print "Or you can try a Restic keyword directly for wrapper mode" + exit 1 + fi +fi + + +## +## Here be dragons +## + +exit 0