#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' if [ -r /etc/crypted-backups ]; then . /etc/crypted-backups else echo "Configuration file (/etc/crypted-backups) not readable! Aborting!" exit 1 fi user_mode="" source_mode="" cleanup_mode=0 verbose=0 function notification_source_to_destination () { local source_directory=$1 local destination_directory=$2 if [ $source_directory != "" -a $destination_directory != "" ];then echo "$source_directory -> $destination_directory" return 0 else echo "Source ($source_directory) or destination ($destination_directory) directory not given!" return 1 fi } function generate_timestamp () { local timestamp="$(date +"%Y%m%d-%H%M%S")-" echo ${timestamp} return 0 } # Adds trailing slash, if missing # Fails when path is not absolute, or not within user home, when not root function sanitize_pathname () { local path=$1 if [ ${#path} -ne 0 ]; then if [ ${path:0:1} = "/" -o ${path:0:2} = "~/" -a $(id -u) -ne 0 ]; then if [ ${path:${#path}-1} != "/" ]; then echo "$path/" return 0 else echo "$path" return 0 fi else echo "Directory must be absolute!" return 1 fi else echo "Directory can not be empty!" return 1 fi } function check_cleanup_settings () { [ $verbose -gt 0 ] && echo "Checking cleanup settings." if ![[ $cleanup_older_than =~ '^[0-9]+$' ]];then echo "Error! \"cleanup_older_than\" is not an integer: $cleanup_older_than." return 1 elif [ ${#cleanup_directory} -eq 0 ];then echo "Error! \"cleanup_directory\" is not set!" return 1 fi } function check_gpg_set () { [ $verbose -gt 0 ] && echo "Checking, if \"gpg_public_key\" is set." if [ -z "$gpg_public_key" ];then echo "Error. \"gpg_public_key\" not set!" exit 1 fi } function check_database_server () { [ $verbose -gt 0 ] && echo "Checking database server." if [ ! -x /usr/bin/mysql ]; then echo "/usr/bin/mysql is not available. Is MariaDB or MySQL actually installed?" return 1 elif [ !$(systemctl is-active mysqld) = "active" ]; then echo "No MariaDB or MySQL service is currently running. Start it with 'systemctl start mysqld'." return 1 fi } function check_database_settings () { [ $verbose -gt 0 ] && echo "Checking database settings." if [ -z "$database_destination" ]; then echo "The \"database_destination\" variable can not be empty." return 1 elif [ -z "$database_user" ]; then echo "The \"database_user\" variable can not be empty." return 1 elif [ -z "$database_password" ]; then echo "The \"database_password\" variable can not be empty." return 1 fi return 0 } function check_directory_exists () { local directory=$1 [ $verbose -gt 0 ] && echo "Checking if directory exists: $directory" if [ ! -d $directory ]; then echo "Directory \"$directory\" does not exist!" return 1 else return 0 fi } function check_directory_exists_and_autocreate () { local directory=$1 [ $verbose -gt 0 ] && echo "Checking if directory exists and creating it, if it doesn't: $directory" if [ ! -d $directory ]; then echo "Directory \"$directory\" does not exist yet. Creating..." mkdir -p $directory else return 0 fi } function check_directory_permission_root () { local directory=$1 [ $verbose -gt 0 ] && echo "Checking root's permission on directory: $directory" if [ -w $directory ]; then return 0 else echo "Directory not writable: $directory." return 1 fi } function check_directory_writable_user () { local directory=$1 [ $verbose -gt 0 ] && echo "Checking the user's permission on directory: $directory" if [ -w $directory ]; then return 0 else echo "Directory not writable: $directory." return 1 fi } function check_directory_owner_user() { local directory=$1 [ $verbose -gt 0 ] && echo "Checking ownership of directory ($directory) by user $(whoami)." if [ ! -O $directory ]; then echo "Directory not owned by user $(whoami): $directory" return 1 else return 0 fi } function check_user_directory () { local directory=$1 [ $verbose -gt 0 ] && echo "Checking directory \"$directory\"." check_directory_exists_and_autocreate $directory check_directory_writable_user $directory return 0 } function check_root_directory () { local directory=$1 [ $verbose -gt 0 ] && echo "Checking directory \"$directory\"." check_directory_exists_and_autocreate $directory check_directory_permission_root $directory return 0 } function get_parent_directory () { local directory=$1 local parent="" parent=$(dirname $directory) parent=$(sanitize_pathname $parent) echo "$parent" return 0 } function get_basename_directory () { local directory=$1 local base=$(basename $directory) echo "$base" return 0 } function compress_to_tmp_file () { local source_file=$1 local tmp_file=$2 [ $verbose -gt 0 ] && echo "Compressing source ($source_file) to temporary file ($tmp_file)." case $tar_suffix in ".tar.tbz") tar cfj "$tmp_file" $source_file ;; ".tar.tgz") tar cfz "$tmp_file" $source_file ;; ".tar.tlz") tar --lzma -cf "$tmp_file" $source_file ;; ".tar.xz") tar cfJ "$tmp_file" $source_file ;; *) echo "Using \"$tar_suffix\" as \$tar_suffix is not supported." return 1 ;; esac return 0 } function encrypt_tmp_file () { local tmp_file=$1 local destination_file=$2 local encrypt_return=-1 [ $verbose -gt 0 ] && echo "Encrypting $tmp_file to $destination_file." set +eu gpg -e \ -r "$gpg_public_key" \ -o "$destination_file" \ "$tmp_file" encrypt_return=$? set -eu if [ $encrypt_return -gt 0 ];then echo "GnuPG encryption returns with: $encrypt_return" [ $verbose -gt 0 ] && echo "Removing $tmp_file." rm -f "$tmp_file" return 1 fi [ $verbose -gt 0 ] && echo "Removing $tmp_file." rm -f "$tmp_file" return 0 } function backup_single_directory () { local source_directory=$1 local source_directory_basename=$(get_basename_directory $source_directory) local source_parent_directory=$(get_parent_directory $source_directory) local tmp_directory=$(sanitize_pathname $tmp) local destination_directory=$(sanitize_pathname $2) local timestamp=$(generate_timestamp) local tmp_file="$tmp_directory$timestamp$source_directory_basename$tar_suffix" local destination_file="$destination_directory$timestamp$source_directory_basename$tar_suffix$gpg_suffix" notification_source_to_destination "$source_parent_directory$source_directory_basename" $destination_file [ $verbose -gt 0 ] && echo "Going to $source_directory_basename's parent directory: $source_parent_directory." cd $source_parent_directory check_directory_exists $source_directory "check_"$user_mode"_directory" $tmp_directory "check_"$user_mode"_directory" $destination_directory compress_to_tmp_file $source_directory_basename $tmp_file encrypt_tmp_file $tmp_file $destination_file } function backup_multiple_directories () { local source_directory=$(sanitize_pathname $1) local destination_directory=$(sanitize_pathname $2) local layered=0 local sub_count=0 if [ ${#@} -gt 2 ]; then layered=$3 [ $verbose -gt 0 ] && echo "Recursive backup (depth=1) of \"$source_directory\"." fi check_directory_exists $source_directory "check_"$user_mode"_directory" $destination_directory for sub_directory in $source_directory* do if [ -d $sub_directory ];then sub_count=$((sub_count+1)) "check_"$user_mode"_directory" $sub_directory if [ $layered -eq 1 ];then backup_multiple_directories $sub_directory $destination_directory$(get_basename_directory $sub_directory) else backup_single_directory $sub_directory $destination_directory fi fi done if [ $sub_count -eq 0 ]; then echo "There are actually no folders to backup in \"$source_directory\". Please check the configuration file!" fi } function dump_database () { local db=$1 local tmp_file=$2 [ $verbose -gt 0 ] && echo "Dumping database $db to file $tmp_file." mysqldump --force \ --opt \ -u$database_user \ -p$database_password \ --databases $db > $tmp_file } function backup_all_databases () { local databases=( ) local destination=$(sanitize_pathname $database_destination) local database="" check_database_server check_database_settings [ $verbose -gt 0 ] && echo "Backing up all available databases." set +eu databases=$(mysql -u$database_user \ -p$database_password \ -e "SHOW DATABASES;" \ | grep -Ev "(Database|information_schema|performance_schema|tmp)") set -eu if [ ${#databases} -eq 0 ];then echo "There are actually no databases on this server. If you've set wrong user or password variables MariaDB/ MySQL will by now have complained about it." return 1 else [ $verbose -gt 0 ] && echo "Following databases will be backed up: $( ${databases[@]} )" for database in $databases; do backup_database $database $destination done fi } function backup_database () { local db=$1 local destination_directory=$2 local timestamp=$(generate_timestamp) local tmp_directory=$(sanitize_pathname $tmp) local sql_file="$timestamp$db$sql_suffix" local tmp_file="$tmp_directory$sql_file$tar_suffix" local destination_file="$destination_directory$timestamp$db$sql_suffix$tar_suffix$gpg_suffix" "check_"$user_mode"_directory" $tmp_directory "check_"$user_mode"_directory" $destination_directory [ $verbose -gt 0 ] && echo "Backing up database $db." [ $verbose -gt 0 ] && echo "Going to temporary directory ($tmp_directory)." notification_source_to_destination "$db" $destination_file cd $tmp_directory dump_database $db $sql_file compress_to_tmp_file $sql_file $tmp_file encrypt_tmp_file $tmp_file $destination_file } function set_user_mode () { if [ $(id -u) -eq 0 ]; then user_mode="root" else user_mode="user" fi [ $verbose -gt 0 ] && echo "user_mode set to $user_mode." return 0 } function cleanup_backup () { local cleanup_directory=$(sanitize_pathname $cleanup_directory) check_cleanup_settings "check_"$user_mode"_directory" $cleanup_directory [ $verbose -gt 0 ] && echo "Cleaning up $cleanup_directory by removing files older than $cleanup_older_than days." find $cleanup_directory -type f -atime +$cleanup_older_than -print -exec rm {} \; return 0 } function check_mirror_settings () { if [ $mirror_host = "" -o $mirror_host = "127.0.0.1" -o $mirror_host = "localhost" ];then "check_"$user_mode"_directory" $mirror_source echo "local" return 0 else if [ $mirror_user = "" ];then echo "The \"mirror_user\" variable can not be empty if using a remote mirror!" return 1 fi echo "remote" return 0 fi } function mirror_backup () { local mirror_source=$(sanitize_pathname $mirror_source) local mirror_destination=$mirror_destination local mirror_type=$(check_mirror_settings) check_directory_exists $mirror_source if [ $mirror_type = "local" ];then rsync -r \ -t \ -p \ -o \ -g \ -v \ --progress\ --delete\ --ignore-existing\ --size-only\ -s \ --exclude 'lost+found' \ --exclude '.Trash-1000' \ --exclude '$RECYCLEBIN' \ --exclude 'System Volume Information' \ --exclude '.thumbs' \ $mirror_source \ $mirror_destination elif [ $mirror_type = "remote" ];then rsync -r \ -t \ -p \ -o \ -g \ -v \ --progress\ --delete\ --ignore-existing\ --size-only\ -s \ --exclude 'lost+found' \ --exclude '.Trash-1000' \ --exclude '$RECYCLEBIN' \ --exclude 'System Volume Information' \ --exclude '.thumbs' \ $mirror_source \ $mirror_user"@"$mirror_host":"$mirror_destination fi } function print_help () { echo "help" exit 0 } #TODO: Add function to recall backup by selection #TODO: Add function to automatically add key to keyring, if not found check_gpg_set set_user_mode if [ ${#@} -gt 0 ]; then while getopts 'c:hr:s:v' flag; do case "${flag}" in c) cleanup_mode=1 ;; h) print_help ;; m) mirror_backup ;; r) echo "recall" ;; s) source_mode="${OPTARG}" ;; v) verbose=1 ;; *) echo "Error. Unrecognized option: ${flag}." exit 1 ;; esac done else print_help fi if [ -n "$source_mode" ];then case $source_mode in aura) backup_single_directory $aura_source $aura_destination ;; bitlbee) backup_single_directory $bitlbee_source $bitlbee_destination ;; etc) backup_single_directory $etc_source $etc_destination ;; git) backup_multiple_directories $git_source $git_destination ;; mail) if [ "$mail_domains_as_folders" = "yes" ]; then backup_multiple_directories $mail_source $mail_destination "1" elif [ "$mail_domains_as_folders" = "no" ]; then backup_multiple_directories $mail_source $mail_destination else echo "Setting \"mail_domains_as_folders\" to $mail_domains_as_folders is not supported!" exit 1 fi ;; mailman) backup_single_directory $mailman_source $mailman_destination ;; databases) backup_all_databases ;; logs) backup_single_directory $logs_source $logs_destination ;; websites) backup_multiple_directories $websites_source $websites_destination ;; firefox) backup_single_directory $firefox_source $firefox_destination ;; irssi) backup_single_directory $irssi_source $irssi_destination ;; thunderbird) backup_single_directory $thunderbird_source $thunderbird_destination ;; weechat) backup_single_directory $weechat_source $weechat_destination ;; *) echo "Error. $source_mode is not a valid backup option." exit 1 esac elif [ $cleanup_mode -eq 1 ];then cleanup_backup fi exit 0