#!/usr/bin/bash
#
# mywrapper 
# do multiple mysql backups in one shot
# 
# v1.1
# 

################
### pre-exec ###

IFS=$'\n'

help () {
  cat <<- HELPMSG
Usage:
  $(basename "${0}") [OPTIONS …] handy wrapper script for mydumper
  puts dumps into subdirectories. writes timestamps into stdout for each database dumped
Options:

    -h         # show this dialog

    -b         # destination folder for backups

    -d         # defaults file for both mysql and mydumper binary
    
    -D         # takes a batch of defaults files. path to folder with configs.
               # note: -d and -D are mutually exclusive! 
    
    -r         # rotate. old backups get deleted
               # by default, delete files older than
               # 5 days in day folder
               # 28 days in week folder
               # 180 days in month folder

    -s         # backup only to daily folder. use only daily rotation. s stands for simple
    
    -j         # janitor. switch on if you wish to only rotate previous backups

    -f         # full. on one file. with mysqldump. don't ask
    

HELPMSG
}

while getopts :b:d:D:srjfh opts 
do
  case "${opts}" in
    b)  BASEDIR="${OPTARG}";;
    d)  defaultsfiles+=("${OPTARG}");;
    D)  DEFAULTS_DIR="${OPTARG}";;
    s)  SIMPLE=1;;
    r)  ROTATE=1;;
    j)  JANITOR=1;;
    f)  FULL=1;;
    h)  HELP=1;;
    ?)  printf "%s\n" "stop using invalid options, man."
        help
        exit 2
    ;;
  esac
done

[[ "${HELP}" == 1 ]] && help && exit 0


###################
### definitions ###

extract_value_from_section() {
  local file=$1
  local section=$2
  local key=$3
  local inSection=false
  local value=""
  
  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ "$line" == "[$section]" ]]; then
      inSection=true
    elif $inSection && [[ "$line" =~ ^\[.*\] ]]; then
      break
    elif $inSection && [[ "$line" =~ ^$key= ]]; then
      value="${line#*=}"
      break
    fi
  done < "${file}"
  
  echo "${value}"
}

set_rota_vars () {
  if [[ ! "${SIMPLE}" == 1 ]]
  then
    case "${rota}" in
      month) 
        find_dir="${DBDIR}/month"
        rota_dir="${DBDIR}/month/${DATE}"
        rota_days=$((MROTA*30))
        ;;
      week)
        find_dir="${DBDIR}/week"
        rota_dir="${DBDIR}/week/${DATE}" 
        rota_days=$((WROTA*7))
        ;;
      day) 
        find_dir="${DBDIR}/day"
        rota_dir="${DBDIR}/day/${DATE}" 
        rota_days="${DROTA}"
        ;;
        esac
    else
    find_dir="${DBDIR}/day"
    rota_dir="${DBDIR}/day/${DATE}" 
    rota_days="${DROTA}"
  fi
    
  [[ ! -d "${rota_dir}" ]] && mkdir -p "${rota_dir}"
}

rotate () {
  printf "%s \n" "rotating ${dbhostname} database backups older than ${rota_days} days"
  find "${find_dir}"/* \
    -regextype egrep \
    -regex ".*(.|-)(sql|metadata)" \
    -mtime +"${rota_days}" \
    -type f \
      -exec rm -v {} \;
  printf "%s \n" "finished rotation of ${dbhostname}"
}

cleanup () {
  printf "%s \n" "removing old full dumps and other data from ${DBDIR}"
  find "${DBDIR}" \
    -type f \
    -regextype egrep \
    -regex ".*/(fulldump\.sql|metadata\.partial)" \
    -mtime +"${rota_days}" \
      -exec rm -v {} \;
  printf "%s \n" "finished removing :-)"

  printf "%s \n" "cleaning up old directories..."
  find "${DBDIR}" \
    -type d \
    -empty \
    -delete
  printf "%s \n" "cleanup completed"
}

MYDUMPER="/usr/bin/mydumper"
WEEKLY=1                                                # weekly backup on n-th day of week
DATE=$(date +%Y-%m-%d)                                  # Timestamp e.g 2002-09-21
DOW=$(date +%u)                                         # day of the week from 1-7, 1 is monday
DOM=$(date +%d)                                         # day of the nonth eg 27
DDOW=$((10#${DOW}))
DDOM=$((10#${DOM}))
DDIFF=$((WEEKLY-DDOW))
DROTA=5                                                 # delete files older than 5 days
WROTA=4                                                 # *7
MROTA=6                                                 # *30

####################
### set-rotation ###

case $((
  1*(DDOM == 1) +\
  2*((WEEKLY == DOW && DDOM != 1) || ( DDIFF == 1 && DDOM == 2))
  ))
  in 
  1) rota=month ;;
  2) rota=week ;;
  *) rota=day ;;
esac

################
### do-stuff ###
################

if [[ "${DEFAULTS_DIR}" ]]
then
  mapfile -t defaultsfiles < <(ls -1 "${DEFAULTS_DIR}")
fi

if [[ ! "${defaultsfiles[*]}" ]]
then
  printf "%s\n\n\n" "no defaults provided, exiting..."
  help
  exit 2
fi

# main loop for every defaults-file
for ((f=0;f<${#defaultsfiles[@]};f++))
do
  # Substitute
  if [[ "${DEFAULTS_DIR}" ]]
  then
    d="${DEFAULTS_DIR}/${defaultsfiles[f]}"
  else
    d="${defaultsfiles[f]}"
  fi

  if [[ -f "${d}" ]]
  then
    ARGS="--defaults-file=${d}"
    
    # get mysql hostnamen and test DB connection
    if mysql "${ARGS}" -N -e "select now();" >/dev/null
    then
      # get list of databases to backup
      mapfile -t databases < <(mysql "${ARGS}" -N -e "show databases" \
        | grep -Ev "^(information|performance)_schema$|^(test|sys)$")
      # get mysql hostname
      dbhostname="$(mysql "${ARGS}" -N -e "select @@hostname")"
    else
      printf "%s\n" "backup failed at $(date) - can't perform queries with defaults file ${d}"
      continue
    fi

    # Extract outputdir if present and set DBDIR appropriately
    outputdir=$(extract_value_from_section "${d}" "mywrapper" "outputdir")
    if [[ -n "${outputdir}" ]]; then
      DBDIR="${BASEDIR}/${outputdir}"
    else
      DBDIR="${BASEDIR}/${dbhostname}"
    fi
  else
    printf "%s\n" "${d} defaults file doesn't seem to be a file"
    printf "%s\n" "exiting!"
    help
    exit 2
  fi
  
  set_rota_vars

  # ich bin keine signatur ich putze hier nur
  if [[ "${JANITOR}" ]]
  then
    rotate
    cleanup
    continue 
  fi

  for ((i=0;i<${#databases[@]};i++))
  do
    # variablen substituieren damit code weniger hässlich wird
    db="${databases[i]}"
    re="--regex '^(${db}\.)'"
    output="${rota_dir}/${db}"
    backup="${MYDUMPER} ${ARGS} ${re} -o ${output}"
    
    # create subfolders
    [[ ! -d "${output}" ]] && mkdir -p "${output}"

    if eval "${backup}"
    then
      printf "%s\n" "${dbhostname} db ${db} dump +++++ Finished at $(date)"
    else
      printf "%s\n" "${dbhostname} db ${db} dump failed at $(date)"
    fi
    
    # ehy alter wie kann man eigentlich so blöd sein der function rotate
    # variablen zu geben die von einer anderen function in der loop definiert werden
    # aber die rotate function ausserhalb der loop setzen und sich dann wundern wieso
    # der scheiss nicht läuft xdddd
    if [[ "${ROTATE}" == 1  ]] 
    then
      rotate
      cleanup
    fi
  done

  # perform full dump
  if [[ "${FULL}" == 1 ]]
  then
    mysqldump \
      "${ARGS}" \
      --skip-lock-tables \
      --skip-set-charset \
      --default-character-set=utf8 \
      --single-transaction \
      --flush-logs \
      --routines \
      --databases "${databases[@]}" > "${rota_dir}/fulldump.sql"
  fi
  printf "%s\n" "${dbhostname} backup +++++ Finished at $(date)"
done
