#!/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="" verbose='' 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_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 () { 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 destination=$1 if [ ! -d $destination ]; then echo "Directory \"$destination\" does not exist!" return 1 else return 0 fi } function check_directory_exists_and_autocreate () { local destination=$1 if [ ! -d $destination ]; then echo "Directory \"$destination\" does not exist yet. Creating..." mkdir -p $destination else return 0 fi } function check_directory_permission_root () { if [ -w $1 ]; then return 0 else echo "Directory not writable: $1." return 1 fi } function check_directory_writable_user () { if [ -w $1 ]; then return 0 else echo "Directory not writable: $1." return 1 fi } function check_directory_owner_user() { if [ ! -O $1 ]; then echo "Directory not owned by user $(whoami): $1" return 1 else return 0 fi } function check_user_directory () { local directory=$1 echo "Checking directory \"$directory\"." check_directory_exists_and_autocreate $directory check_directory_writable_user $directory return 0 } function check_root_directory () { local directory=$1 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 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 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" echo "Removing $tmp_file." rm -f "$tmp_file" return 1 fi 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 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 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 mysqldump --force \ --opt \ -u$database_user \ -p$database_password \ --databases $db > $tmp_file } function backup_all_databases () { check_database_server check_database_settings local databases=( ) local destination=$(sanitize_pathname $database_destination) local database 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 echo "Databases for which backups will be created: ${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 echo "Going to temporary directory ($tmp_directory)." 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 return 0 } function check_gpg_set () { if [ -z "$gpg_public_key" ];then echo "Error. \"gpg_public_key\" not set!" exit 1 fi } function print_help () { echo "help" exit 0 } #TODO: Add function to cleanup backups #TODO: Add function to mirror backups #TODO: Add function to automatically add key to keyring, if not found #TODO: Add verbose flag check_gpg_set set_user_mode if [ ${#@} -gt 0 ]; then while getopts 'c:hr:s:v' flag; do case "${flag}" in c) echo "Cleanup" ;; h) print_help ;; r) echo "recall" ;; s) source_mode="${OPTARG}" ;; v) verbose='true' ;; *) 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 ;; 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 fi exit 0