#!/bin/bash
#
# Backup mit Hilfe von borgbackup
#
# Syntax:
# do_borgbackup
#         [deffile <path to default-File>]
#         [data <path to save>]
#          [domail {no | <email-Adresse>}]
#         [archivname <string>]
#         [target <string>]
#         [repopath <string>]
#         [doverify {no | local | <path to remote script>}]
#         [cmdlineexclude <path to exlcude file>]
#         [passphrase <string>]
#         [keypos {local | repo}]  (für init)
#         [doarchive {no | create | restorearchive | restorefiles | filelist | archivelist}]
#         [doprune {no | first | after}]
#         [selectrepo {last | last-1 | last-2 | <string>}]
#         [restoreselect <string>]
#               [restoreefi]
#               [restoreswap]
#               [restoreboot]
#               [restoredev]
#         [keepdaily {0..99}]
#         [keepweekly {0..99}]
#         [keepmonthly {0..99}]
#         [keepyearly {0..99}]
#         [doinitarchive {no | yes}]
#         [snapshotname <string>]
#         [version]
#
# Wenn ein <string> Leerzeichen enthält, müssen "" drumherum.
#
# ${BASEDIR}/borg_Doku/hk_history                # globale History

VERSION="2.0.2-20200903"

KONFIG_DIR="/etc/borgbackup"
FIRST_LOG="/tmp/borg_$(pidof -s -x $0) "

if [ -f ${KONFIG_DIR}/borg_default ]             # Abfrage, ob borg_default vorhanden
  then
    . ${KONFIG_DIR}/borg_default                 # default-File anziehen
  else
    echo "${KONFIG_DIR}/borg_default ist nicht vorhanden"
    exit 2
  fi

PRUNE_DEL="-del-"                                # Kennung für mögliche Löschung durch prune
BOOTSICHERUNG="/root/boot_sicherung"             # Pfad für Systemsicherungen aus gemounteten boot-Partitionen
LINIE="----------------------------------------"

BASEDIR=${0%/*}                                  # den Aufrufpfad des laufenden Skripts extrahieren
                                                 # Die verwendeten wildcards (* und ?) sind hier bereits aufgelöst
#BASEDIR=${dirname ${0}}                          # alternativ kann auch dies verwendet werden
if [[ "${BASEDIR}" != /* ]]
  then
    # Umstellen auf absoluten Pfad, damit die Verwendung unabhängig vom aktuellen Pfad klappt.
    BASEDIR="${PWD}/${BASEDIR}"
  fi
PRE_POST_BASEDIR="${BASEDIR}/borgbackup"

function execute_scripts () {
  IFS=","
  read -r -a  tab <<< "${1}"
  n=0
  while [ ${tab[$n]} ]
    do
      . ${PRE_POST_BASEDIR}/${tab[$n]}
      (( n += 1 ))
    done
  }

function exit_on_error () {
  echo -e "${1}\n${LINIE}" >> ${LOG_FILE}
  execute_scripts "${POST_ERROR}"                # Skripte zur Fehlerbehandlung
  cat ${LOG_FILE}
  exit 2
  }

function do_init () {
  #
  # Annahmen:
  # - Für ZIEL_RECHNER liegt ein ssh preshared key vor und ist eingerichtet
  #
  # Info:
  # - ssh-key-Verwendung
  #   + Durch den Eintrag des Remote-hosts in /home/<user>/.ssh/config kann "export BORG_RSH ssh -i <keyfile>"
  #     beim Aufruf von ssh entfallen (preshared keys).
  #
  GID=$(id -g)                                   # group-id des ausführenden Users. UID ist bash-build-in.
  echo -e "Init for Borg-Backup at $(date) \n${LINIE}\n" >> ${LOG_FILE}
  if [ -n "${ZIEL/*@*}" ]                        # Erkennung ob $ZIEL kein @ enthält und damit lokal ist
    then
      mkdir -p "${BORG_REPO}"
      chown -R ${GID}:${UID} ${ZIEL}             # Nur zur Sicherheit, sollte durch mkdir bereits ok sein.
    fi
  export BORG_NEW_PASSPHRASE=${BORG_PASSPHRASE}
  if [ ${KEYPOS} = "repo" ]
    then
      borg init --encryption=repokey-blake2 &>> ${LOG_FILE}               # Schlüssel liegt im Repo
    else
      borg init --encryption=keyfile-blake2 &>> ${LOG_FILE}               # Schlüssel liegt auf QUELLE
    fi
  if [ $? -ne 0 ]
    then
      exit_on_error "${LINIE}\nInitialiserung des Repositories fehlgeschlagen  $(date) "
    else
      echo -e "${LINIE}\nInitialiserung des Repositories erfolgreich  $(date) \n${LINIE}" >> ${LOG_FILE}
    fi
  }

function check_keep_val () {
  local aa
  if [ ${1} -lt 0 ]                              # Wertebereich der KEEP_* Parameter prüfen [0..99]
    then
      aa=0
  elif [ ${1} -gt 99 ]
    then
      aa=99
  else
      aa=${1}
  fi
  echo "${aa}"
  }

function do_prune () {
  KEEP_DAILY=$(check_keep_val ${KEEP_DAILY})
  KEEP_WEEKLY=$(check_keep_val ${KEEP_WEEKLY})
  KEEP_MONTHLY=$(check_keep_val ${KEEP_MONTHLY})
  KEEP_YEARLY=$(check_keep_val ${KEEP_YEARLY})
  execute_scripts "${PRE_PRUNE}"                 # Skripte zur Vorbereitung
  # Use the 'prune' subcommand to maintain archives of THIS machine. The '{hostname}-' prefix is very important to
  # limit prune's operation to this machine's archives and not apply to other machines' archives also.
  echo -e  "Pruning repository at $(date) \n${LINIE}" >> ${LOG_FILE}
  borg prune --stats --list --prefix ${REPO_NAME}${PRUNE_DEL} --show-rc \
       --keep-daily    ${KEEP_DAILY} \
       --keep-weekly   ${KEEP_WEEKLY}  \
       --keep-monthly  ${KEEP_MONTHLY} \
       --keep-yearly   ${KEEP_YEARLY}  &>> ${LOG_FILE}
  #  --keep-within  2d \
  #  --force \    force pruning of corrupted archives ist zu überlegen
  if [ $? -ne 0 ]
    then
      exit_on_error "${LINIE}\nFehler bei borg prune at $(date) "
    fi
  echo -e "${LINIE}\nEnde von prune at $(date) \n${LINIE}" >> ${LOG_FILE}
  execute_scripts "${POST_PRUNE}"                # Skripte zur Nachbereitung
  }

function check_quelle () {
  if  [ -f ${QUELL_PFAD} ]                       # warum -d nicht geht, weiß ich nicht
    then
      if [ ${UID} = 0 ] && [ $(id -g) = 0 ]
        then
          mkdir -p "${QUELL_PFAD}"               # Für Systemsicherungen das snapshot-Verzeichnis erstellen
        else
          exit_on_error "Quell-Verzeichnis nicht vorhanden"
        fi
    fi
  }

function do_create () {
  check_quelle
  execute_scripts "${PRE_CREATE}"                # Skripte zur Vorbereitung
  echo -e "Dynamische Exclude-List:\n${LINIE}" >> ${LOG_FILE}
  cat ${LOG_DIR}/all_exclude >> ${LOG_FILE}
  sync -f                                        # Damit alles auf den Speichermedien ist und nichts mehr im Filecache.
  echo -e "${LINIE}\nstart create: $(date) " >> ${LOG_FILE}
  cd "${QUELL_PFAD}" >/dev/null                  # Wechsel ins Quell-Verzeichnis, damit relative Pfade möglich werden
                                                 # borg schneidet ohnehin den führenden "/" weg
  borg create --stats --show-rc --compression zstd,15 --exclude-caches \
       --exclude-from ${KONFIG_DIR}/borg_exclude \
       --exclude-from ${KONFIG_DIR}/${CMDLINE_EXCLUDE} \
       --exclude-from ${LOG_DIR}/all_exclude \
       ::${REPO_NAME}${PRUNE_DEL}"{now}" . &>> ${LOG_FILE}
  # {now}: Erweitert den Archivname um das aktuelle Datum im Format %Y-%m-%dT%H:%M:%S
  #         Ansonsten sind wiederholte Aufrufe des Skripts nicht möglich (Archive ... already exists)
  # ${PRUNE_DEL}: ist Teil des Archivnames und kennzeichnet, daß dies mit prune gelöscht werden kann
  #               Alles ohne diesen Namensteil wird von "borg prune" nicht berücksichtig => langfristiges Backup
  # Ein generelles Ausschließen von gemounteten Partitionen nur sinnvoll, wenn davon nichts regelmäßig ins Backup soll.
  # --files-cache MODE operate files cache in MODE. default: ctime,size,inode
  # --files-cache disabled: alle File werden als verändert angenommen
  if [ $? -ne 0 ]
    then
      exit_on_error "${LINIE}\nFehler bei borg create $(date) "
    fi
  cd ~ >/dev/null                                # Pfad zurückstellen, da prune bei btrfs-snapshots ansonsten nicht funktioniert
  echo "${LINIE}" >> ${LOG_FILE}
  execute_scripts "${POST_CREATE}"               # Skripte zur Nachbereitung
  }

function do_check () {
  # Es werden immer alle verfügbaren Archive überprüft
  resultText="fehlgeschlagen"                    # preset-Text für Mail
  execute_scripts "${PRE_CHECK}"                 # Skripte zur Vorbereitung
  echo -e "Check all created archives at $(date) \n${LINIE}" >> ${LOG_FILE}
  if [ ${DO_VERIFY} = "local" ]
    then
      borg list --format="{archive} | finished: {end} {NL}" >> ${LOG_FILE}
      # if you use a remote repo server via ssh:, the archive check is executed on the client machine (because if
      # encryption is enabled, the checks will require decryption and this is always done client-side, because key
      # access will be required).
      borg check --show-rc --verify-data --sort-by timestamp --prefix ${REPO_NAME} &>> ${LOG_FILE}
      GLOBAL_EXIT=$?
    else
      echo -e "Verify direct on remote system at $(date) \n${LINIE}" >> ${LOG_FILE}
      cat > ${LOG_DIR}/remote_verify << EOT
#!/bin/bash
#
# Check borg Archiv auf dem RePo-Server
#
LOG_FILE="${LOG_DIR}/borgRemoteCheck.log"
export BORG_REPO="/${ZIEL#*/}${REPO_PFAD}"
export BORG_PASSPHRASE="${REPO_PASSPHRASE}"

mkdir -p "${LOG_DIR}"
echo -e "Direct Remote Borg-check at \`date\`\n${LINIE}" > \${LOG_FILE}
borg list --format="{archive} | finished: {end} {NL}" >> \${LOG_FILE}
echo "${LINIE}" >> \${LOG_FILE}
borg check --show-rc --verify-data --sort-by timestamp --prefix ${REPO_NAME} &>> \${LOG_FILE}
echo "Ergebnis: \$?" >> \${LOG_FILE}
echo -e "Check abgeschlossen at \`date\`" >> \${LOG_FILE}
cat \${LOG_FILE}                                 # zum Befüllen des gemeinsamen LOG_FILEs
EOT
      chmod 755 ${LOG_DIR}/remote_verify
      ssh ${ZIEL%%/*} "mkdir -p ${LOG_DIR}"                                         # Verzeichnis für lokale Binaries anlegen
      scp -p ${LOG_DIR}/remote_verify ${ZIEL%%/*}:${LOG_DIR} >> ${LOG_FILE}         # verify-Skript übertragen
      ssh ${ZIEL%%/*} "${LOG_DIR}/remote_verify" >> ${LOG_FILE}
      # Durch die Ausgabe des log-Files auf die remote-Console wird automatisch auch das lokale LOG_FILE befüllt
      GLOBAL_EXIT=$?
      scp -p ${ZIEL%%/*}:${LOG_DIR}/borgRemoteCheck.log ${LOG_DIR} >> ${LOG_FILE}   # Remote-Log-File zur Sicherheit holen
      if [ ${GLOBAL_EXIT} -eq 0 ]
        then
	  x="dummy-text"
          # ssh ${ZIEL%%/*} "rm -rf ${LOG_DIR}"                                       # auf dem Remotesystem aufräumen
        fi
    fi

  if [ ${GLOBAL_EXIT} -eq 0 ]
    then
      echo "Check of all Archives finished successfully" >> ${LOG_FILE}
      #flag success or error
      resultText="erfolgreich"
    elif [ ${GLOBAL_EXIT} -eq 1 ] ; then
      echo "Check of all Archives finished with warnings" >> ${LOG_FILE}
    else
      exit_on_error "Check of all Archives finished with errors"
    fi
  echo "${LINIE}" >> ${LOG_FILE}
  execute_scripts "${POST_CHECK}"                # Skripte zur Nachbereitung
  }

function select_archive () {
  n=0
  while IFS= read -r tab[$n]
    do
      (( n += 1 ))
    done <<< $(borg list --format="{archive}{NL}"|sed "s/^\(.*\)${PRUNE_DEL}//")    # {now} aller verfügbarer Archive im Array
  if [ $n -eq 0 ]
    then
      exit_on_error "Kein Archiv vorhanden"
    fi
  case ${REPO_SELECT} in
    "last")
      REPO_SELECT=${tab[$n-1]}
      ;;
    "last-1")
      if [ $n -lt 2 ]
        then
          REPO_SELECT=${tab[0]}
	else
          REPO_SELECT=${tab[$n-2]}
	fi
      ;;
    "last-2")
      if [ $n -le 3 ]
        then
          REPO_SELECT=${tab[$n-1]}               # für n=2 wird tab[1] an Stelle von tab[0] verwendet
	else
          REPO_SELECT=${tab[$n-3]}
	fi
      ;;
    *)
      ok=0
      n=0
      while [ ${tab[$n]} ]
        do
          if [ ${REPO_SELECT} = ${tab[$n]} ]
            then
	      ok=1
	      break
	    fi
          (( n += 1 ))
        done
      if [[ $ok -eq 0 ]]                          # Überprüfung, ob es überhaupt ein passendes Repo gibt!
        then
          exit_on_error "Kein passendes Archiv vorhanden"
        fi
      ;;
  esac
  echo "Archiv: <${REPO_NAME}${PRUNE_DEL}${REPO_SELECT}>" >> ${LOG_FILE}
  }

function do_filelist () {
  echo -e "start filelist: $(date) \n${LINIE}" >> ${LOG_FILE}
  select_archive
  echo -e "File-Liste für ${REPO_NAME}${PRUNE_DEL}${REPO_SELECT}\n" > ${LOG_DIR}/file_list
  echo "Attribute  GID     UID    Größe               Datum                 Filename" >> ${LOG_DIR}/file_list
  echo "---------------------------------------------------------------------------------------" >> ${LOG_DIR}/file_list
  borg list ::${REPO_NAME}${PRUNE_DEL}${REPO_SELECT} \
      --format="{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NEWLINE}" &>> ${LOG_DIR}/file_list
  echo "${LINIE}" >> ${LOG_FILE}
  }

function do_archivelist () {
  echo -e "start archivelist: $(date) \n${LINIE}" >> ${LOG_FILE}
  select_archive
  echo -e "komplette Archiv-Liste für ${REPO_NAME}\n" > ${LOG_DIR}/archive_list
  borg list --format="{archive} | ID: {id} {NL}" &>> ${LOG_DIR}/archive_list
  echo "${LINIE}" >> ${LOG_FILE}
  }

function do_restore_archive () {
  mkdir -p "${QUELL_PFAD}"                       # Ziel erzeugen, falls noch nicht vorhanden
  check_quelle
  execute_scripts "${PRE_EXTRACT}"               # Skripte zur Vorbereitung
  echo -e "start archive restore: $(date) \n${LINIE}" >> ${LOG_FILE}
  select_archive
  # Berechtigung prüfen, ob mkdir zulässig für den aktuellen User
  cd "${QUELL_PFAD}" >/dev/null                  # Wechsel ins Ziel-Verzeichnis, damit relative Pfade möglich werden
                                                 # Currently, extract always writes into the current working directory (“.”)
  echo -e "Ausgewähltes Archiv: ${REPO_NAME}${PRUNE_DEL}${REPO_SELECT}\nAusgewählte Verzeichnisse: ${RESTORE_SELECT}" >> ${LOG_FILE}
  borg extract --show-rc \
       --exclude-from ${KONFIG_DIR}/borg_exclude \
       --exclude-from ${KONFIG_DIR}/${CMDLINE_EXCLUDE} \
       --exclude-from ${LOG_DIR}/all_exclude \
       ::${REPO_NAME}${PRUNE_DEL}${REPO_SELECT} &>> ${LOG_FILE}
  # --sparse interessant??
  if [ $? -ne 0 ]
    then
      exit_on_error "${LINIE}\nRestore des Repositories fehlgeschlagen $(date) "
    fi
  echo "${LINIE}" >> ${LOG_FILE}
  execute_scripts "${POST_EXTRACT}"              # Skripte zur Nachbereitung
  }

function do_fuse_mount () {
  echo -e "start fuse-mount: $(date) \n${LINIE}" >> ${LOG_FILE}
  if [ ! -d ${FUSE_POINT} ]                      # ggf. zusätzlich die Zugriffsmöglichkeit prüfen
    then
      exit_on_error "\n${LINIE}\nFuse-Mount-Point nicht vorhanden $(date) "
    fi
  select_archive
  borg mount --show-rc ::${REPO_NAME}${PRUNE_DEL}${REPO_SELECT} ${FUSE_POINT} &>> ${LOG_FILE}
  echo -e "unter ${FUSE_POINT} gemountet\n-${LINIE}" >> ${LOG_FILE}
  }

#----------------------------------------Main----------------------------------------
echo -e "Start of HK-Borg-Backup (${VERSION}) at $(date) \n${LINIE}\n" > ${FIRST_LOG}
. ${PRE_POST_BASEDIR}/borg_cmd_parameter >> ${FIRST_LOG}   # Besetzung der default-Werte auf Grund der Übergabeparameter

mkdir -p "${LOG_DIR}"                            # jetzt das endgültige LOG_DIR anlegen
if [ -O ${LOG_DIR} -a -G ${LOG_DIR} -o ${UID}=0 ]          # Test, ob LOG_DIR zum running user/group gehört oder root
  then
    LOG_FILE="${LOG_DIR}/borgBackupResult.log"
    MAILFILE="${LOG_DIR}/borgBackupResultMail.log"
    mv ${FIRST_LOG} ${LOG_FILE}                # Geschehen vor LOG_FILE sichern
  else
    LOG_FILE="${FIRST_LOG}"
    exit_on_error "LOG_Verzeichnis ${LOG_DIR} nicht zugreifbar"
  fi

#----------------------------------------
# Environment vorbesetzen
if [ -n "${ZIEL/*@*}" ]                          # Erkennung ob $ZIEL ein @ enthält und damit remote ist
  then
    # lokales Repository
    export BORG_REPO=${ZIEL}${REPO_PFAD}
    if [ ${DO_VERIFY} = "remote" ] ; then ${DO_VERIFY}="local" ; fi       # lokales RePo und remote verifay paßt nicht.
  else
    # remote Repository
    export BORG_REPO="ssh://${ZIEL%%/*}:22//${ZIEL#*/}${REPO_PFAD}"
  fi
export BORG_PASSPHRASE=${REPO_PASSPHRASE}
. ${KONFIG_DIR}/borg_env_print >> ${LOG_FILE}      # Kontrollausgabe der Konfigurationsparameter

#----------------------------------------
execute_scripts "${PRE_BACKUP}"
#----------------------------------------
if [ ${DO_INITARCHIVE} = "yes" ] ; then  do_init ; fi                     # init Archiv
if [ ${DO_PRUNE} = "first" ] ; then  do_prune ; fi                        # prune Archiv
if [ ${DO_ARCHIVE} = "create" ] ; then  do_create ; fi                    # create Archiv
if [ ${DO_PRUNE} = "after" ] ; then  do_prune ; fi                        # prune Archiv
if [ ${DO_VERIFY} != "no" ] ; then  do_check ; fi                         # verify all created backups
if [ ${DO_ARCHIVE} = "filelist" ] ; then  do_filelist ; fi                # komplette File-Liste
if [ ${DO_ARCHIVE} = "archivelist" ] ; then  do_archivelist ; fi          # komplettes Archiv-Liste
if [ ${DO_ARCHIVE} = "restorearchive" ] ; then  do_restore_archive ; fi   # restore komplettes Archiv
if [ ${DO_ARCHIVE} = "fuse" ] ; then  do_fuse_mount ; fi                  # fuse-mount eines Archives
#----------------------------------------
execute_scripts "${POST_BACKUP}"
#----------------------------------------
echo -e "Borg-Backup script finished at $(date) \n${LINIE}" >> ${LOG_FILE}
