#!/bin/bash
# ======================================================================
#
# BACKUP SCRIPTS - CREATE LOCAL DUMPS
#
# ----------------------------------------------------------------------
# ah - Axel Hahn <axel.hahn@iml.unibe.ch>
# ds - Daniel Schueler <daniel.schueler@iml.unibe.ch>
#
# 2016-11-04  ah,ds  created
# 2016-11-10  .....  v1.0
# 2017-03-27  .....  added first param for mode backup|restore
# 2018-02-09  .....  fix: restore-selection of target uses default on return only
# 2021-05-18  .....  move supported backup types to plugins/localdump/[service].sh
# 2021-07-13  .....  remove leading ./ in localdump.sh restore
# 2022-02-18  .....  WIP: use class like functions
# 2022-03-17  .....  WIP: add lines with prefix __DB__
# 2022-11-04  ah     rename hooks
# ======================================================================

# --- variables:
# BACKUP_BASEDIR      {string}  base directory for db dumps
# BACKUP_DATE         {string}  string with current timestamp; will be part of filename for backups
# BACKUP_KEEP_DAYS    {int}     count of days how long to keep db dumps below $BACKUP_BASEDIR
# BACKUP_PLUGINDIR    {string}  scripts for supported databases; [APP]/plugins/localdump
# BACKUP_SCRIPT       {string}  script name of db service
# BACKUP_TARGETDIR    {string}  target directory db dumps of current service
# SERVICENAME         {string}  name of db service (one of mysql|pgsql|...)

# ----------------------------------------------------------------------
# CONFIG VARS
# ----------------------------------------------------------------------

  . $(dirname $0)/vendor/ini.class.sh    || exit 1  
  . $(dirname $0)/vendor/color.class.sh  || exit 1

  . $(dirname $0)/includes/jobhelper.sh  || exit 1
  . $(dirname $0)/includes/inc_bash.sh   || exit 1

  . $(dirname $0)/includes/dbdetect.class.sh || exit 1

  if [ ! -r "${JOBFILE}" ]; then
    color.echo error "ERROR: missing config file ${JOBFILE}."
    exit 1
  fi

  LOCALDUMP_LOADED=1

  ARCHIVE_BASEDIR=
  BACKUP_BASEDIR=
  BACKUP_PLUGINDIR=

  DBD_DEBUG=0

  # Cleanup local dumps older N days
  typeset -i BACKUP_KEEP_DAYS=0

  BACKUP_DATE=

# ----------------------------------------------------------------------
# FUNCTIONS 4 DB-WRAPPER
# ----------------------------------------------------------------------


  # helpfer function for SERVICENAME.backup
  # it is called after the service specific dump was done.
  # param  {string}  filename of created dump file
  function db._compressDumpfile(){
    local _outfile=$1

    # $myrc is last returncode - set in fetchrc
    if [ $myrc -eq 0 ]; then
      echo -n "gzip $_outfile ... "
      gzip -9 -f "${_outfile}"
      fetchrc
    else
      color.echo error "ERROR occured while dumping - no gzip of $_outfile"
    fi
    # echo -n "__DB__$SERVICENAME INFO: backup to " 
    # ls -l "$_outfile"* 2>&1
    # echo
  }

# ----------------------------------------------------------------------
# FUNCTIONS 4 BACKUP
# ----------------------------------------------------------------------


  # ------------------------------------------------------------
  # cleanup a backup dir: remove old files and delete empty dirs
  function cleanup_backup_target(){
    if [ -d "${BACKUP_TARGETDIR}" ]; then
      h3 "CLEANUP ${BACKUP_TARGETDIR} older $BACKUP_KEEP_DAYS days ..."

      echo find "${BACKUP_TARGETDIR}" -mtime +$BACKUP_KEEP_DAYS -delete -print
      color.preset cmd
      find "${BACKUP_TARGETDIR}" -mtime +$BACKUP_KEEP_DAYS -delete -print
      color.reset

      if [ $(find "${BACKUP_TARGETDIR}" -type f | wc -l) -eq 0 ]; then
        echo "INFO: the directory is empty - deleting it"
        rm -rf "${BACKUP_TARGETDIR}"
      fi
    fi
  }

  # ------------------------------------------------------------
  # compress a file
  # shared function in localdump_*
  # param  string  filename of uncompressed output file
  function compress_file(){
    echo -n compressing $1 ...
    gzip -9 -f "${1}"
    fetchrc
  }

  # ------------------------------------------------------------
  # create a backup directory with name of service
  # shared function in localdump_*
  function create_targetdir(){
    mkdir -p "${BACKUP_TARGETDIR}" 2>/dev/null
    if [ ! -d "${BACKUP_TARGETDIR}" ]; then
      color.echo "error" "FATAL ERROR: directory ${BACKUP_TARGETDIR} was not created"
      exit 1
    fi
  }


  # ------------------------------------------------------------
  # generate a base filename for backup dump based on on db name
  # ... and added timestamp
  # param  string  name of database schema
  # --> see listBackupedDBs() and guessDB() - these function must be able to split this
  function get_outfile(){
    echo $*__${BACKUP_DATE}
  }

  # ------------------------------------------------------------
  # get name of a service script
  # param  string  name of a service 
  function get_service_script(){
    local _service=$1
    local _type; _type=$( dbdetect.getType "$_service" )
    ls -1 ${BACKUP_PLUGINDIR}/${_type}.sh 2>/dev/null
  }

  # ------------------------------------------------------------
  # get a list of existing database profiles
  function get_database_profiles(){
    for config in $(dbdetect.getConfigs); do
        if dbdetect.exists "$config"; then
            echo "$( dbdetect.getProfile $config )"
        fi
    done
  }

  # ------------------------------------------------------------
  # show directory infos with count of files and used space
  # show used space and count of files and dirs
  function show_info_backup_target(){
    if [ -d "${BACKUP_TARGETDIR}" ]; then
      h3 "INFO about backup target ${BACKUP_TARGETDIR}"

      echo -n "used space: "
      du -hs "${BACKUP_TARGETDIR}"

      echo -n "subdirs   : "
      find "${BACKUP_TARGETDIR}" -type d | wc -l

      echo -n "files     : "
      find "${BACKUP_TARGETDIR}" -type f | wc -l

      echo -n "free space: "
      df -h "${BACKUP_TARGETDIR}" | tail -1 | awk '{ print $4 }'
      echo
    fi
  }

# ----------------------------------------------------------------------
# FUNCTIONS 4 RESTORE
# ----------------------------------------------------------------------

  # ------------------------------------------------------------
  # restore: show profiles from that exist backups
  # global  string  BACKUP_BASEDIR  base directory of all backups
  function listBackupedServices(){
    (
      find "${BACKUP_BASEDIR}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;
      test -n "${ARCHIVE_BASEDIR}" && find "${ARCHIVE_BASEDIR}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;
    ) | sort -u
  }

  # ------------------------------------------------------------
  # restore: show databases that can be restored
  # param  string  file filter optional;
  function listBackupedDBs(){
    if [ -d "${BACKUP_TARGETDIR}" ]; then
      cd "${BACKUP_TARGETDIR}"
      if [ -z $1 ]; then
        find -type f | sed "s#__[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9].*##g" | grep -v "\.meta" | sort -ud| sed "s#^\./##g"
      else
        ls -ltr "$*__"* | sed "s#^\./##g" | grep -v "\.meta"
      fi
      cd - >/dev/null
    else
      color.echo error "ERROR: ${BACKUP_TARGETDIR} does not exist - here are no backups to restore."
      echo
      echo "You can try to restore dumps:"
      echo "1) Restore dump files from a backup set"
      echo "     $(dirname $0)/restore.sh $BACKUP_BASEDIR"
      echo "2) Copy restored dumps into $BACKUP_TARGETDIR"
      echo "3) Start database restore again"
      echo "     $(dirname $0)/localdump.sh restore [service]"
      echo
 
      exit 1
    fi
  }


  # ------------------------------------------------------------
  # guess name of the database file
  # param  string  filename of db dump; can be full path or not
  function guessDB(){
    dumpfile=$1

    # the metafile is written in sqlite backup to store full path
    metafile=${BACKUP_TARGETDIR}/${dumpfile}.meta
    if [ -f $metafile ]; then
      grep "^/" "$metafile" || grep "^  File: " "$metafile" | cut -c 9-
    else
      sBasename=$(basename $1)
      sDb=$(echo ${sBasename} | sed "s#__[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9].*##g")
      if [ -z $sDb ]; then
        color.echo error "ERROR: db name was not detected from file $1"
        exit 1
      fi
      echo $sDb
    fi
  }


  # read .meta file (that contains output of stats) and restore last owner and file permissions
  # param  string  filename of db dump
  # param  string  restored database file
  function restorePermissions(){
    local sMyMeta="${1}.meta"
    local sTargetfile="$2"
    if [ -f "${sMyMeta}" ]; then

      # Access: (0674/-rw-rwxr--)  Uid: ( 1000/    axel)   Gid: ( 1000/    axel)
      #          ^                                 ^                       ^
      #          _sPerm                            _sUser                  _sGroup
      local _sPerm=$(  grep "^Access: (" "${sMyMeta}" | cut -f 2 -d '(' | cut -f 1 -d '/')
      if [ -n "$_sPerm" ]; then
        local _sUser=$(  grep "^Access: (" "${sMyMeta}" | cut -f 3 -d '/' | cut -f 1 -d ')' | tr -d ' ')
        local _sGroup=$( grep "^Access: (" "${sMyMeta}" | cut -f 4 -d '/' | cut -f 1 -d ')' | tr -d ' ')

        echo -n "Restoring file owner $_sUser:$_sGroup and permissions $_sPerm ... "
        chown "$_sUser:$_sGroup" "${sTargetfile}" && chmod "$_sPerm" "${sTargetfile}"
        fetchrc
      fi
    fi    
  }

  # ------------------------------------------------------------
  # show help
  # ------------------------------------------------------------
  function showhelp(){
    local _self
    _self=$( basename "$0" )
    echo
    echo "LOCALDUMP detects existing local databases and dumps them locally."
    echo "It is included in the backup.sh to dump all before a file backup will store them."
    echo
    echo "It can be started seperately for manual database backups or for restore"
    echo
    echo "SYNTAX:"
    echo "  $_self [OPTIONS] <operation> <profile [more_profiles]>"
    echo
    echo "OPTIONS:"
    echo "  -h|--help   show this help"
    echo
    echo "PARAMETERS:"
    echo "  operation     - one of check|backup|restore; optional parameter"
    echo "                    backup   dump all databases/ schemes of a given service"
    echo "                    check    show info only if the service is available"
    echo "                    restore  import a dump into same or new database"
    echo "                             Without a filename it starts an interactive mode"
    echo "  profile     - name of database profiles"
    echo "                You get a list of all available services without parameter"
    echo "                Use ALL for bulk command"
    echo "  file        - filename of db dump to restore to origin database scheme"
    echo
    echo "EXAMPLES:"
    echo "  $_self backup"
    echo "  $_self backup ALL"
    echo "               Backup all databases of all found services"
    echo
    echo "  $_self backup mysql"
    echo "               Backup all Mysql databases."
    echo
    echo "  $_self restore"
    echo "               Start interactive restore of a database of any service."
    echo
    echo "  $_self restore sqlite"
    echo "               Start interactive restore of an sqlite database."
    echo
    echo "  $_self restore <file-to-restore> [<database-name>]"
    echo "               Restore a given dump file to the origin database scheme or"
    echo "               to a new/ other database with the given name."
  }


# ----------------------------------------------------------------------
# INIT
# ----------------------------------------------------------------------

  while [[ "$#" -gt 0 ]]; do case $1 in
      -h|--help)      showhelp; exit 0;;
      *) if grep "^-" <<< "$1" >/dev/null ; then
          echo; echo "ERROR: Unknown parameter: $1"; echo; showhelp; exit 2
        fi
        break;
        ;;
  esac; done

  mode=""
  case "$1" in
    backup|check|restore|shell)
      mode=$1
      shift 1
      ;;
  esac

  if [ -z "$mode" ]; then
    color.echo error "ERROR: missing parameter for operation."
    echo
    showhelp
    echo
    echo "Hint: On this machine working profiles:"
    get_database_profiles | nl
    echo
    exit 1
  fi


  # ----- init vars
  BACKUP_BASEDIR=$(_j_getvar "${JOBFILE}" "dir-localdumps")

  # check
  if [ -z "$BACKUP_BASEDIR" ]; then
    color.echo error "ERROR: missing config for backup target."
    echo There must be an entry dir-localdumps in ${JOBFILE}
    exit 1
  fi
  ARCHIVE_BASEDIR=$(_j_getvar "${JOBFILE}" dir-dbarchive)

  BACKUP_PLUGINDIR=$(dirname $0)/plugins/localdump
  DBD_BASEDIR=$BACKUP_PLUGINDIR/profiles

  BACKUP_KEEP_DAYS=$(_j_getvar ${JOBFILE} "keep-days")

  if [ $BACKUP_KEEP_DAYS -eq 0 ]; then
    BACKUP_KEEP_DAYS=7
  fi
  BACKUP_DATE=$(/bin/date +%Y%m%d-%H%M)

  # ----- checks

  # . /usr/local/bin/inc_cronfunctions.sh
  j_requireUser "root"

  h1 $(date) IML BACKUP :: LOCALDUMP :: $*


  export SERVICENAME=$1
  # BACKUP_TARGETDIR=${BACKUP_BASEDIR}/${SERVICENAME}
  # BACKUP_SCRIPT=$( get_service_script ${SERVICENAME} )

  case "$mode" in
    # ------------------------------------------------------------
    check)
      DBD_DEBUG=1
      for PROFILENAME in $(dbdetect.getConfigs)
      do
        echo "----- $PROFILENAME"
        dbdetect.exists "${PROFILENAME}"
        echo
      done
      # . $BACKUP_SCRIPT $mode
      ;;
    # ------------------------------------------------------------
    backup)
      if [ "$1" = "ALL" ] || [ -z "$1" ]; then

        profiles2run=$(get_database_profiles)
        echo AUTO: calling local backup scripts for all active profiles
        echo "$profiles2run" | nl
        echo
      else
        profiles2run=$*
      fi

      # ----- GO
      # PROFILENAME    mysql_localhost_13306
      # SERVICENAME    mysql
      #
      for PROFILENAME in $profiles2run
      do

        
        if dbdetect.setProfile "${PROFILENAME}"; then
          h2 "START PROFILE [${PROFILENAME}]"

          SERVICENAME=$( dbdetect.getType "$PROFILENAME" )
          BACKUP_PARAMS=$( dbdetect.getParams )

          BACKUP_TARGETDIR=${BACKUP_BASEDIR}/${PROFILENAME}
          ARCHIVE_DIR=${ARCHIVE_BASEDIR}/${PROFILENAME}
          BACKUP_SCRIPT=$( get_service_script ${SERVICENAME} )
  

          # ------ set env
          # echo "BACKUP_PARAMS = $BACKUP_PARAMS"
          # dbdetect.setenv
          eval $( dbdetect.setenv )

          _j_runHooks "200-before-db-service"

          h3 "BACKUP [${PROFILENAME}] -> ${SERVICENAME}"
          . $BACKUP_SCRIPT $mode

          test $rc -gt 0 && j_notify "db ${SERVICENAME}" "$BACKUP_SCRIPT $mode was finished with rc=$rc" $rc
          _j_runHooks "230-after-db-service" "$rc"

          # ------ unset env
          eval $( dbdetect.unssetenv )

          # ----- post jobs: cleanup
          cleanup_backup_target
          show_info_backup_target


        else

          echo "SKIP: profile '$PROFILENAME' "

          # see why it is not active
          DBD_DEBUG=1; dbdetect.setProfile "${PROFILENAME}"; echo; DBD_DEBUG=0

        fi

        # just to have it in the output
        dbdetect.validate

      done
      ;;

    # ------------------------------------------------------------
    restore)

      h1 "RESTORE DATABASE"

      if ! listBackupedServices | grep -q . ; then
        color.echo error "ERROR: No database dump was found in [${BACKUP_BASEDIR}] nor [${ARCHIVE_BASEDIR}]."
        exit 1
      fi

      if [ -z $1 ] || [ ! -f "$1" ]; then

        parService="$1"

        # ----- interactive selections

        h2 "Select profile that has a dump"

        if [ -z "${parService}" ]; then
          if [ "$( listBackupedServices | wc -l )" -eq 1 ]; then
            parService="$( listBackupedServices )"
            echo "Selecting the only existing profile: $parService"
          fi
        fi

        if [ -z "${parService}" ]; then
          listBackupedServices
          color.print input "Restore for profile name >"
          read -r parService
          test -z "$parService" && exit 1
        else
          echo "Taken from command line: $parService"
        fi

        # ----- check if profile exists
        if ! dbdetect.setProfile "${parService}"; then
          color.echo error "ERROR: profile [${parService}] is not known here (or database service is stopped)."
          echo
          echo "Existing database profiles:"
          get_database_profiles | nl
          echo
          exit 1
        fi

        # ----- check if dump exists in archive and in backup
        if [ -d "${BACKUP_BASEDIR}/${parService}" ] && [ -d "${ARCHIVE_BASEDIR}/${parService}" ]; then
          echo
          echo "Backup   ${BACKUP_BASEDIR}"
          echo "Archive  ${ARCHIVE_BASEDIR}"
          color.print input "Select a source directory >"
          read -r backupsource
          test -z "$backupsource" && exit 1
          BACKUP_BASEDIR="${backupsource}"
        else
          # just one test needed because BACKUP_BASEDIR is BACKUP_BASEDIR
          test -d "${ARCHIVE_BASEDIR}/${parService}" && BACKUP_BASEDIR="${ARCHIVE_BASEDIR}/${parService}"
        fi

        if [ ! -d "${BACKUP_BASEDIR}/${parService}" ]; then
          color.echo error "ERROR: profile ${parService} has no backup yet."
          exit 1
        fi

        BACKUP_TARGETDIR="${BACKUP_BASEDIR}/${parService}"

        h2 "Select database"
        listBackupedDBs
        color.print input "name of db to restore >"
        read -r fileprefix
        test -z "$fileprefix" && exit 1
        echo

        h2 "Select a specific dump for that database"
        listBackupedDBs $fileprefix
        color.print input "backupset to import >"
        read -r dbfile
        test -z "$dbfile" && exit 1
        echo

        sTargetDb=$(guessDB ${dbfile})
        color.print input "New database name [$sTargetDb] >"
        read -r sTargetDb
        if [ -z $sTargetDb ]; then
          sTargetDb=$(guessDB ${dbfile})
        fi
        echo

        sDumpfile="${BACKUP_TARGETDIR}/${dbfile}"
      else
        sDumpfile=$1
        sTargetDb=$2
      fi
      shift 2

      # ----- start restore

      if [ ! -f "${sDumpfile}" ]; then
        color.echo error "ERROR: [${sDumpfile}] is not a file"
        rc=$rc+1
      else

          PROFILENAME="${sDumpfile//${BACKUP_BASEDIR}/}"
          PROFILENAME="$( echo $PROFILENAME | sed "s,^/*,," | cut -f 1 -d '/')"

          if dbdetect.setProfile "${PROFILENAME}"; then

            SERVICENAME=$( dbdetect.getType "$PROFILENAME" )

            BACKUP_TARGETDIR=${BACKUP_BASEDIR}/${PROFILENAME}
            BACKUP_SCRIPT=$( get_service_script ${SERVICENAME} )

            BACKUP_PARAMS=$( dbdetect.getParams )
            eval $( dbdetect.setenv )
            h2 $BACKUP_SCRIPT $mode "${sDumpfile}" "${sTargetDb}"
            . $BACKUP_SCRIPT $mode "${sDumpfile}" "${sTargetDb}"

            if [ $? -ne 0 -o $rc -ne 0 ]; then
              color.echo error "ERROR: $mode failed. See ouput above. :-/"
            else
              color.echo ok "OK, $mode was successful."
            fi

            # ------ unset env
            eval $( dbdetect.unssetenv )
          else
            color.echo error "ERROR: Profile $PROFILENAME was detected but its database service is not available."
          fi

      fi
      ;;
    # ------------------------------------------------------------
    # shell)

    #   export BACKUP_TARGETDIR
    #   . $BACKUP_SCRIPT
    #   ( 
    #     mycmd=
    #     echo
    #     echo "Starting interactive shell..."
    #     echo
    #     echo "STATUS: STILL ALPHA as long existing db plugins are not rewritten."
    #     echo
    #     echo "INFO: Try ${SERVICENAME}.help to see database specific commands."
    #     echo "INFO: Type exit and return to leave the shell."
    #     echo
    #     while [ ! "$mycmd" = "exit" ]; do
    #       echo -n "[${SERVICENAME}]"
    #       color.print input " $( pwd )"
    #       echo -n " % "
    #       read -r mycmd
    #       if [ ! "$mycmd" = "exit" ];then
    #         color.preset cmd
    #         eval $mycmd
    #         color.reset
    #       fi
    #     done
    #   )
    #   ;;
    
    # ----- start restore
    *)
      color.echo error "ERROR: unknown command [$mode]"
      ;;
  esac

  echo _______________________________________________________________________________
  echo STATUS $0 exit with final returncode rc=$rc
  exit $rc

# ----------------------------------------------------------------------
