Script automysqlbackup

#!/usr/bin/env bash
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
shopt -s extglob

# BEGIN _flags
let "filename_flag_encrypted=0x01"
let "filename_flag_gz=0x02"
let "filename_flag_bz2=0x04"
let "filename_flag_diff=0x08"
# END _flags

# BEGIN _errors_notifications
let "E=0x00" # no errors
let "N=0x00" # no notifications

let "E_dbdump_failed=0x01"
let "E_backup_local_failed=0x02"
let "E_mkdir_basedir_failed=0x04"
let "E_mkdir_subdirs_failed=0x08"
let "E_perm_basedir=0x10"
let "E_enc_cleartext_delfailed=0x20"
let "E_enc_failed=0x40"
let "E_db_empty=0x80"
let "E_create_pipe_failed=0x100"
let "E_missing_deps=0x200"
let "E_no_basedir=0x400"
let "E_config_backupdir_not_writable=0x800"
let "E_dump_status_failed=0x1000"
let "E_dump_fullschema_failed=0x2000"

let "N_config_file_missing=0x01"
let "N_arg_conffile_parsed=0x02"
let "N_arg_conffile_unreadable=0x04"
let "N_too_many_args=0x08"
let "N_latest_cleanup_failed=0x10"
let "N_backup_local_nofiles=0x20"
# END _errors_notifications

# BEGIN _functions

# @info:    Default configuration options.
# @deps:    (none)
load_default_config() {
  CONFIG_configfile="/etc/automysqlbackup/automysqlbackup.conf"
  CONFIG_backup_dir='/var/backup/db'
  CONFIG_multicore='yes'
  CONFIG_multicore_threads=2
  CONFIG_do_monthly="01"
  CONFIG_do_weekly="5"
  CONFIG_rotation_daily=6
  CONFIG_rotation_weekly=35
  CONFIG_rotation_monthly=150
  CONFIG_mysql_dump_port=3306
  CONFIG_mysql_dump_usessl='yes'
  CONFIG_mysql_dump_username='root'
  CONFIG_mysql_dump_password=''
  CONFIG_mysql_dump_host='localhost'
  CONFIG_mysql_dump_host_friendly=''
  CONFIG_mysql_dump_socket=''
  CONFIG_mysql_dump_create_database='no'
  CONFIG_mysql_dump_use_separate_dirs='yes'
  CONFIG_mysql_dump_compression='gzip'
  CONFIG_mysql_dump_commcomp='no'
  CONFIG_mysql_dump_latest='no'
  CONFIG_mysql_dump_latest_clean_filenames='no'
  CONFIG_mysql_dump_max_allowed_packet=''
  CONFIG_mysql_dump_single_transaction='no'
  CONFIG_mysql_dump_master_data=
  CONFIG_mysql_dump_full_schema='yes'
  CONFIG_mysql_dump_dbstatus='yes'
  CONFIG_mysql_dump_differential='no'
  CONFIG_backup_local_files=()
  CONFIG_db_names=()
  CONFIG_db_month_names=()
  CONFIG_db_exclude=( 'information_schema' )
  CONFIG_table_exclude=()
  CONFIG_mailcontent='stdout'
  CONFIG_mail_maxattsize=4000
  CONFIG_mail_splitandtar='yes'
  CONFIG_mail_use_uuencoded_attachments='no'
  CONFIG_mail_address='root'
  CONFIG_encrypt='no'
  CONFIG_encrypt_password='password0123'
}

# @return:    true, if variable is set; else false
isSet() {
  if [[ ! ${!1} && ${!1-_} ]]; then return 1; else return 0; fi
}

# @return:    true, if variable is empty; else false
isEmpty() {
  if [[ ${!1} ]]; then return 1; else return 0; fi
}

# @info:    Called when one of the signals EXIT, SIGHUP, SIGINT, SIGQUIT or SIGTERM is emitted.
#            It removes the IO redirection, mails any log file information and cleans up any temporary files.
# @args:    (none)
# @return:    (none)
mail_cleanup () {
  removeIO
  # if the variables $log_file and $log_errfile aren't set or are empty and both associated files don't exist, skip output methods.
  # this might happen if 'exit' occurs before they are set.
  if [[ ! -e "$log_file" && ! -e "$log_errfile" ]];then
    echo "Skipping normal output methods, since the program exited before any log files could be created."
  else
    case "${CONFIG_mailcontent}" in
        'files')
                # Include error log if larger than zero.
                if [[ -s "$log_errfile" ]]; then
                    backupfiles=( "${backupfiles[@]}" "$log_errfile" )
                    errornote="WARNING: Error Reported - "
                fi
                temp="$(mktemp "$CONFIG_backup_dir"/tmp/mail_content.XXXXXX)"
                # Get backup size
                attsize=`du -c "${backupfiles[@]}" | awk 'END {print $1}'`
                if (( ${CONFIG_mail_maxattsize} >= ${attsize} )); then
                    if [[ "x$CONFIG_mail_use_uuencoded_attachments" = "xyes" ]]; then
                      cat "$log_file" > "$temp"
                      for j in "${backupfiles[@]}"; do
                        uuencode "$j" "$j" >> "$temp"
                      done
                      mail -s "${errornote} MySQL Backup Log and SQL Files for ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" ${CONFIG_mail_address} < "$temp"
                    else
                      mutt -s "${errornote} MySQL Backup Log and SQL Files for ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" -a "${backupfiles[@]}" -- ${CONFIG_mail_address} < "$log_file"
                    fi
                elif (( ${CONFIG_mail_maxattsize} <= ${attsize} )) && [[ "x$CONFIG_mail_splitandtar" = "xyes" ]]; then
                    if sPWD="$PWD"; cd "$CONFIG_backup_dir"/tmp && pax -wv "${backupfiles[@]}" | bzip2_compression | split -b $((CONFIG_mail_maxattsize*1000)) - mail_attachment_${datetimestamp}_ && cd "$sPWD"; then
                      files=("$CONFIG_backup_dir"/tmp/mail_attachment_${datetimestamp}_*)
                      echo -e "\n\nThe attachments have been split into multiple files.\nUse 'cat mail_attachment_2011-08-13_13h15m_* > mail_attachment_2011-08-13_13h15m.tar.bz2' to combine them and \
                              'bunzip2 <mail_attachment_2011-08-13_13h15m.tar.bz2 | pax -rv' to extract the content."
                      for ((j=0;j<"${#files[@]}";j++)); do
                        if [[ "x$CONFIG_mail_use_uuencoded_attachments" = "xyes" ]]; then
                          if (( $j = 0 )); then
                            cat "$log_file" > "$temp"
                            uuencode "$j" "$j" >> "$temp"
                          else
                            uuencode "$j" "$j" > "$temp"
                          fi
                          mail -s "${errornote} MySQL Backup Log and SQL Files for ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" ${CONFIG_mail_address} < "$temp"
                        else
                          mutt -s "${errornote} MySQL Backup Log and SQL Files for ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}; Part $((j+1))/${#files[@]}" -a "${files[j]}" -- ${CONFIG_mail_address} < "$log_file"
                        fi
                      done
                    else
                      cat "$log_file" | mail -s "WARNING! - MySQL Backup exceeds set maximum attachment size on ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" ${CONFIG_mail_address}
                    fi
                else
                    cat "$log_file" | mail -s "WARNING! - MySQL Backup exceeds set maximum attachment size on ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" ${CONFIG_mail_address}
                fi
                rm "$temp"
                ;;
        'log')
                cat "$log_file" | mail -s "MySQL Backup Log for ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" ${CONFIG_mail_address}
                [[ -s "$log_errfile" ]] && cat "$log_errfile" | mail -s "ERRORS REPORTED: MySQL Backup error Log for ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" ${CONFIG_mail_address}
                ;;
        'quiet')
                [[ -s "$log_errfile" ]] && cat "$log_errfile" | mail -s "ERRORS REPORTED: MySQL Backup error Log for ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host} - ${datetimestamp}" ${CONFIG_mail_address}
                ;;
        *)
                if [[ -s "$log_errfile" ]]; then
                        cat "$log_file"
                        echo
                        echo "###### WARNING ######"
                        echo "Errors reported during AutoMySQLBackup execution.. Backup failed"
                        echo "Error log below.."
                        cat "$log_errfile"
                else
                  cat "$log_file"
                fi
                ;;
    esac
    ###################################################################################
    # Clean up and finish
    [[ -e "$log_file" ]] && rm -f "$log_file"
    [[ -e "$log_errfile" ]] && rm -f "$log_errfile"
  fi
}

# @params:    #month    #year
# @deps:    (none)
days_of_month() {
  m="$1"; y="$2"; a=$(( 30+(m+m/8)%2 ))
  (( m==2 )) && a=$((a-2))
  (( m==2 && y%4==0 && ( y<100 || y%100>0 || y%400==0) )) && a=$((a+1))
  printf '%d' $a
}

# @info:    Checks if a folder is writable by creating a temporary file in it and removing it afterwards.
# @args:    folder to test
# @return:    returns false if creation of temporary file failed or it can't be removed afterwards; else true
# @deps:    (none)
chk_folder_writable () {
  local temp; temp="$(mktemp "$1"/tmp.XXXXXX)"
  if (( $? == 0 )); then
    rm "${temp}" || return 1
    return 0
  else
    return 1
  fi
}

# @info:    bzip2 compression
bzip2_compression() {
  var=("$@")
  re='^[0-9]*$'
  if [[ "x$CONFIG_multicore" = 'xyes' ]]; then
      if [[ "x$CONFIG_multicore_threads" != 'xauto' ]] && [[ "x$CONFIG_multicore_threads" =~ $re ]]; then
          var=( "-p${CONFIG_multicore_threads}" "${var[@]}" )
      fi
      pbzip2 "${var[@]}"
  else
      bzip2 "${var[@]}"
  fi
}

# @info:    gzip compression
gzip_compression() {
  var=("$@")
  re='^[0-9]*$'
  if [[ "x$CONFIG_multicore" = 'xyes' ]]; then
      if [[ "x$CONFIG_multicore_threads" != 'xauto' ]] && [[ "x$CONFIG_multicore_threads" =~ $re ]]; then
          var=( "-p${CONFIG_multicore_threads}" "${var[@]}" )
      fi
      pigz "${var[@]}"
  else
      gzip "${var[@]}"
  fi
}

# @info:    Remove date and time information from filename by renaming it.
# @args:    filename
# @return:    (none)
# @deps:    (none)
remove_datetimeinfo () {
  mv "$1" "$(echo "$1" | sed -re 's/_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}h[0-9]{2}m_(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday|January|February|March|April|May|June|July|August|September|October|November|December|[0-9]{1,2})//g')"
}
export -f remove_datetimeinfo

# @info:    Set time and date variables.
# @args:    (none)
# @deps:    days_of_month
set_datetime_vars() {
  datetimestamp=`date +%Y-%m-%d_%Hh%Mm`        # Datestamp e.g 2002-09-21_18h12m
  date_stamp=`date +%Y-%m-%d`                    # Datestamp e.g 2002-09-21
  date_day_of_week=`date +%A`                    # Day of the week e.g. Monday
  date_dayno_of_week=`date +%u`                # Day number of the week 1 to 7 where 1 represents Monday
  date_day_of_month=`date +%e | sed -e 's/^ //'`                # Date of the Month e.g. 27
  date_month=`date +%B`                        # Month e.g January
  date_weekno=`date +%V | sed -e 's/^0//'`    # Week Number e.g 37
  year=`date +%Y`
  month=`date +%m | sed -e 's/^0//'`
  date_lastday_of_last_month=$(days_of_month $(( $month==1 ? 12 : $month-1 )) $(( $month==1 ? ($year-1):$year )) )
  date_lastday_of_this_month=$(days_of_month $month $year)
}

# @info:    This function is called after data has already been saved. It performs encryption and
#            hardlink-copying of files to a latest folder.
# @return:    flags
# @deps:    load_default_config
files_postprocessing () {
    local flags
    let "flags=0x00"
    let "flags_files_postprocessing_success_encrypt=0x01"

    # -> CONFIG_encrypt
    [[ "${CONFIG_encrypt}" = "yes" && "${CONFIG_encrypt_password}" ]] && {
      if (( $CONFIG_dryrun )); then
        printf 'dry-running: openssl enc -aes-256-cbc -e -in %s -out %s.enc -pass pass:%s\n' ${1} ${1} "${CONFIG_encrypt_password}"
      else
        openssl enc -aes-256-cbc -e -in ${1} -out ${1}.enc -pass pass:"${CONFIG_encrypt_password}"
        if (( $? == 0 )); then
          if rm ${1} 2>&1; then
            echo "Successfully encrypted archive as ${1}.enc"
            let "flags |= $flags_files_postprocessing_success_encrypt"
          else
            echo "Successfully encrypted archive as ${1}.enc, but could not remove cleartext file ${1}."
            let "E |= $E_enc_cleartext_delfailed"
          fi
        else
          let "E |= $E_enc_failed"
        fi
      fi
    }
    # <- CONFIG_encrypt

    # -> CONFIG_mysql_dump_latest
    [[ "${CONFIG_mysql_dump_latest}" = "yes" ]] && {
      if (( $flags & $flags_files_postprocessing_success_encrypt )); then
        if (( $CONFIG_dryrun )); then
          printf 'dry-running: cp -al %s.enc %s/latest/\n' "${1}" "${CONFIG_backup_dir}"
        else
          cp -al "${1}${suffix}.enc" "${CONFIG_backup_dir}"/latest/
        fi
      else
        if (( $CONFIG_dryrun )); then
          printf 'dry-running: cp -al %s %s/latest/\n' "${1}" "${CONFIG_backup_dir}"
        else
          cp -al "${1}" "${CONFIG_backup_dir}"/latest/
        fi
      fi
    }
    # <- CONFIG_mysql_dump_latest

    return $flags
}

# @info:    When called, sets error and notify strings matching their flags. It then goes through all
#            collected error and notify messages and displays them.
# @args:    (none)
# @return:    true if no errors were set, otherwise false
# @deps:    log_base2, load_default_config
error_handler () {

  errors=(
    [0x01]='dbdump() failed.'
    [0x02]='Backup of local files failed. This is not this scripts primary objective. Continuing anyway.'
    [0x04]="Could not create the backup_dir ${CONFIG_backup_dir}. Please check permissions of the higher directory."
    [0x08]='At least one of the subdirectories (daily, weekly, monthly, latest) failed to create.'
    [0x10]="The backup_dir ${CONFIG_backup_dir} is not writable AND/OR executable."
    [0x20]='Could not remove the cleartext file after encryption. This error did not cause an abort. Remove it manually and check permissions.'
    [0x40]='Encryption failed. Continuing without encryption.'
    [0x80]='The mysql server is empty, i.e. no databases found. Check if something is wrong. Exiting.'
    [0x100]='Failed to create the named pipe (fifo) for reading in all databases. Exiting.'
    [0x200]='Dependency programs are missing. Perhaps they are not in $PATH. Exiting.'
    [0x400]='No basedir found, i.e. '
    [0x800]="${CONFIG_backup_dir} is not writable. Exiting."
    [0x1000]='Running of mysqlstatus failed.'
    [0x2000]='Running of mysqldump full schema failed.'
  )

  notify=(
    [0x01]="${CONFIG_configfile} was not found - no global config file."
    [0x02]="Parsed config file ${opt_config_file}."
    [0x04]="Unreadable config file \"${opt_config_file}\""
    [0x08]='Supplied more than one argument, ignoring ALL arguments - using default and global config file only.'
    [0x10]='Could not remove the files in the latest directory. Please check this.'
    [0x20]='No local backup files were set.'
    [0x40]=''
    [0x80]=''
    [0x100]=''
    [0x200]=''
    [0x400]=''
    [0x800]=''
    [0x1000]=''
    [0x2000]=''
  )

  local n
  local e

  n=$((${#notify[@]}-1))
  while (( N > 0 )); do
    e=$((2**n))
    if (( N&e )); then
      echo "Note:" ${notify[e]}
      let "N-=e"
    fi
    ((n--))
  done
  unset n;

  n=$((${#errors[@]}-1))
  if (( E > 0 )); then
    while (( E > 0 )); do
      e=$((2**n))
      if (( E&e )); then
    echo "Error:" ${errors[e]}
    let "E-=e"
      fi
      ((n--))
    done
    exit 1
  else
    exit 0
  fi
}

# @info:    Packs files in array ${#CONFIG_backup_local_files[@]} into tar file with optional compression.
# @args:    archive file without compression suffix, i.e. ending on .tar
# @return:    true in case of dry-run, otherwise the return value of tar -cvf
# @deps:    load_default_config
backup_local_files () {
  if ((! ${#CONFIG_backup_local_files[@]})) ; then
    if (( $CONFIG_dryrun )); then
      case "${CONFIG_mysql_dump_compression}" in
      'gzip')
        echo "tar -czvf ${1}${suffix} ${CONFIG_backup_local_files[@]}";
        ;;
      'bzip2')
        echo "tar -cjvf ${1}${suffix} ${CONFIG_backup_local_files[@]}";
        ;;
      *)
        echo "tar -cvf ${1}${suffix} ${CONFIG_backup_local_files[@]}";
        ;;
      esac
      echo "dry-running: tar -cv ${1} ${CONFIG_backup_local_files[@]}"
      return 0;
    else
      case "${CONFIG_mysql_dump_compression}" in
      'gzip')
        tar -czvf "${1}${suffix}" "${CONFIG_backup_local_files[@]}";
        return $?
        ;;
      'bzip2')
        tar -cjvf "${1}${suffix}" "${CONFIG_backup_local_files[@]}";
        return $?
        ;;
      *)
        tar -cvf "${1}${suffix}" "${CONFIG_backup_local_files[@]}";
        return $?
        ;;
      esac
    fi
  else
    let "N |= $N_backup_local_nofiles"
    echo "No local backup files specified."
  fi
}

# @info:    Parses the configuration options and sets the variables appropriately.
# @args:    (none)
# @deps:    load_default_config
parse_configuration () {
    # OPT string for use with mysqldump ( see man mysqldump )
    opt=( '--quote-names' '--opt' )

    # OPT string for use with mysql (see man mysql )
    mysql_opt=()

    # OPT string for use with mysqldump fullschema
    opt_fullschema=( '--all-databases' '--routines' '--no-data' )

    # OPT string for use with mysqlstatus
    opt_dbstatus=( '--status' )

    [[ "${CONFIG_mysql_dump_usessl}" = "yes" ]]         && {
      opt=( "${opt[@]}" '--ssl' )
      mysql_opt=( "${mysql_opt[@]}" '--ssl' )
      opt_fullschema=( "${opt_fullschema[@]}" '--ssl' )
      opt_dbstatus=( "${opt_dbstatus[@]}" '--ssl' )
    }
    [[ "${CONFIG_mysql_dump_master_data}" ]] && (( ${CONFIG_mysql_dump_master_data} == 1 || ${CONFIG_mysql_dump_master_data} == 2 )) && { opt=( "${opt[@]}" "--master-data=${CONFIG_mysql_dump_master_data}" );}
    [[ "${CONFIG_mysql_dump_single_transaction}" = "yes" ]]    && {
      opt=( "${opt[@]}" '--single-transaction' )
      opt_fullschema=( "${opt_fullschema[@]}" '--single-transaction' )
    }
    [[ "${CONFIG_mysql_dump_commcomp}" = "yes" ]]        && {
      opt=( "${opt[@]}" '--compress' )
      opt_fullschema=( "${opt_fullschema[@]}" '--compress' )
      opt_dbstatus=( "${opt_dbstatus[@]}" '--compress' )
    }
    [[ "${CONFIG_mysql_dump_max_allowed_packet}" ]]        && {
      opt=( "${opt[@]}" "--max_allowed_packet=${CONFIG_mysql_dump_max_allowed_packet}" )
      opt_fullschema=( "${opt_fullschema[@]}" "--max_allowed_packet=${CONFIG_mysql_dump_max_allowed_packet}" )
    }
    [[ "${CONFIG_mysql_dump_socket}" ]]            && {
      opt=( "${opt[@]}" "--socket=${CONFIG_mysql_dump_socket}" )
      mysql_opt=( "${mysql_opt[@]}" "--socket=${CONFIG_mysql_dump_socket}" )
      opt_fullschema=( "${opt_fullschema[@]}" "--socket=${CONFIG_mysql_dump_socket}" )
      opt_dbstatus=( "${opt_dbstatus[@]}" "--socket=${CONFIG_mysql_dump_socket}" )
    }
    [[ "${CONFIG_mysql_dump_port}" ]]            && {
      opt=( "${opt[@]}" "--port=${CONFIG_mysql_dump_port}" )
      mysql_opt=( "${mysql_opt[@]}" "--port=${CONFIG_mysql_dump_port}" )
      opt_fullschema=( "${opt_fullschema[@]}" "--port=${CONFIG_mysql_dump_port}" )
      opt_dbstatus=( "${opt_dbstatus[@]}" "--port=${CONFIG_mysql_dump_port}" )
    }

    # Check if CREATE DATABASE should be included in Dump
    if [[ "${CONFIG_mysql_dump_use_separate_dirs}" = "yes" ]]; then
        if [[ "${CONFIG_mysql_dump_create_database}" = "no" ]]; then
            opt=( "${opt[@]}" '--no-create-db' )
        else
            opt=( "${opt[@]}" '--databases' )
        fi
    else
        opt=( "${opt[@]}" '--databases' )
    fi
      
    # if differential backup is active and the specified rotation is smaller than 21 days, set it to 21 days to ensure, that
    # master backups aren't deleted.
    if [[ "x$CONFIG_mysql_dump_differential" = "xyes" ]] && (( ${CONFIG_rotation_daily} < 21 )); then
      CONFIG_rotation_daily=21
    fi

    # -> determine suffix
    case "${CONFIG_mysql_dump_compression}" in
      'gzip')    suffix='.gz';;
      'bzip2')    suffix='.bz2';;
      *)        suffix='';;
    esac
    # <- determine suffix

    # -> check exclude tables for wildcards
    local tmp;tmp=()
    local z;z=0
    for i in "${CONFIG_table_exclude[@]}"; do
      r='^[^*.]+\.[^.]+$'; [[ "$i" =~ $r ]] || { printf 'The entry %s in CONFIG_table_exclude has a wrong format. Ignoring the entry.' "$i"; continue; }
      db=${i%.*}
      table=${i#"$db".}
      r='\*'; [[ "$i" =~ $r ]] || { tmp[z++]="$i"; continue; }
      while read -r; do tmp[z++]="${db}.${REPLY}"; done < <(mysql --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${mysql_opt[@]}" --batch --skip-column-names -e "select table_name from information_schema.tables where table_schema='${db}' and table_name like '${table//\*/%}';")
    done
    for l in "${tmp[@]}"; do echo "exclude $l";done
    CONFIG_table_exclude=("${tmp[@]}")
    # <-

    if ((${#CONFIG_table_exclude[@]})); then
      for i in "${CONFIG_table_exclude[@]}"; do
        opt=( "${opt[@]}" "--ignore-table=$i" )
      done
    fi
}

# @info:    Backup database status
# @args:    archive file without compression suffix, i.e. ending on .txt
# @return:    true in case of dry-run, otherwise the return value of mysqlshow
# @deps:    load_default_config, parse_configuration
dbstatus() {
  if (( $CONFIG_dryrun )); then
    case "${CONFIG_mysql_dump_compression}" in
    'gzip')
      echo "dry-running: mysqlshow --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt_dbstatus[@]} | gzip_compression > ${1}${suffix}";
      ;;
    'bzip2')
      echo "dry-running: mysqlshow --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt_dbstatus[@]} | bzip2_compression > ${1}${suffix}";
      ;;
    *)
      echo "dry-running: mysqlshow --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt_dbstatus[@]} > ${1}${suffix}";
      ;;
    esac
    return 0;
  else
    case "${CONFIG_mysql_dump_compression}" in
    'gzip')
      mysqlshow --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt_dbstatus[@]}" | gzip_compression > "${1}${suffix}";
      return $?
      ;;
    'bzip2')
      mysqlshow --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt_dbstatus[@]}" | bzip2_compression > "${1}${suffix}";
      return $?
      ;;
    *)
      mysqlshow --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt_dbstatus[@]}" > "${1}${suffix}";
      return $?
      ;;
    esac
  fi
}

# @info:    Backup of the database schema.
# @args:    filename to save data to
# @return:    true in case of dry-run, otherwise the return value of mysqldump
# @deps:    load_default_config, parse_configuration
fullschema () {
  if (( $CONFIG_dryrun )); then
    case "${CONFIG_mysql_dump_compression}" in
    'gzip')
      echo "dry-running: mysqldump --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt_fullschema[@]} | gzip_compression > ${1}${suffix}";
      ;;
    'bzip2')
      echo "dry-running: mysqldump --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt_fullschema[@]} | bzip2_compression > ${1}${suffix}";
      ;;
    *)
      echo "dry-running: mysqldump --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt_fullschema[@]} > ${1}${suffix}";
      ;;
    esac
    return 0;
  else
    case "${CONFIG_mysql_dump_compression}" in
    'gzip')
      mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt_fullschema[@]}" | gzip_compression > "${1}${suffix}";
      return $?
      ;;
    'bzip2')
      mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt_fullschema[@]}" | bzip2_compression > "${1}${suffix}";
      return $?
      ;;
    *)
      mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt_fullschema[@]}" > "${1}${suffix}";
      return $?
      ;;
    esac
  fi
}

# @info:    Process a single db.
# @args:    subfolder, prefix, midfix, extension, rotation, rotation_divisor, rotation_string, 0/1 (db/dbs), db[, db ...]
process_dbs() {
  local subfolder="$1"
  local prefix="$2"
  local midfix="$3"
  local extension="$4"
  local rotation="$5"
  local rotation_divisor="$6"
  local rotation_string="$7"
  local multipledbs="$8"
  shift 8

  local name
  local subsubfolder

  # only activate differential backup for daily backups
  [[ "x$subfolder" != "xdaily" ]] && activate_differential_backup=0 || activate_differential_backup=1

  if (( $multipledbs )); then
    # multiple dbs
    subsubfolder=""
    name="all-databases"
  else
    # single db
    subsubfolder="/$1"
    name="$@"
  fi

  [[ -d "${CONFIG_backup_dir}/${subfolder}${subsubfolder}" ]] || {
  if (( $CONFIG_dryrun )); then
      printf 'dry-running: mkdir -p %s/${subfolder}%s\n' "${CONFIG_backup_dir}" "${subsubfolder}"
    else
      mkdir -p "${CONFIG_backup_dir}/${subfolder}${subsubfolder}"
    fi
  }

  manifest_file="${CONFIG_backup_dir}/${subfolder}${subsubfolder}/Manifest"
  fname="${CONFIG_backup_dir}/${subfolder}${subsubfolder}/${prefix}${name}_${datetimestamp}${midfix}${extension}"

  (( $CONFIG_debug )) && echo "DEBUG: process_dbs >> Setting manifest file to: ${manifest_file}"

  if (( $multipledbs )); then
    # multiple databases
    db="all-databases"
  else
    # single db
    db="$1"
  fi

  if [[ "x$CONFIG_mysql_dump_differential" = "xyes" ]] && [[ "x${CONFIG_encrypt}" != "xyes" ]] && (( $activate_differential_backup )); then

      unset manifest_entry manifest_entry_to_check

      echo "## Reading in Manifest file"
      parse_manifest "$manifest_file"
      echo
      echo "Number of manifest entries: $(num_manifest_entries)"
      echo

      # -> generate diff file
      let "filename_flags=0x00"
      
#       ## -> get latest differential manifest entry for specified db
#       if get_latest_manifest_entry_for_db "$db" 1; then
#         pid="${manifest_entry[2]}"
#         # filename format: prefix_db_YYYY-MM-DD_HHhMMm_[A-Za-z0-9]{8}(.sql|.diff)(.gz|.bz2)(.enc)
#         FileStub=${manifest_entry[0]%.@(sql|diff)*}
#         FileExt=${manifest_entry[0]#"$FileStub"}
#         re=".*\.enc.*";        [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_encrypted"
#         re=".*\.gz.*";        [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_gz"
#         re=".*\.bz2.*";        [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_bz2"
#         re=".*\.diff.*";    [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_diff"
#         manifest_latest_diff_entry=("${manifest_entry[@]}")
#       else    # no entries in manifest
#         pid=0
#       fi
#       ## <- get latest differential manifest entry for specified db

      ## -> get latest master manifest entry for specified db
      # Create a differential backup if a master entry in the manifest exists, it isn't the day we do weekly master backups or the master file we fetched is already from today.
      if get_latest_manifest_entry_for_db "$db" 0 && ( (( ${date_dayno_of_week} != ${CONFIG_do_weekly} )) || [[ "${manifest_entry[0]}" = *_$(date +%Y-%m-%d)_* ]] ); then
        pid="${manifest_entry[2]}"
        # filename format: prefix_db_YYYY-MM-DD_HHhMMm_[A-Za-z0-9]{8}(.sql|.diff)(.gz|.bz2)(.enc)
        FileStub="${manifest_entry[0]%.@(sql|diff)*}"
        FileExt="${manifest_entry[0]#"$FileStub"}"
        re=".*\.enc.*";        [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_encrypted"
        re=".*\.gz.*";        [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_gz"
        re=".*\.bz2.*";        [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_bz2"
        re=".*\.diff.*";    [[ "$FileExt" =~ $re ]] && let "filename_flags|=$filename_flag_diff"
        manifest_latest_master_entry=("${manifest_entry[@]}")
      else    # no entries in manifest
        pid=0
      fi
      ## <- get latest master manifest entry for specified db

  fi

  if [[ "x$CONFIG_mysql_dump_differential" = "xyes" ]] && [[ "x${CONFIG_encrypt}" != "xyes" ]] && (( $activate_differential_backup )) && ((! ($filename_flags & $filename_flag_encrypted) )); then

      # the master file is encrypted ... well this just shouldn't happen ^^ not going to decrypt or stuff like that ...at least not today :)

      if [[ "x$pid" = "x0" ]]; then
        # -> create master backup
        cfname="$(mktemp "${fname%.sql}_"XXXXXXXX".sql${suffix}")"
        uid="${cfname%.@(diff|sql)*}"
        uid="${uid:-8:8}"
        case "${CONFIG_mysql_dump_compression}" in
        'gzip')
          mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@" | gzip_compression > "$cfname";
          ;;
        'bzip2')
          mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@" | bzip2_compression > "$cfname";
          ;;
        *)
          mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@" > "$cfname";
          ;;
        esac
        add_manifest_entry "$manifest_file" "$cfname" "$pid" "$db" && parse_manifest "$manifest_file" && cp -al "$cfname" "${CONFIG_backup_dir}"/latest/ && echo "Generated master backup $cfname" && return 0 || return 1
        # <- create master backup
      else
        cfname="$(mktemp "${fname%.sql}_"XXXXXXXX".diff${suffix}")"
        uid="${cfname%.@(diff|sql)*}"
        uid=${uid:-8:8}
        echo "Creating differential backup to ${manifest_entry[0]}:"
        case "${CONFIG_mysql_dump_compression}" in
        'gzip')
          if (( $filename_flags & $filename_flag_gz )); then
            diff <(gzip_compression -dc "${manifest_latest_master_entry[0]}") <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") | gzip_compression > "$cfname";
          elif (( $filename_flags & $filename_flag_bz2 )); then
            diff <(bzip2_compression -dc "${manifest_latest_master_entry[0]}") <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") | gzip_compression > "$cfname";
          else
            diff "${manifest_latest_master_entry[0]}" <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") | gzip_compression > "$cfname";
          fi
          ;;
        'bzip2')
          if (( $filename_flags & $filename_flag_gz )); then
            diff <(gzip_compression -dc "${manifest_latest_master_entry[0]}") <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") | bzip2_compression > "$cfname";
          elif (( $filename_flags & $filename_flag_bz2 )); then
            diff <(bzip2_compression -dc "${manifest_latest_master_entry[0]}") <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") | bzip2_compression > "$cfname";
          else
            diff "${manifest_latest_master_entry[0]}" <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") | bzip2_compression > "$cfname";
          fi
          ;;
        *)
          if (( $filename_flags & $filename_flag_gz )); then
            diff <(gzip_compression -dc "${manifest_latest_master_entry[0]}") <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") > "$cfname";
          elif (( $filename_flags & $filename_flag_bz2 )); then
            diff <(bzip2_compression -dc "${manifest_latest_master_entry[0]}") <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") > "$cfname";
          else
            diff "${manifest_latest_master_entry[0]}" <(mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@") > "$cfname";
          fi
          ;;
        esac
        add_manifest_entry "$manifest_file" "$cfname" "$pid" "$db" && parse_manifest "$manifest_file" && cp -al "$cfname" "${manifest_latest_master_entry[0]}" "${CONFIG_backup_dir}"/latest/ && echo "generated $cfname" && return 0 || return 1
        
      fi
      # <- generate diff filename

  else
      cfname="${fname}${suffix}"
      if (( $CONFIG_dryrun )); then
        case "${CONFIG_mysql_dump_compression}" in
        'gzip')
          echo "dry-running: mysqldump --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt[@]} $@ | gzip_compression > ${cfname}"
          ;;
        'bzip2')
          echo "dry-running: mysqldump --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt[@]} $@ | bzip2_compression > ${cfname}"
          ;;
        *)
          echo "dry-running: mysqldump --user=${CONFIG_mysql_dump_username} --password=${CONFIG_mysql_dump_password} --host=${CONFIG_mysql_dump_host} ${opt[@]} $@ > ${cfname}"
          ;;
        esac
        return 0;
      else
        case "${CONFIG_mysql_dump_compression}" in
        'gzip')
          mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@" | gzip_compression > "${cfname}"
          ret=$?
          ;;
        'bzip2')
          mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@" | bzip2_compression > "${cfname}"
          ret=$?
          ;;
        *)
          mysqldump --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${opt[@]}" "$@" > "${cfname}"
          ret=$?
          ;;
        esac
      fi

  fi
 

  if (( $ret == 0 )); then
      echo "Rotating $(( ${rotation}/${rotation_divisor} )) ${rotation_string} backups for ${name}"
      if (( $CONFIG_dryrun )); then
        find "${CONFIG_backup_dir}/${subfolder}${subsubfolder}" -mtime +"${rotation}" -type f -exec echo "dry-running: rm" {} \;
      else
        find "${CONFIG_backup_dir}/${subfolder}${subsubfolder}" -mtime +"${rotation}" -type f -exec rm {} \;
      fi
      files_postprocessing "$cfname"
      tmp_flags=$?; var=; (( $tmp_flags & $flags_files_postprocessing_success_encrypt )) && var=.enc
      backupfiles=( "${backupfiles[@]}" "${cfname}${var}" )
  else
      let "E |= $E_dbdump_failed"
      echo "dbdump with parameters \"${CONFIG_db_names[@]}\" \"${cfname}\" failed!"
  fi
}

# @info:    Save stdout and stderr
# @deps:    (none)
activateIO() {
  ###################################################################################
  # IO redirection for logging.
  # $1 = $log_file, $2 = $log_errfile

  #(( $CONFIG_debug )) || {
    touch "$log_file"
    exec 6>&1                # Link file descriptor #6 with stdout. Saves stdout.
    exec > "$log_file"        # stdout replaced with file $log_file.

    touch "$log_errfile"
    exec 7>&2                # Link file descriptor #7 with stderr. Saves stderr.
    exec 2> "$log_errfile"    # stderr replaced with file $log_errfile.
  #}
}

# @info:    Restore stdout and stderr redirections.
# @deps:    (none)
removeIO() {
  exec 1>&6 6>&-      # Restore stdout and close file descriptor #6.
  exec 2>&7 7>&-      # Restore stdout and close file descriptor #7.
}

# @info:    Checks directories and subdirectories for existence and activates logging to either
#        $CONFIG_backup_dir or /tmp depending on what exists.
# @args:    (none)
# @deps:    load_default_config, activateIO, chk_folder_writable, error_handler
directory_checks_enable_logging () {
    ###################################################################################
    # Check directories and do cleanup work

    checkdirs=( "${CONFIG_backup_dir}"/{daily,weekly,monthly,latest,tmp} )
    [[ "${CONFIG_backup_local_files[@]}" ]] && { checkdirs=( "${checkdirs[@]}" "${CONFIG_backup_dir}/backup_local_files" ); }
    [[ "${CONFIG_mysql_dump_full_schema}" = 'yes' ]] && { checkdirs=( "${checkdirs[@]}" "${CONFIG_backup_dir}/fullschema" ); }
    [[ "${CONFIG_mysql_dump_dbstatus}" = 'yes' ]] && { checkdirs=( "${checkdirs[@]}" "${CONFIG_backup_dir}/status" ); }

    tmp_permcheck=0
    printf '# Checking for permissions to write to folders:\n'

    # "dirname ${CONFIG_backup_dir}" exists?
    # Y -> ${CONFIG_backup_dir} exists?
    #      Y -> Dry-run?
    #           Y -> log to /tmp, proceed to test subdirs
    #           N -> check writable ${CONFIG_backup_dir}?
    #                Y -> proceed to test subdirs
    #                N -> error: can't write to ${CONFIG_backup_dir}. Exit.
    #      N -> Dry-run?
    #           N -> proceed without testing subdirs
    #           Y -> create directory ${CONFIG_backup_dir}?
    #                Y -> check writable ${CONFIG_backup_dir}?
    #                     Y -> proceed to test subdirs
    #                     N -> error: can't write to ${CONFIG_backup_dir}. Exit.
    #                N -> error: ${CONFIG_backup_dir} is not writable. Exit.
    # N -> Dry-run?
    #      Y -> log to /tmp, proceed without testing subdirs
    #      N -> error: no basedir. Exit.

    # -> check base folder
    printf 'base folder %s ... ' "$(dirname "${CONFIG_backup_dir}")"
    if [[ -d "$(dirname "${CONFIG_backup_dir}")" ]]; then

    printf 'exists ... ok.\n'
    printf 'backup folder %s ... ' "${CONFIG_backup_dir}"

    if [[ -d "${CONFIG_backup_dir}" ]]; then
        printf 'exists ... writable? '
        if (( $CONFIG_dryrun )); then
          printf 'dry-running. Skipping. Logging to /tmp\n'
          log_file="/tmp/${CONFIG_mysql_dump_host}-`date +%N`.log"
          log_errfile="/tmp/ERRORS_${CONFIG_mysql_dump_host}-`date +%N`.log"
          activateIO "$log_file" "$log_errfile"
          tmp_permcheck=1
        else
        if chk_folder_writable "${CONFIG_backup_dir}"; then
          printf 'yes. Proceeding.\n'
          log_file="${CONFIG_backup_dir}/${CONFIG_mysql_dump_host}-`date +%N`.log"
          log_errfile="${CONFIG_backup_dir}/ERRORS_${CONFIG_mysql_dump_host}-`date +%N`.log"
          activateIO "$log_file" "$log_errfile"
          tmp_permcheck=1
        else
          printf 'no. Exiting.\n'
          let "E |= $E_config_backupdir_not_writable"
          error_handler
        fi
        fi

    else

        printf 'creating ... '
        if (( $CONFIG_dryrun )); then
        printf 'dry-running. Skipping.\n'
        else
        if mkdir -p "${CONFIG_backup_dir}" >/dev/null 2>&1; then
          printf 'success.\n'
          log_file="${CONFIG_backup_dir}/${CONFIG_mysql_dump_host}-`date +%N`.log"
          log_errfile="${CONFIG_backup_dir}/ERRORS_${CONFIG_mysql_dump_host}-`date +%N`.log"
          activateIO "$log_file" "$log_errfile"
          tmp_permcheck=1
        else
          printf 'failed. Exiting.\n'
          let "E |= $E_mkdir_basedir_failed"
          error_handler
        fi
        fi

    fi

    else

    if (( $CONFIG_dryrun )); then
        printf 'dry-running. Skipping. Logging to /tmp\n'
        log_file="/tmp/${CONFIG_mysql_dump_host}-`date +%N`.log"
        log_errfile="/tmp/ERRORS_${CONFIG_mysql_dump_host}-`date +%N`.log"
        activateIO "$log_file" "$log_errfile"
    else
      printf 'does not exist. Exiting.\n'
      let "E |= $E_no_basedir"
      error_handler
    fi

    fi
    # <- check base folder

    # -> check subdirs
    if (( $tmp_permcheck ==  1 )); then

    (( $CONFIG_dryrun )) || [[ -r "${CONFIG_backup_dir}" && -x "${CONFIG_backup_dir}" ]] || { let "E |= $E_perm_basedir"; error_handler; }

    for i in "${checkdirs[@]}"; do
      printf 'checking directory "%s" ... ' "$i"
      if [[ -d "$i" ]]; then
        printf 'exists.\n'
      else
        printf 'creating ... '
        if (( $CONFIG_dryrun )); then
          printf 'dry-running. Skipping.\n'
        else
        if mkdir -p "$i" >/dev/null 2>&1; then
          printf 'success.\n'
        else
          printf 'failed. Exiting.\n'
          let "E |= $E_mkdir_subdirs_failed"
          error_handler
        fi
        fi
      fi
    done

    fi
    # <- check subdirs

}

# @info:    If CONFIG_mysql_dump_latest is set to 'yes', the directory ${CONFIG_backup_dir}"/latest will
#            be cleaned.
# @args:    (none)
# @deps:    load_default_config
cleanup_latest ()  {
    # -> latest cleanup
    if [[ "${CONFIG_mysql_dump_latest}" = "yes" ]]; then
      printf 'Cleaning up latest directory ... '
      if (( $CONFIG_dryrun )); then
    printf 'dry-running. Skipping.\n'
      else
    if rm -f "${CONFIG_backup_dir}"/latest/* >/dev/null 2>&1; then
      printf 'success.\n'
    else
      printf 'failed. Continuing anyway, activating Note-Flag.\n'
      let "N |= $N_latest_cleanup_failed"
    fi
      fi
    fi
    # <- latest cleanup
}

# @info:    Checks for dependencies in form of external programs, that need to be available when running
#            this program.
# @args:    (none)
# @deps:    load_default_config
check_dependencies () {
    echo
    echo "# Testing for installed programs"
    dependencies=( 'mysql' 'mysqldump' )

    if [[ "x$CONFIG_multicore" = 'xyes' ]]; then

        if [[ "x$CONFIG_mysql_dump_compression" = 'xbzip2' ]]; then
          if type pbzip2 &>/dev/null; then
            echo "pbzip2 ... found."
          else
            CONFIG_multicore='no' # turn off multicore support, since the program isn't there
            echo "WARNING: Turning off multicore support, since pbzip2 isn't there."
          fi
        elif [[ "x$CONFIG_mysql_dump_compression" = 'xgzip' ]]; then
          if type pigz &>/dev/null; then
            echo "pigz ... found."
          else
            CONFIG_multicore='no' # turn off multicore support, since the program isn't there
            echo "WARNING: Turning off multicore support, since pigz isn't there."
          fi
        fi

    else
        [[ "x$CONFIG_mysql_dump_compression" = 'xbzip2' ]] && dependencies=("${dependencies[@]}" 'bzip2' )
        [[ "x$CONFIG_mysql_dump_compression" = 'xgzip' ]] && dependencies=("${dependencies[@]}" 'gzip' )
    fi      

    if [[ "x$CONFIG_mailcontent" = 'xlog' || "x$CONFIG_mailcontent" = 'xquiet' ]]; then
      dependencies=( "${dependencies[@]}" 'mail' )
    elif [[ "x$CONFIG_mailcontent" = 'xfiles' ]]; then
      dependencies=( "${dependencies[@]}" 'mail' )
      if [[ "x$CONFIG_mail_use_uuencoded_attachments" != 'xyes' ]]; then
        dependencies=( "${dependencies[@]}" 'mutt' )
      fi
    fi

    for i in "${dependencies[@]}"; do
      printf '%s ... ' "$i"
      if type "$i" &>/dev/null; then
        printf 'found.\n'
      else
        printf 'not found. Aborting.\n';
        let "E |= $E_missing_deps"
        error_handler
      fi
    done
    echo
}

# @info:    Get database list and remove excluded ones.
# @args:    (none)
# @deps:    load_default_config, error_handler
#
#    alldbnames = array of all databases
#    empty?    ->    error
#    remove excludes from array alldbnames
#    CONFIG_db_names empty? -> set to alldbnames
#    CONFIG_db_month_names empty? -> set to alldbnames
#
parse_databases() {
  # bash 4.x version
  #mapfile -t alldbnames < <(mysql --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" --batch --skip-column-names -e "show databases")
  alldbnames=()

  printf "# Parsing databases ... "
  # bash 3.0
  local i;i=0;
  while read -r; do alldbnames[i++]="$REPLY"; done < <(mysql --user="${CONFIG_mysql_dump_username}" --password="${CONFIG_mysql_dump_password}" --host="${CONFIG_mysql_dump_host}" "${mysql_opt[@]}" --batch --skip-column-names -e "show databases")
  unset i

  # mkfifo foo || exit; trap 'rm -f foo' EXIT

  ((! "${#alldbnames[@]}" )) && { let "E |= $E_db_empty"; error_handler; }

  # -> remove excluded dbs from list
  for exclude in "${CONFIG_db_exclude[@]}"; do
    for i in "${!alldbnames[@]}"; do if [[ "x${alldbnames[$i]}" = "x${exclude}" ]]; then unset 'alldbnames[i]'; fi; done
  done
  # <- remove excluded dbs from list

  # check for empty array lists and copy all dbs
  ((! ${#CONFIG_db_names[@]}))    && CONFIG_db_names=( "${alldbnames[@]}" )
  ((! ${#CONFIG_db_month_names[@]}))    && CONFIG_db_month_names=( "${alldbnames[@]}" )
  printf "done.\n"
}

# @return:    true if locked, false otherwise
# @param:    manifest_file
status_manifest() {
  if [[ -e "$1".lock ]]; then
    return 0
  else
    return 1
  fi
}
# @return: true if successfully created lock file, else false
# @param:    manifest_file
lock_manifest() {
  if status_manifest "$1"; then
    return 0
  else
    if touch "$1".lock &>/dev/null; then
      return 0
    else
      return 1
    fi
  fi
}
# @return: true if successfully removed lock file, else false
# @param:    manifest_file
unlock_manifest() {
  if status_manifest "$1"; then
    if rm "$1".lock &>/dev/null; then
      return 0
    else
      return 1
    fi
  else
    return 0
  fi
}
# @return: true if unlock_manifest or lock_manifest, depending on status_manifest, return true, else false
# @param:    manifest_file
toggle_manifest() {
  if status_manifest "$1"; then
    unlock_manifest "$1" && return 0 || return 1
  else
    lock_manifest "$1" && return 0 || return 1
  fi
}

# expects manifest_entry_to_check to be an array with four entries
# @param:    manifest_file
# return:    0, if all is okay
#            1, file doesn't exist - removed entry from manifest
#            2, file doesn't exist - tried to remove entry from manifest, but failed
check_manifest_entry() {
  local entry_md5sum
  [[ ! -e "${manifest_entry_to_check[0]}" ]] && { rm_manifest_entry_by_filename "${manifest_entry_to_check[0]}" 1 && return 1 || return 2; }
  entry_md5sum="$(md5sum "${manifest_entry_to_check[0]}" | awk '{print $1}')"
  if [[ "${entry_md5sum}" != "${manifest_entry_to_check[1]}" ]]; then
    printf 'g/%s/s//%s/g\nw\nq' "${manifest_entry_to_check[1]}" "${entry_md5sum}" | ed -s "$1"
  else
    return 0
  fi
}

# parse manifest file and collect entries in manifest_array
# @param:    manifest_file
#
#    sort manifest file after first field (filename) -> read this line by line
#        check if line matches regexp || add to array manifest_entries_corrupted && continue
#        split lines at tab character \t and put into array line_arr
#        check manifest entry
#            -> file does not exist -> remove entry from manifest; continue no matter if this succeeds or not
#        loop through previous entries in the manifest
#            filename already in there? remove all entries with the same filename but the one that is already in the array
#            md5sum has already occured?
#                if size = 0
#                    then don't compare
#                else
#                    request user action by adding entry to array manifest_entries_user_action_required with information, that identical files exist && continue 2
#                fi
#        add entry to manifest_array
#
parse_manifest() {
  local i n re line line_arr check
  unset manifest_array; manifest_array=()
  local tmp_md5sum
  # array ( filename_1, md5sum_1, id_1[, rel_id_1] ), ... )
  # reserving 4 members for each entry, thus each filename entry in the array has array key 4(n-1)+1
  (( $CONFIG_debug )) && echo ">>>>>>> Parsing manifest file: $1"
  n=1
  [[ -s "$1" ]] &&
  while read line
  do
    # ANY CHANGES INSIDE HERE ON THE MANIFEST_FILE HAVE NO IMPACT ON THE LINES WE LOOP OVER; THE sort COMMAND READS THE FILE ENTIRELY AT THE BEGINNING AND PASSES THE OUTPUT TO THE LOOP
    # check if line has expected format, i.e. check against regular expression
    re=$'^[^\t]*\tmd5sum\t[^\t]*\tdiff_id\t[A-Za-z0-9]{8}\trel_id\t(0|[A-Za-z0-9]{8})\tdb\t[^\t]*$'
    [[ $line =~ $re ]] || { echo "Corrupted line: $line"; manifest_entries_corrupted=( "${manifest_entries_corrupted[@]}" "$1" "$line" ); continue; }
    IFS=$'\t' read -ra line_arr <<< "$line"

    # prepare array of the current line
    manifest_entry_to_check=()
    for ((i=0;i<${#line_arr[@]};i=$i+2)); do
      manifest_entry_to_check[i/2]="${line_arr[i]}"
    done
    # check manifest entry, which uses the array manifest_entry_to_check
    check_manifest_entry "$1"
    check=$?
    case $check in
      1)  (( $CONFIG_debug )) && echo "File for manifest entry $line does not exist. Entry removed."
          continue # file doesn't exist - removed entry from manifest
          break;;
      2)  (( $CONFIG_debug )) && echo "File for manifest entry $line does not exist. Failed to remove the entry."
          continue # file doesn't exist - tried to remove entry from manifest, but failed
          break;;
    esac

    # loop through the manifest_array, as it has been filled by now and check if an entry already exists with the same values
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "x${manifest_array[i]}" = "x${line_arr[0]}" ]]; then    # found entry with the same filename
        (( $CONFIG_debug )) && echo "Found multiple entries with the same filename. Removing all but the first-found entry from manifest."
        # remove all entries with this filename and add a new one based on the values of the item already in the array
        rm_manifest_entry_by_filename "$1" "${manifest_array[i]}" 1 && add_manifest_entry "$1" "${manifest_array[i]}" "${manifest_array[i+3]}"
        continue 2    # the original entry, to which we compared, is already in the manifest_array; no matter if this is resolved or not, we don't
                    # need to add this entry to the manifest_array
      elif [[ "x${manifest_array[i+1]}" = "x${line_arr[2]}" ]]; then    # found entry with different filename but same md5sum - file copied and renamed?!
        if [[ ! -s "${line_arr[0]}" ]]; then # empty file - don't start to compare md5sums ...
          (( $CONFIG_debug )) && echo "Found empty file ${line_arr[0]}."
        else
          (( $CONFIG_debug )) && echo "Found multiple entries with the same md5sum but different filename."
          (( $CONFIG_debug )) && echo -e ">> fname_manifest:\t${manifest_array[i]}\t${manifest_array[i+1]}\n>> fname_line:\t\t${line_arr[0]}\t${line_arr[2]}"
          if [[ "x${line_arr[6]}" != "x0" ]]; then
            if [[ "x${manifest_array[i+3]}" = "x${line_arr[6]}" ]]; then    # parent id is the same; TODO inform user of this predicament and suggest solution
              manifest_entries_user_action_required=( "${manifest_entries_user_action_required[@]}" "$1" "${manifest_array[i]}" "The file has an identical copy with the same parent id. If you don't know why it exists, it is safe to remove it." )
              continue 2
            else
              manifest_entries_user_action_required=( "${manifest_entries_user_action_required[@]}" "$1" "${manifest_array[i]}" "The file has an identical copy with different parent id. This should not happen. Remove the file, which is not the correct follow-up to the previous differential or master backup." )
              continue 2
            fi
          fi
        fi
      fi
    done

    # add entry to manifest array
    for ((i=0;i<${#line_arr[@]};i=$i+2)); do
      manifest_array[(n-1)*${fields}+i/2]="${line_arr[i]}"
      #echo "manifest array key $((($n-1)*4+$i/2)) with value ${line_arr[i]}"
    done
    
    ((n++))
  done < <(sort -t $'\t' -k"1" "$1")
  (( $CONFIG_debug )) && echo "<<<<<<< # manifest entries: $((${#manifest_array[@]}/$fields))"
  (( $CONFIG_debug )) && echo "<<<<<<< FINISHED"
  return 0
}

# get_manifest_entry_by_* PATTERN [regexp]
# if second parameter 'regexp' (string!) is passed, PATTERN will be matched as regular expression
get_manifest_entry_by_filename() {
  local i
  if [[ "x$2" = "xregexp" ]]; then
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "${manifest_array[i]}" =~ $1 ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  else
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "x${manifest_array[i]}" = "x$1" ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  fi
  return 1
}
get_manifest_entry_by_md5sum() {
  local i
  if [[ "x$2" = "xregexp" ]]; then
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "${manifest_array[i+1]}" =~ $1 ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  else
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "x${manifest_array[i+1]}" = "x$1" ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  fi
  return 1
}
get_manifest_entry_by_id() {
  local i
  if [[ "x$2" = "xregexp" ]]; then
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "${manifest_array[i+2]}" =~ $1 ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  else
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "x${manifest_array[i+2]}" = "x$1" ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  fi
  return 1
}
get_manifest_entry_by_rel_id() {
  local i
  if [[ "x$2" = "xregexp" ]]; then
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "${manifest_array[i+3]}" =~ $1 ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  else
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "x${manifest_array[i+3]}" = "x$1" ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  fi
  return 1
}
get_manifest_entry_by_db() {
  local i
  if [[ "x$2" = "xregexp" ]]; then
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "${manifest_array[i+4]}" =~ $1 ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  else
    for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
      if [[ "x${manifest_array[i+4]}" = "x$1" ]]; then
        manifest_entry=( "${manifest_array[i]}" "${manifest_array[i+1]}" "${manifest_array[i+2]}" "${manifest_array[i+3]}" "${manifest_array[i+4]}" )
        return 0
        break;
      fi
    done
  fi
  return 1
}

# @params:    db, master/diff (0,1)
# @return:    2: no entries in manifest for the specified database 'db'
#            1: could not get manifest element by filename
#            0: all fine, match is in array 'manifest_entry'
get_latest_manifest_entry_for_db() {
  local db_array newarray i
  for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
    if (( $2 )); then # latest differential or master backup, i.e. just take the latest one!
      if [[ "x${manifest_array[i+4]}" = "x$1" ]]; then
        db_array=( "${db_array[@]}" "${manifest_array[i]}" )
      fi
    else # latest master backup, pid=0
      if [[ "x${manifest_array[i+4]}" = "x$1" && "x${manifest_array[i+3]}" = "x0" ]]; then
        db_array=( "${db_array[@]}" "${manifest_array[i]}")
      fi
    fi
  done
  if (( "${#db_array[@]}" == 0 )); then return 2;
  else
    #newarray=(); while IFS= read -r -d '' line; do newarray+=("$line"); done < <(printf '%s\0' "${db_array[@]}" | sort -z)
    get_manifest_entry_by_filename "${db_array[@]:(-1)}" # last entry of db_array, has, due to the way sort works, to be the latest one
    return $?
  fi
}

# @params:    manifest_file    filename/md5sum/id/rel_id    [1(=don't parse manifest after finished)]
# if second parameters
#
#    lock manifest -> use awk, print all lines that don't have second parameter at the appropriate field -> unlock manifest
#    param3=0 -> parse manifest
#
rm_manifest_entry_by_filename() {
  lock_manifest "$1" && awk -F"\t" -v v="$2" '$1 != v' "$1" > "$1".tmp && mv "$1".tmp "$1" && unlock_manifest "$1" || return 1
  (( "$3" )) || parse_manifest "$1"
  return 0
}
rm_manifest_entry_by_md5sum() {
  lock_manifest "$1" && awk -F"\t" -v v="$2" '$3 != v' "$1" > "$1".tmp && mv "$1".tmp "$1" && unlock_manifest "$1" || return 1
  (( "$3" )) || parse_manifest "$1"
  return 0
}
rm_manifest_entry_by_id() {
  lock_manifest "$1" && awk -F"\t" -v v="$2" '$5 != v' "$1" > "$1".tmp && mv "$1".tmp "$1" && unlock_manifest "$1" || return 1
  (( "$3" )) || parse_manifest "$1"
  return 0
}
rm_manifest_entry_by_rel_id() {
  lock_manifest "$1" && awk -F"\t" -v v="$2" '$7 != v' "$1" > "$1".tmp && mv "$1".tmp "$1" && unlock_manifest "$1" || return 1
  (( "$3" )) || parse_manifest "$1"
  return 0
}
rm_manifest_entry_by_db() {
  lock_manifest "$1" && awk -F"\t" -v v="$2" '$9 != v' "$1" > "$1".tmp && mv "$1".tmp "$1" && unlock_manifest "$1" || return 1
  (( "$3" )) || parse_manifest "$1"
  return 0
}

# parameters: manifest_file, filename, parent_id, db
add_manifest_entry() {
  local md5sum
  local id
  local filename
  local parent_id
  local db
  filename="$2"
  parent_id="$3"
  db="$4"
  lock_manifest "$1" || return 1
  id="${filename%.@(diff|sql)*}"
  id="${id:(-8):8}"
  #id="$(echo $filename | sed -re 's/.*_[0-9]{2}h[0-9]{2}m_([^\.]*)\..*/\1/')"
  md5sum="$(md5sum "$filename" | awk '{print $1}')"
  if [[ "x$parent_id" = 'x' ]]; then
    echo -e "${filename}\tmd5sum\t${md5sum}\tdiff_id\t${id}\trel_id\t0\tdb\t${db}" >> "$1"
  else
    echo -e "${filename}\tmd5sum\t${md5sum}\tdiff_id\t${id}\trel_id\t${parent_id}\tdb\t${db}" >> "$1"
  fi
  unlock_manifest "$1" || return 1
}

# @info:    Echos number of manifest entries.
num_manifest_entries() {
  echo "$((${#manifest_array[@]}/$fields))"
}

# @info:    Test if a value is in the array testarray
# @param:    value
# @var in_array_index:    array index of the first match
# @return    0 if a match was found, otherwise 1
in_array() {
  local j
  for ((j=0;j<"${#testarray[@]}";j++)); do
    if [[ "x${testarray[j]}" = "x$1" ]]; then
      in_array_index=$j
      return 0
    fi
  done
  return 1
}

# @param:    clear(0/1), meta_information, list_value1, list_value2, ...
extended_select() {
    local a c k m i r r_number meta_information choice selection do_clear
    meta_information="$2"
    do_clear="$1"
    shift 2
    declare -a list=("$@")
    selection=()
    # BEGIN _select_filenames
    #tput sc
    while true; do
        if (( $do_clear )); then
          clear
        else
          : #tput rc
        fi
        declare -a testarray=("${selection[@]}")
        echo "Selection for <$meta_information>"
        echo "Notation: 1,2-4,-5,-6-9 or * or -* ('-' will remove selections)."
        # print options

        for ((i=0;i<"${#list[@]}";i++)); do
          if in_array $i; then
            echo -e "$i) [+]\t${list[i]}"
          else
            echo -e "$i) [ ]\t${list[i]}"
          fi
        done
        
        echo -e "$i)\tDONE"
        done_id=$i

        min=0
        max=${#list[@]} # we have to account for the last possible number of DONE
        # evaluate response
        while true; do
            printf '#? '
            read choice
            r='^((-?[0-9]+(-[0-9]+)?,)*-?[0-9]+(-[0-9]+)?|-?\*)$'
            [[ $choice =~ $r ]] || continue
            if [[ "x$choice" = 'x*' ]]; then
                unset m
                for ((m=0;m<"${#list[@]}";m++)); do
                  selection=("${selection[@]}" "$m")
                done
                continue 2
            elif [[ "x$choice" = 'x-*' ]]; then
                selection=()
                continue 2
            else
                unset string num1 num2 op op_rm
                r_number='^[0-9]$'

                # BEGIN process_choice
                for ((a=0;a<${#choice};a++))
                do
                    c="${choice:a:1}"
                    declare -a testarray=("${selection[@]}")

                    if (( ${#string} == 0 )) && [[ "x$c" = "x-" ]] && ! (($op)); then
                        op_rm=1
                        continue
                    elif [[ $c =~ $r ]]; then
                        string=${string}"$c"
                        if (( $a == (${#choice}-1) )); then # last character
                            # we have a A-B case
                            if (($op)); then
                                num2="$string"
                                unset k
                                for ((k=$num1;k<=$num2;k++)); do
                                  (( $k >= $min )) && (( $k <= $max )) || continue
                                  if ! in_array $k; then
                                    selection=("${selection[@]}" $k)
                                  else
                                    if (( $op_rm )); then
                                      new_array=()
                                      for ((m="$((${#selection[@]}-1))";m>=0;m--)); do
                                        if [[ "x${selection[m]}" != "x$k" ]]; then
                                          new_array=("${new_array[@]}" "${selection[m]}")
                                        fi
                                      done
                                      declare -a selection=("${new_array[@]}")
                                    fi
                                  fi
                                done
                                unset op op_rm num1 num2 string
                                continue
                            else
                                (( $string >= $min )) && (( $string <= $max )) || continue
                                if ! in_array "$string"; then
                                  selection=("${selection[@]}" "$string")
                                else
                                  if (($op_rm)); then
                                    unset m
                                    new_array=()
                                    for ((m=0;m<"${#selection[@]}";m++)); do
                                      if [[ "x${selection[m]}" != "x$string" ]]; then
                                        new_array=("${new_array[@]}" "${selection[m]}")
                                      fi
                                    done
                                    declare -a selection=("${new_array[@]}")
                                  fi
                                fi
                            fi
                        else
                          continue
                        fi
                    elif [[ "x$c" = "x-" ]]; then
                        num1="$string"
                        unset string
                        op=1
                        if (( $a == (${#choice}-1) )); then
                          break
                        else
                          continue
                        fi
                    elif [[ "x$c" = "x," ]]; then
                        # we have a A-B case
                        if (($op)); then
                          num2="$string"
                          unset k
                          for ((k=$num1;k<=$num2;k++)); do
                            (( $k >= $min )) && (( $k <= $max )) || continue
                            if ! in_array $k; then
                              selection=("${selection[@]}" $k)
                            else
                              if (( $op_rm )); then
                                unset m
                                new_array=()
                                for ((m=0;m<"${#selection[@]}";m++)); do
                                  if [[ "x${selection[m]}" != "x$k" ]]; then
                                    new_array=("${new_array[@]}" "${selection[m]}")
                                  fi
                                done
                                declare -a selection=("${new_array[@]}")
                              fi
                            fi
                          done
                          unset op op_rm num1 num2 string
                          continue
                        else # it's just a single number
                            (( $string >= $min )) && (( $string <= $max )) || { unset op op_rm num1 num2 string; continue; }
                            if ! in_array "$string"; then
                              selection=("${selection[@]}" "$string")
                            else
                              if (($op_rm)); then
                                unset m
                                new_array=()
                                for ((m=0;m<"${#selection[@]}";m++)); do
                                  if [[ "x${selection[m]}" != "x$string" ]]; then
                                    new_array=("${new_array[@]}" "${selection[m]}")
                                  fi
                                done
                                declare -a selection=("${new_array[@]}")
                              fi
                            fi
                            unset op op_rm num1 num2 string
                            continue
                        fi
                    else
                      continue 2; # this should not happen
                    fi
                done
                # END process_choice

                declare -a testarray=("${selection[@]}")
                if in_array "$done_id"; then
                  break 2
                else
                  continue 2
                fi
            fi
        done
    done
    extended_select_return=()
    extended_select_return_id=()
    for i in "${selection[@]}"; do
      [[ "x$i" != "x$done_id" ]] && { extended_select_return=("${extended_select_return[@]}" "${list[i]}"); extended_select_return_id=("${extended_select_return_id[@]}" "$i"); }
    done
    
}

# END _functions

# BEGIN _methods

# @info:    Backup method
method_backup () {
    manifest_entries_corrupted=()
    manifest_entries_user_action_required=()

    # END __FUNCTIONS
    ##############################################################################################################
    # BEGIN __STARTUP

    load_default_config

    trap mail_cleanup EXIT SIGHUP SIGINT SIGQUIT SIGTERM
    if [[ -r "${CONFIG_configfile}" ]]; then source "${CONFIG_configfile}"; echo "Parsed config file \"${CONFIG_configfile}\""; else let "N |= $N_config_file_missing"; fi; echo
    if (( $opt_flag_config_file )); then if [[ -r "${opt_config_file}" ]]; then source "${opt_config_file}"; let "N |= $N_arg_conffile_parsed"; else let "N |= $N_arg_conffile_unreadable"; fi; else let "N |= $N_too_many_args"; fi

    (( $CONFIG_dryrun )) && {
      echo "NOTE: We are dry-running. That means, that the script just shows you what it would do, if it were operating normally."
      echo "THE PRINTED COMMANDS CAN'T BE COPIED AND EXECUTED IF THERE ARE SPECIAL CHARACTERS, SPACES, ETC. IN THERE THAT WOULD NEED TO BE PROPERLY QUOTED IN ORDER TO WORK. THESE WERE CORRECTLY QUOTED FOR THE OUTPUT COMMAND, BUT CAN'T BE SEEN NOW."
      echo
    }

    export LC_ALL=C
    PROGNAME=`basename $0`
    PATH=${PATH}:/usr/local/bin:/usr/bin:/bin:/usr/local/mysql/bin
    version=3.0
    fields=5 # manifest fields

    directory_checks_enable_logging
    cleanup_latest
    set_datetime_vars
    check_dependencies    # check for required programs
    parse_configuration    # parse configuration and set variables appropriately

    # END __STARTUP
    #--------------------------------------------------------------------------------------------------------------------------------------
    # BEGIN __PREPARE

    backupfiles=()
    parse_databases

    # debug output of variables
    (( $CONFIG_debug )) && { echo; echo "# DEBUG: printing all current variables"; declare -p | egrep -o '.* (CONFIG_[a-z_]*|opt|mysql_opt|opt_dbstatus|opt_fullschema)=.*'; echo; }
    (( $CONFIG_debug )) && { echo "DEBUG: before pre-backup"; ( IFS=,; echo "DEBUG: CONFIG_db_names '${CONFIG_db_names[*]}'" ); ( IFS=,; echo "DEBUG: CONFIG_db_month_names '${CONFIG_db_month_names[*]}'" );}

    # END __PREPARE
    #--------------------------------------------------------------------------------------------------------------------------------------
    # BEGIN __MAIN

    ### filename formats
    ##
    ## example date values:
    # 14'th of August (08) 2011
    # week number: 32
    # Sunday (date_dayno_of_week: 7)
    ##
    ## separate db's:
    #    monthly_DBNAME_2011-08-14_18h12m_August.sql(.enc).{gz,bzip2}
    #    weekly_DBNAME_2011-08-14_18h12m_32.sql(.enc).{gz,bzip2}
    #    daily_DBNAME_2011-08-14_18h12m_7.sql(.enc).{gz,bzip2}
    ## all-databases:
    #    monthly_all-databases_DBNAME_2011-08-14_18h12m_August.sql(.enc).{gz,bzip2}
    #    weekly_all-databases_DBNAME_2011-08-14_18h12m_32.sql(.enc).{gz,bzip2}
    #    daily_all-databases_DBNAME_2011-08-14_18h12m_7.sql(.enc).{gz,bzip2}

    echo "======================================================================"
    echo "AutoMySQLBackup version ${version}"
    echo "http://sourceforge.net/projects/automysqlbackup/"
    echo
    echo "Backup of Database Server - ${CONFIG_mysql_dump_host_friendly:-$CONFIG_mysql_dump_host}"
    ( IFS=,; echo "Databases - ${CONFIG_db_names[*]}" )
    ( IFS=,; echo "Databases (monthly) - ${CONFIG_db_month_names[*]}" )
    echo "======================================================================"

    # -> preback commands
    if [[ "${CONFIG_prebackup}" ]]; then
        echo "======================================================================"
        echo "Prebackup command output."
        echo
        source ${CONFIG_prebackup}
        echo
        echo "======================================================================"
        echo
    fi
    # <- preback commands

    # -> backup local files
    if [[ "${CONFIG_backup_local_files[@]}" ]] && [[ ${CONFIG_do_weekly} != 0 && ${date_dayno_of_week} = ${CONFIG_do_weekly} ]] && (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/backup_local_files/bcf_weekly_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m_${date_weekno}.tar${suffix}"); ((! ${#f[@]}))); then
        echo "======================================================================"
        echo "Backup local files. Doing this weekly on CONFIG_do_weekly."
        echo
        backup_local_files "${CONFIG_backup_dir}/backup_local_files/bcf_weekly_${datetimestamp}_${date_weekno}.tar"
        tmp_flags=$?; var=;
        if (( $? == 0 )); then
          echo "success!"
          backupfiles=( "${backupfiles[@]}" "${CONFIG_backup_dir}/backup_local_files/bcf_weekly_${datetimestamp}_${date_weekno}.tar" )
        else
          let "E |= $E_backup_local_failed"
          echo "failed!"
        fi
        echo
        echo "======================================================================"
        echo
    fi
    # <- backup local files

    # -> dump full schema
    if [[ "${CONFIG_mysql_dump_full_schema}" = 'yes' ]]; then
        echo "======================================================================"
        echo "Dump full schema."
        echo

        # monthly
        if (( ${CONFIG_do_monthly} != 0 && (${date_day_of_month} == ${CONFIG_do_monthly} || $date_day_of_month == $date_lastday_of_this_month && $date_lastday_of_this_month < ${CONFIG_do_monthly}) )) && (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/fullschema/fullschema_monthly_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m_${date_month}.sql${suffix}"); ((! ${#f[@]}))); then
          fullschema "${CONFIG_backup_dir}/fullschema/fullschema_monthly_${datetimestamp}_${date_month}.sql"
          if (( $? == 0 )); then
            echo "Rotating $(( ${CONFIG_rotation_monthly}/31 )) month backups for ${mdb}"
            if (( $CONFIG_dryrun )); then
              find "${CONFIG_backup_dir}/fullschema" -mtime +"${CONFIG_rotation_monthly}" -type f -name 'fullschema_monthly*' -exec echo "dry-running: rm" {} \;
            else
              find "${CONFIG_backup_dir}/fullschema" -mtime +"${CONFIG_rotation_monthly}" -type f -name 'fullschema_monthly*' -exec rm {} \;
            fi
            files_postprocessing "${CONFIG_backup_dir}/fullschema/fullschema_monthly_${datetimestamp}_${date_month}.sql${suffix}"
            tmp_flags=$?; var=; (( $tmp_flags & $flags_files_postprocessing_success_encrypt )) && var=.enc
            backupfiles=( "${backupfiles[@]}" "${CONFIG_backup_dir}/fullschema/fullschema_monthly_${datetimestamp}_${date_month}.sql${suffix}${var}" )
          else
            let "E |= $E_dump_fullschema_failed"
          fi
        fi

        # weekly
        if [[ ${CONFIG_do_weekly} != 0 && ${date_dayno_of_week} = ${CONFIG_do_weekly} ]] && (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/fullschema/fullschema_weekly_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m_${date_weekno}.sql${suffix}"); ((! ${#f[@]}))); then
          fullschema "${CONFIG_backup_dir}/fullschema/fullschema_weekly_${datetimestamp}_${date_weekno}.sql"
          if (( $? == 0 )); then
            echo "Rotating $(( ${CONFIG_rotation_monthly}/31 )) month backups for ${mdb}"
            if (( $CONFIG_dryrun )); then
              find "${CONFIG_backup_dir}/fullschema" -mtime +"${CONFIG_rotation_weekly}" -type f -name 'fullschema_weekly*' -exec echo "dry-running: rm" {} \;
            else
              find "${CONFIG_backup_dir}/fullschema" -mtime +"${CONFIG_rotation_weekly}" -type f -name 'fullschema_weekly*' -exec rm {} \;
            fi
            files_postprocessing "${CONFIG_backup_dir}/fullschema/fullschema_weekly_${datetimestamp}_${date_weekno}.sql${suffix}"
            tmp_flags=$?; var=; (( $tmp_flags & $flags_files_postprocessing_success_encrypt )) && var=.enc
            backupfiles=( "${backupfiles[@]}" "${CONFIG_backup_dir}/fullschema/fullschema_weekly_${datetimestamp}_${date_weekno}.sql${suffix}${var}" )
          else
            let "E |= $E_dump_fullschema_failed"
          fi
        fi

        # daily
        fullschema "${CONFIG_backup_dir}/fullschema/fullschema_daily_${datetimestamp}_${date_day_of_week}.sql"
        if (( $? == 0 )); then
          echo "Rotating $(( ${CONFIG_rotation_monthly}/31 )) month backups for ${mdb}"
          if (( $CONFIG_dryrun )); then
            find "${CONFIG_backup_dir}/fullschema" -mtime +"${CONFIG_rotation_daily}" -type f -name 'fullschema_daily*' -exec echo "dry-running: rm" {} \;
          else
            find "${CONFIG_backup_dir}/fullschema" -mtime +"${CONFIG_rotation_daily}" -type f -name 'fullschema_daily*' -exec rm {} \;
          fi
          files_postprocessing "${CONFIG_backup_dir}/fullschema/fullschema_daily_${datetimestamp}_${date_day_of_week}.sql${suffix}"
          tmp_flags=$?; var=; (( $tmp_flags & $flags_files_postprocessing_success_encrypt )) && var=.enc
          backupfiles=( "${backupfiles[@]}" "${CONFIG_backup_dir}/fullschema/fullschema_daily_${datetimestamp}_${date_day_of_week}.sql${suffix}${var}" )
        else
          let "E |= $E_dump_fullschema_failed"
        fi
        echo
        echo "======================================================================"
        echo
      
    fi
    # <- dump full schema

    # -> dump status
    if [[ "${CONFIG_mysql_dump_dbstatus}" = 'yes' ]]; then
        echo "======================================================================"
        echo "Dump status."
        echo

        # monthly
        if (( ${CONFIG_do_monthly} != 0 && (${date_day_of_month} == ${CONFIG_do_monthly} || $date_day_of_month == $date_lastday_of_this_month && $date_lastday_of_this_month < ${CONFIG_do_monthly}) )) && (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/status/status_monthly_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m_${date_month}.txt${suffix}"); ((! ${#f[@]}))); then
          dbstatus "${CONFIG_backup_dir}/status/status_monthly_${datetimestamp}_${date_month}.txt"
          if (( $? == 0 )); then
            echo "Rotating $(( ${CONFIG_rotation_monthly}/31 )) month backups for ${mdb}"
            if (( $CONFIG_dryrun )); then
              find "${CONFIG_backup_dir}/status" -mtime +"${CONFIG_rotation_monthly}" -type f -name 'status_monthly*' -exec echo "dry-running: rm" {} \;
            else
              find "${CONFIG_backup_dir}/status" -mtime +"${CONFIG_rotation_monthly}" -type f -name 'status_monthly*' -exec rm {} \;
            fi
            files_postprocessing "${CONFIG_backup_dir}/status/status_monthly_${datetimestamp}_${date_month}.txt${suffix}"
            tmp_flags=$?; var=; (( $tmp_flags & $flags_files_postprocessing_success_encrypt )) && var=.enc
            backupfiles=( "${backupfiles[@]}" "${CONFIG_backup_dir}/status/status_monthly_${datetimestamp}_${date_month}.txt${suffix}${var}" )
          else
            let "E |= $E_dump_status_failed"
          fi
        fi

        # weekly
        if [[ ${CONFIG_do_weekly} != 0 && ${date_dayno_of_week} = ${CONFIG_do_weekly} ]] && (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/status/status_weekly_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m_${date_weekno}.txt${suffix}"); ((! ${#f[@]}))); then
          dbstatus "${CONFIG_backup_dir}/status/status_weekly_${datetimestamp}_${date_weekno}.txt"
          if (( $? == 0 )); then
            echo "Rotating $(( ${CONFIG_rotation_monthly}/31 )) month backups for ${mdb}"
            if (( $CONFIG_dryrun )); then
              find "${CONFIG_backup_dir}/status" -mtime +"${CONFIG_rotation_weekly}" -type f -name 'status_weekly*' -exec echo "dry-running: rm" {} \;
            else
              find "${CONFIG_backup_dir}/status" -mtime +"${CONFIG_rotation_weekly}" -type f -name 'status_weekly*' -exec rm {} \;
            fi
            files_postprocessing "${CONFIG_backup_dir}/status/status_weekly_${datetimestamp}_${date_weekno}.txt${suffix}"
            tmp_flags=$?; var=; (( $tmp_flags & $flags_files_postprocessing_success_encrypt )) && var=.enc
            backupfiles=( "${backupfiles[@]}" "${CONFIG_backup_dir}/status/status_weekly_${datetimestamp}_${date_weekno}.txt${suffix}${var}" )
          else
            let "E |= $E_dump_status_failed"
          fi
        fi

        # daily
        dbstatus "${CONFIG_backup_dir}/status/status_daily_${datetimestamp}_${date_day_of_week}.txt"
        if (( $? == 0 )); then
          echo "Rotating $(( ${CONFIG_rotation_monthly}/31 )) month backups for ${mdb}"
          if (( $CONFIG_dryrun )); then
            find "${CONFIG_backup_dir}/status" -mtime +"${CONFIG_rotation_daily}" -type f -name 'status_daily*' -exec echo "dry-running: rm" {} \;
          else
            find "${CONFIG_backup_dir}/status" -mtime +"${CONFIG_rotation_daily}" -type f -name 'status_daily*' -exec rm {} \;
          fi
          files_postprocessing "${CONFIG_backup_dir}/status/status_daily_${datetimestamp}_${date_day_of_week}.txt${suffix}"
          tmp_flags=$?; var=; (( $tmp_flags & $flags_files_postprocessing_success_encrypt )) && var=.enc
          backupfiles=( "${backupfiles[@]}" "${CONFIG_backup_dir}/status/status_daily_${datetimestamp}_${date_day_of_week}.txt${suffix}${var}" )
        else
          let "E |= $E_dump_status_failed"
        fi
        echo
        echo "======================================================================"
        echo
      
    fi
    # <- dump status

    # -> BACKUP DATABASES
      echo "Backup Start Time `date`"
      echo "======================================================================"

      ## <- monthly backup, unique per month
      if (( ${CONFIG_do_monthly} != 0 && (${date_day_of_month} == ${CONFIG_do_monthly} || $date_day_of_month == $date_lastday_of_this_month && $date_lastday_of_this_month < ${CONFIG_do_monthly}) )); then
          echo "Monthly Backup ..."
          echo

          subfolder="monthly"
          prefix="monthly_"
          midfix="_${date_month}"
          extension=".sql"
          rotation="${CONFIG_rotation_monthly}"
          rotation_divisor="31"
          rotation_string="month"

          if [[ "${CONFIG_mysql_dump_use_separate_dirs}" = "yes" ]]; then
            for db in "${CONFIG_db_month_names[@]}"; do
              echo "Monthly Backup of Database ( ${db} )"
              (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/${subfolder}/${db}/${prefix}${db}_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m${midfix}${extension}${suffix}"); ((${#f[@]}))) && continue
              process_dbs "$subfolder" "$prefix" "$midfix" "$extension" "$rotation" "$rotation_divisor" "$rotation_string" 0 "$db"
              echo ----------------------------------------------------------------------
            done
          else
              echo "Monthly backup of databases ( ${CONFIG_db_month_names[@]} )."
              (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/${subfolder}/${prefix}all-databases_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m${midfix}${extension}${suffix}"); ((${#f[@]}))) &&
                process_dbs "$subfolder" "$prefix" "$midfix" "$extension" "$rotation" "$rotation_divisor" "$rotation_string" 1 "${CONFIG_db_month_names[@]}"
              echo "----------------------------------------------------------------------"
          fi
      fi
      ## <- monthly backup

      ## <- weekly backup, unique per week
      if (( ${CONFIG_do_weekly} != 0 && ${date_dayno_of_week} == ${CONFIG_do_weekly} )); then
          echo "Weekly Backup ..."
          echo

          subfolder="weekly"
          prefix="weekly_"
          midfix="_${date_weekno}"
          extension=".sql"
          rotation="${CONFIG_rotation_weekly}"
          rotation_divisor="7"
          rotation_string="week"
          if [[ "${CONFIG_mysql_dump_use_separate_dirs}" = "yes" ]]; then
            for db in "${CONFIG_db_names[@]}"; do
              echo "Weekly Backup of Database ( ${db} )"
              (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/${subfolder}/${db}/${prefix}${db}_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m${midfix}${extension}${suffix}"); ((${#f[@]}))) && continue
              process_dbs "$subfolder" "$prefix" "$midfix" "$extension" "$rotation" "$rotation_divisor" "$rotation_string" 0 "$db"
              echo "----------------------------------------------------------------------"
            done
          else
            echo "Weekly backup of databases ( ${CONFIG_db_names[@]} )."
            (shopt -s nullglob dotglob; f=("${CONFIG_backup_dir}/${subfolder}/${prefix}all-databases_${date_stamp}_"[0-9][0-9]"h"[0-9][0-9]"m${midfix}${extension}${suffix}"); ((${#f[@]}))) &&
              process_dbs "$subfolder" "$prefix" "$midfix" "$extension" "$rotation" "$rotation_divisor" "$rotation_string" 1 "${CONFIG_db_names[@]}"
            echo "----------------------------------------------------------------------"
          fi
      fi
      ## <- weekly backup

      ## -> daily backup, test (( 1 )) is always true, just creates a grouping for Kate, which can be closed ^^
      if (( 1 )); then
          echo "Daily Backup ..."
          echo

          subfolder="daily"
          prefix="daily_"
          midfix="_${date_day_of_week}"
          extension=".sql"
          rotation="${CONFIG_rotation_daily}"
          rotation_divisor="1"
          rotation_string="day"

          if [[ "${CONFIG_mysql_dump_use_separate_dirs}" = "yes" ]]; then
            for db in "${CONFIG_db_names[@]}"; do
              echo "Daily Backup of Database ( ${db} )"
              process_dbs "$subfolder" "$prefix" "$midfix" "$extension" "$rotation" "$rotation_divisor" "$rotation_string" 0 "$db"
              echo "----------------------------------------------------------------------"
            done
          else
            echo "Daily backup of databases ( ${CONFIG_db_names[@]} )."
            process_dbs "$subfolder" "$prefix" "$midfix" "$extension" "$rotation" "$rotation_divisor" "$rotation_string" 1 "${CONFIG_db_names[@]}"
            echo "----------------------------------------------------------------------"
          fi
      fi
      ## <- daily backup

      echo
      echo "Backup End Time `date`"
      echo "======================================================================"
    # <- BACKUP DATABASES

    # -> clean latest filenames
    [[ "${CONFIG_mysql_dump_latest_clean_filenames}" = 'yes' ]] && find "${CONFIG_backup_dir}"/latest/ -type f -exec bash -c 'remove_datetimeinfo "$@"' -- {} \;
    # <- clean latest filenames

    # -> finished information
    echo "Total disk space used for backup storage..."
    echo "Size - Location"
    echo `du -hsH "${CONFIG_backup_dir}"`
    echo
    echo "======================================================================"
    # <- finished information

    # -> postbackup commands
    if [[ "${CONFIG_postbackup}" ]];then
        echo "======================================================================"
        echo "Postbackup command output."
        echo
        source ${CONFIG_postbackup}
        echo
        echo "======================================================================"
    fi
    # <- postbackup commands

    if [[ -s "$log_errfile" ]];then status=1; else status=0; fi

    exit ${status}

}

# @return    variable method_list_manifest_entries_array
method_list_manifest_entries () {
    local files files_master files_manifest file db manifest_files manifest_files_db selected_dbs i z l master_flags master to_rm actions
    manifest_entries_corrupted=()
    manifest_entries_user_action_required=()
    files=()
    files_master=()
    files_manifest=()
    master_flags=0
    let "filename_flag_encrypted=0x01"
    let "filename_flag_gz=0x02"
    let "filename_flag_bz2=0x04"
    let "filename_flag_diff=0x08"

    ##############################################################################################################
    # BEGIN __STARTUP

    load_default_config

    if [[ -r "${CONFIG_configfile}" ]]; then source "${CONFIG_configfile}"; echo "Parsed config file \"${CONFIG_configfile}\""; else let "N |= $N_config_file_missing"; fi; echo
    if (( $opt_flag_config_file )); then if [[ -r "${opt_config_file}" ]]; then source "${opt_config_file}"; let "N |= $N_arg_conffile_parsed"; else let "N |= $N_arg_conffile_unreadable"; fi; else let "N |= $N_too_many_args"; fi

    export LC_ALL=C
    PROGNAME=`basename $0`
    PATH=${PATH}:/usr/local/bin:/usr/bin:/bin:/usr/local/mysql/bin
    version=3.0
    fields=5 # manifest fields

    set_datetime_vars
    check_dependencies    # check for required programs
    parse_configuration    # parse configuration and set variables appropriately

    # BEGIN __MAIN
    unset manifest_files manifest_files_db i db
    while IFS= read -r -d '' file; do
      db="${file#/var/backup/db/@(daily|monthly|weekly|latest)/}";
      db="${db%/Manifest}";
      manifest_files_db[i]="$db"
      manifest_files[i++]="$file"
    done < <(find "${CONFIG_backup_dir}"/ -type f -name 'Manifest' -print0)

    extended_select 0 "Databases" "${manifest_files_db[@]}"
    declare -a selected_dbs=("${extended_select_return[@]}")

    for db in "${selected_dbs[@]}"; do
        selected_available_files=()
        for ((i=0;i<"${#manifest_files_db[@]}";i++)); do
          if [[ "x${manifest_files_db[i]}" = "x$db" ]]; then
            selected_available_files[j++]="${manifest_files[i]}"
          fi
        done
        if (( "${#selected_available_files[@]}" > 0 )); then
            extended_select 1 "$db" "${selected_available_files[@]}"
            declare -a selected_entries=("${extended_select_return[@]}")
            if (( "${#selected_entries[@]}" > 0 )); then
                for z in "${selected_entries[@]}"; do
                  parse_manifest "$z"
                  list=()
                  list_id=()
                  for ((i=0;$i<"${#manifest_array[@]}";i=$i+$fields)); do
                    if [[ "${manifest_array[i+3]}" != 0 ]]; then # only add differential backups
                      list=("${list[@]}" "${manifest_array[i]}")
                      list_id=("${list_id[@]}" "${manifest_array[i+3]}") # save rel_id, so we can retrieve the master backup file
                    fi
                  done
                  if (( "${#list[@]}" > 0 )); then
                    extended_select 1 "$z" "${list[@]}"
                    if (( "${#extended_select_return[@]}" > 0 )); then
                      for ((i=0;$i<"${#extended_select_return[@]}";i++)); do
                        if get_manifest_entry_by_id "${list_id[${extended_select_return_id[i]}]}"; then
                          files=("${files[@]}" "${extended_select_return[i]}")
                          files_master=("${files_master[@]}" "${manifest_entry[0]}")
                          files_manifest=("${files_manifest[@]}" "$z")
                        else
                          echo "no found master for id ${list_id[${extended_select_return_id[i]}]}"
                        fi
                      done
                    fi
                  fi
                done
            fi
        fi
    done
    # END _select_filenames
    declare -a method_list_manifest_entries_array=("${files[@]}")
    declare -a method_list_manifest_entries_array_master=("${files_master[@]}")
    declare -a method_list_manifest_entries_array_manifest=("${files_manifest[@]}")

    clear
    echo "You have selected the following files:"
    for i in "${files[@]}"; do printf '>>> %s\n' "$i"; done
    echo
    actions=('diff to full' 'remove files (also from Manifest)')
    extended_select 0 "Actions" "${actions[@]}"
    for action in "${extended_select_return[@]}"; do
        case "$action" in
            'diff to full')
                for ((l=0;$l<"${#files[@]}";l++)); do
                  # put the unpacking of the master file in here, so that in the case of multiple diffs with the same
                  # master file don't cause the script to unpack the same master file multiple times
                  master="${files_master[l]}"
                  diff="${files[l]}"
                  FileStub="${master%.@(sql|master)*}"
                  FileExt="${master#"$FileStub"}"
                  re=".*\.enc.*";        [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_encrypted"
                  re=".*\.gz.*";        [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_gz"
                  re=".*\.bz2.*";        [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_bz2"
                  re=".*\.diff.*";    [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_diff"
                  if (( $master_flags & $filename_flag_gz )); then
                    declare -a testarray=("${to_rm[@]}")
                    if ! in_array "${master%.gz}"; then
                      gzip_compression -dc "$master" > "${master%.gz}"
                      to_rm=("${to_rm[@]}" "${master%.gz}")
                    fi
                    master="${master%.gz}"
                  elif (( $master_flags & $filename_flag_bz2 )); then
                    declare -a testarray=("${to_rm[@]}")
                    if ! in_array "${master%.bz2}"; then
                      bzip2_compression -dc "$master" > "${master%.bz2}"
                      to_rm=("${to_rm[@]}" "${master%.bz2}")
                    fi
                    master="${master%.bz2}"
                  else
                    :
                  fi
                  method_diff_to_full "$master" "$diff"
                  #printf '%s\n>>> master: %s\n>>> manifest: %s\n' "${files[l]}" "${files_master[l]}" "${files_manifest[l]}"
                done
                # cleanup all unpacked master files ... the unpacked diff files are cleaned up by method_diff_to_full
                for i in "${to_rm[@]}"; do rm "$i"; done
            ;;
            'remove files (also from Manifest)')
                for ((l=0;$l<"${#files[@]}";l++)); do
                  if rm_manifest_entry_by_filename "${files_manifest[l]}" "${files[l]}" 1; then
                    rm "${files[l]}"
                  fi
                done
            ;;
            *)
              echo "Unrecognized option. This Should not happen! Error!"
            ;;
        esac
    done

    # END __MAIN
}

# @info:    Convert a differential backup file to a full one.
# @param:    master_backup_file diff_backup_file
method_diff_to_full() {
    local diff full diff_flags master_flags to_rm
    master="$1"
    diff="$2"
    diff_flags=0
    master_flags=0
    to_rm=()

    FileStub="${diff%.@(sql|diff)*}"
    FileExt="${diff#"$FileStub"}"
    re=".*\.enc.*";        [[ "$FileExt" =~ $re ]] && let "diff_flags|=$filename_flag_encrypted"
    re=".*\.gz.*";        [[ "$FileExt" =~ $re ]] && let "diff_flags|=$filename_flag_gz"
    re=".*\.bz2.*";        [[ "$FileExt" =~ $re ]] && let "diff_flags|=$filename_flag_bz2"
    re=".*\.diff.*";    [[ "$FileExt" =~ $re ]] && let "diff_flags|=$filename_flag_diff"
    FileStub="${master%.@(sql|master)*}"
    FileExt="${master#"$FileStub"}"
    re=".*\.enc.*";        [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_encrypted"
    re=".*\.gz.*";        [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_gz"
    re=".*\.bz2.*";        [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_bz2"
    re=".*\.diff.*";    [[ "$FileExt" =~ $re ]] && let "master_flags|=$filename_flag_diff"

    # TODO: Differential backup with encryption is not yet implemented!
    if (( $diff_flags & $filename_flag_encrypted )); then
      : #decrypt it
    fi

    if (( $master_flags & $filename_flag_encrypted )); then
      : #decrypt it
    fi

    if (( $diff_flags & $filename_flag_gz )); then
      gzip_compression -dc "$diff" > "${diff%.gz}"
      to_rm=("${to_rm[@]}" "${diff%.gz}")
      diff="${diff%.gz}"
    elif (( $diff_flags & $filename_flag_bz2 )); then
      bzip2_compression -dc "$diff" > "${diff%.bz2}"
      to_rm=("${to_rm[@]}" "${diff%.bz2}")
      diff="${diff%.bz2}"
    else
      :
    fi

    if (( $master_flags & $filename_flag_gz )); then
      gzip_compression -dc "$master" > "${master%.gz}"
      to_rm=("${to_rm[@]}" "${master%.gz}")
      master="${master%.gz}"
    elif (( $master_flags & $filename_flag_bz2 )); then
      bzip2_compression -dc "$master" > "${master%.bz2}"
      to_rm=("${to_rm[@]}" "${master%.bz2}")
      master="${master%.bz2}"
    else
      :
    fi

    patch "$master" "$diff" -o "${diff/diff/sql}"

    # cleanup
    for i in "${to_rm[@]}"; do rm "$i"; done
}

# END _methods

# BEGIN __main

NO_ARGS=0
E_OPTERROR=85

if (( $# == $NO_ARGS )); then   # Script invoked with no command-line args?
  echo "Invoking backup method."; echo; method_backup
fi

while getopts ":c:blh" Option
do
  case $Option in
    c     ) echo "Using \"$OPTARG\" as optional config file."; echo; opt_config_file="$OPTARG"; opt_flag_config_file=1;;
    b     ) echo "MySQL backup method invoked."; echo;  opt_flag_method_backup=1;;
    l      ) echo "List manifest entries."; echo; opt_flag_list_manifest_entries=1;;
    h      )    echo "Usage `basename $0` options -cblh"
            echo -e "-c CONFIG_FILE\tSpecify optional config file."
            echo -e "-b\tUse backup method."
            echo -e "-l\tList manifest entries."
            echo -e "-h\tShow this help."
            exit 0;;
    #n | o ) echo "Scenario #2: option -$Option-   [OPTIND=${OPTIND}]";;

    #q     ) echo "Scenario #4: option -q-\
    #                with argument \"$OPTARG\"   [OPTIND=${OPTIND}]";;
    #  Note that option 'q' must have an associated argument,
    #+ otherwise it falls through to the default.
    #r | s ) echo "Scenario #5: option -$Option-";;
    *     ) echo "Unimplemented option chosen.";;   # Default.
  esac
done

(( $opt_flag_method_backup )) && method_backup
(( $opt_flag_list_manifest_entries )) && method_list_manifest_entries

shift $(($OPTIND - 1))
#  Decrements the argument pointer so it points to next argument.
#  $1 now references the first non-option item supplied on the command-line
#+ if one exists.

# For backward compatibility. If no option items are present and only one non-option item is there, we expect it
# to be the optional config file and invoke the backup method.
opt_flags=( "${!opt_flag_@}" )    # array of all set variables starting with opt_flag_
if (( $# == 1 )) && (( ${#opt_flags[@]} == 0 )); then
  opt_config_file="$1"; opt_flag_config_file=1; method_backup
elif (( $# == 0 )) && (( ${#opt_flags[@]} == 0 )); then
  method_backup
fi

# END __main