#
# 500_make_backup.sh
#

function set_tar_features () {
    # Default tar options
    TAR_OPTIONS=
    # Test for features in tar
    # true if at supports the --warning option (v1.23+)
    FEATURE_TAR_WARNINGS=
    local tar_version="$( get_version tar --version )"
    if version_newer "$tar_version" 1.23 ; then
        FEATURE_TAR_WARNINGS="y"
        TAR_OPTIONS+=" --warning=no-xdev"
    fi
    FEATURE_TAR_IS_SET=1
}

local backup_prog_rc

local scheme="$( url_scheme "$BACKUP_URL" )"
local path="$( url_path "$BACKUP_URL" )"
local opath="$( backup_path "$scheme" "$path" )"
test "$opath" && mkdir $v -p "$opath"

# In any case show an initial basic info what is currently done
# so that it is more clear where subsequent messages belong to:
LogPrint "Making backup (using backup method $BACKUP)"

# Verify that preconditions to make the backup are fulfilled and error out if not:
if is_true "$BACKUP_PROG_CRYPT_ENABLED" ; then
    # Backup archive encryption is only supported with 'tar':
    test "tar" = "$BACKUP_PROG" || Error "Backup archive encryption is only supported with BACKUP_PROG=tar"
    # Backup archive encryption is impossible without a BACKUP_PROG_CRYPT_KEY value.
    # Avoid that the BACKUP_PROG_CRYPT_KEY value is shown in debugscript mode
    # cf. the comment of the UserInput function in lib/_input-output-functions.sh
    # how to keep things confidential when usr/sbin/rear is run in debugscript mode
    # ('2>>/dev/$SECRET_OUTPUT_DEV' should be sufficient here because 'test' does not output on stdout):
    { test "$BACKUP_PROG_CRYPT_KEY" ; } 2>>/dev/$SECRET_OUTPUT_DEV || Error "BACKUP_PROG_CRYPT_KEY must be set for backup archive encryption"
    LogPrint "Encrypting backup archive with key defined in BACKUP_PROG_CRYPT_KEY"
fi

# Log what is included in the backup and what is excluded from the backup
# cf. backup/NETFS/default/400_create_include_exclude_files.sh
Log "Backup include list $TMP_DIR/backup-include.txt"
while read -r backup_include_item ; do
    test "$backup_include_item" && Log "  $backup_include_item"
done < $TMP_DIR/backup-include.txt
Log "Backup exclude list $TMP_DIR/backup-exclude.txt"
while read -r backup_exclude_item ; do
    test "$backup_exclude_item" && Log "  $backup_exclude_item"
done < $TMP_DIR/backup-exclude.txt

# Check if the backup needs to be split or not (on multiple ISOs).
# Dummy split command when the backup is not split (the default case).
# Let 'dd' read and write up to 1M=1024*1024 bytes at a time to speed up things
# for example from only 500KiB/s (with the 'dd' default of 512 bytes)
# via a 100MBit network connection to about its full capacity
# cf. https://github.com/rear/rear/issues/2369
SPLIT_COMMAND="dd of=$backuparchive bs=1M"
if test $ISO_MAX_SIZE ; then
    is_positive_integer $ISO_MAX_SIZE || Error "ISO_MAX_SIZE must be a positive integer value"
    # Tell the user when ISO_MAX_SIZE is less than 600MiB because then things will likely not work
    # because a usual recovery system with FIRMWARE_FILES is more than 300MiB
    # cf. https://github.com/rear/rear/pull/2347#issuecomment-602812451
    # so that there is less than 300MiB left for the actual backup split chunk size:
    test $ISO_MAX_SIZE -ge 600 || LogPrintError "ISO_MAX_SIZE should be at least 600 MiB"
    # Computation of the actual backup split chunk size
    # by subtracting the recovery system file sizes (kernel, initrd, ISOLINUX files, UEFI files if used)
    # from the ISO_MAX_SIZE value, see the ISO_MAX_SIZE explanation in default.conf why that is done.
    # Size of the recovery system initrd in bytes:
    INITRD_BYTES=$( stat -c '%s' $TMP_DIR/$REAR_INITRD_FILENAME )
    is_positive_integer $INITRD_BYTES || Error "Cannot determine size of the recovery system initrd $TMP_DIR/$REAR_INITRD_FILENAME"
    # Size of the recovery system initrd in MiB + 1MiB to be safe against integer (floor) rounding:
    INITRD_SIZE=$(( INITRD_BYTES / 1024 / 1024 + 1 ))
    # Size of the recovery system kernel in bytes:
    KERNEL_BYTES=$( stat -c '%s' $KERNEL_FILE )
    is_positive_integer $KERNEL_BYTES || Error "Cannot determine size of the recovery system kernel $KERNEL_FILE"
    # Size of the recovery system kernel in MiB + 1MiB to be safe against integer (floor) rounding:
    KERNEL_SIZE=$(( KERNEL_BYTES / 1024 / 1024 + 1 ))
    # We assume 15MiB is sufficient size for the ISOLINUX bootloader files:
    ISOLINUX_SIZE=15
    # We assume 30MiB is sufficient size for additional UEFI bootloader files:
    UEFI_SIZE=0
    is_true $USING_UEFI_BOOTLOADER && UEFI_SIZE=30
    # Size of the recovery system and its bootloader in MiB:
    RECOVERY_SYSTEM_SIZE=$(( INITRD_SIZE + KERNEL_SIZE + ISOLINUX_SIZE + UEFI_SIZE ))
    # Tell the user when the recovery system plus ISO bootloader is extraordinarily large because that may indicate a problem elsewehre:
    test $RECOVERY_SYSTEM_SIZE -gt 1000 && LogPrintError "Extraordinarily large recovery system plus ISO bootloader $RECOVERY_SYSTEM_SIZE MiB"
    # Size of the actual backup split chunk size in MiB:
    BACKUP_SPLIT_CHUNK_SIZE=$(( ISO_MAX_SIZE - RECOVERY_SYSTEM_SIZE ))
    # When the actual backup split chunk size is less than 100MiB we consider it too small to be useful in practice:
    test $BACKUP_SPLIT_CHUNK_SIZE -ge 100 || Error "Backup split chunk size $BACKUP_SPLIT_CHUNK_SIZE less than 100 MiB (ISO_MAX_SIZE too small?)"
    # Split the 'tar' backup (at stdin) in chunks of BACKUP_SPLIT_CHUNK_SIZE MiB using 'backup.tar.gz.' as prefix with numeric suffixes:
    LogPrint "Backup gets split in chunks of $BACKUP_SPLIT_CHUNK_SIZE MiB (ISO_MAX_SIZE $ISO_MAX_SIZE minus recovery system size $RECOVERY_SYSTEM_SIZE)"
    SPLIT_COMMAND="split -d -b ${BACKUP_SPLIT_CHUNK_SIZE}m - ${backuparchive}."
fi

# Used by "tar" method to record which pipe command failed
FAILING_BACKUP_PROG_FILE="$TMP_DIR/failing_backup_prog"
FAILING_BACKUP_PROG_RC_FILE="$TMP_DIR/failing_backup_prog_rc"

# Do not show the BACKUP_PROG_CRYPT_KEY value in a log file
# where BACKUP_PROG_CRYPT_KEY is only used if BACKUP_PROG_CRYPT_ENABLED is true
# therefore 'Log ... BACKUP_PROG_CRYPT_KEY ...' is used (and not '$BACKUP_PROG_CRYPT_KEY')
# but '$BACKUP_PROG_CRYPT_KEY' must be used in the actual command call which means
# the BACKUP_PROG_CRYPT_KEY value would appear in the log when rear is run in debugscript mode
# so that stderr of the confidential command is redirected to /dev/null
# cf. the comment of the UserInput function in lib/_input-output-functions.sh
# how to keep things confidential when rear is run in debugscript mode
# because it is more important to not leak out user secrets into a log file
# than having stderr error messages when a confidential command fails
# cf. https://github.com/rear/rear/issues/2155
LogPrint "Creating $BACKUP_PROG archive '$backuparchive'"
ProgressStart "Preparing archive operation"
# Begin backup subshell:
(
case "$(basename ${BACKUP_PROG})" in
    # tar compatible programs here
    (tar)
        set_tar_features

        if is_true "$BACKUP_PROG_CRYPT_ENABLED" ; then
            Log $BACKUP_PROG $TAR_OPTIONS --sparse --block-number --totals --verbose \
                --no-wildcards-match-slash --one-file-system \
                --ignore-failed-read "${BACKUP_PROG_OPTIONS[@]}" \
                $BACKUP_PROG_CREATE_NEWER_OPTIONS \
                ${BACKUP_PROG_BLOCKS:+-b $BACKUP_PROG_BLOCKS} "${BACKUP_PROG_COMPRESS_OPTIONS[@]}" \
                -X $TMP_DIR/backup-exclude.txt -C / -c -f - \
                $(cat $TMP_DIR/backup-include.txt) $RUNTIME_LOGFILE \| $BACKUP_PROG_CRYPT_OPTIONS BACKUP_PROG_CRYPT_KEY \| $SPLIT_COMMAND
        else
            Log $BACKUP_PROG $TAR_OPTIONS --sparse --block-number --totals --verbose \
                --no-wildcards-match-slash --one-file-system \
                --ignore-failed-read "${BACKUP_PROG_OPTIONS[@]}" \
                $BACKUP_PROG_CREATE_NEWER_OPTIONS \
                ${BACKUP_PROG_BLOCKS:+-b $BACKUP_PROG_BLOCKS} "${BACKUP_PROG_COMPRESS_OPTIONS[@]}" \
                -X $TMP_DIR/backup-exclude.txt -C / -c -f - \
                $(cat $TMP_DIR/backup-include.txt) $RUNTIME_LOGFILE \| $SPLIT_COMMAND
        fi

        if is_true "$BACKUP_PROG_CRYPT_ENABLED" ; then
            backup_prog_shortnames=(
                "$(basename $(echo "$BACKUP_PROG" | awk '{ print $1 }'))"
                "$(basename $(echo "$BACKUP_PROG_CRYPT_OPTIONS" | awk '{ print $1 }'))"
                "$(basename $(echo "$SPLIT_COMMAND" | awk '{ print $1 }'))"
            )
            $BACKUP_PROG $TAR_OPTIONS --sparse --block-number --totals --verbose                   \
                --no-wildcards-match-slash --one-file-system                                       \
                --ignore-failed-read "${BACKUP_PROG_OPTIONS[@]}"                                   \
                $BACKUP_PROG_CREATE_NEWER_OPTIONS                                                  \
                ${BACKUP_PROG_BLOCKS:+-b $BACKUP_PROG_BLOCKS}                                      \
                "${BACKUP_PROG_COMPRESS_OPTIONS[@]}"                                               \
                -X $TMP_DIR/backup-exclude.txt -C / -c -f -                                        \
                $(cat $TMP_DIR/backup-include.txt) $RUNTIME_LOGFILE |                              \
            { $BACKUP_PROG_CRYPT_OPTIONS "$BACKUP_PROG_CRYPT_KEY" ; } 2>>/dev/$SECRET_OUTPUT_DEV | \
            $SPLIT_COMMAND
            pipes_rc=( ${PIPESTATUS[@]} )
        else
            backup_prog_shortnames=(
                "$(basename $(echo "$BACKUP_PROG" | awk '{ print $1 }'))"
                "$(basename $(echo "$SPLIT_COMMAND" | awk '{ print $1 }'))"
            )
            $BACKUP_PROG $TAR_OPTIONS --sparse --block-number --totals --verbose \
                --no-wildcards-match-slash --one-file-system                     \
                --ignore-failed-read "${BACKUP_PROG_OPTIONS[@]}"                 \
                $BACKUP_PROG_CREATE_NEWER_OPTIONS                                \
                ${BACKUP_PROG_BLOCKS:+-b $BACKUP_PROG_BLOCKS}                    \
                "${BACKUP_PROG_COMPRESS_OPTIONS[@]}"                             \
                -X $TMP_DIR/backup-exclude.txt -C / -c -f -                      \
                $(cat $TMP_DIR/backup-include.txt) $RUNTIME_LOGFILE |            \
            $SPLIT_COMMAND
            pipes_rc=( ${PIPESTATUS[@]} )
        fi

        # Variable used to record the short name of piped commands in case of
        # error, e.g. ( "tar" "cat" "dd" ) in case of unencrypted and unsplit backup.
        for index in "${!backup_prog_shortnames[@]}" ; do
            [ -n "${backup_prog_shortnames[$index]}" ] || BugError "No computed shortname for pipe component $index"
        done

        # Ensure that the numbers of pipe components and return codes match.
        [ ${#backup_prog_shortnames[@]} -eq ${#pipes_rc[@]} ] || BugError "Mismatching numbers of pipe components and return codes"

        # Exit code logic:
        # * don't return rc=1 unless from tar (exit code 1 is reserved for "tar" warning about modified files)
        # * process exit code in pipe's reverse order
        #   - if last command failed (e.g. "dd"), return an error
        #   - otherwise if previous command failed (e.g. "encrypt"), return an error
        #   ...
        #   - otherwise return "tar" exit code
        # When an error occurs, record the program name in $FAILING_BACKUP_PROG_FILE
        # and real exit code in $FAILING_BACKUP_PROG_RC_FILE.
        let index=${#pipes_rc[@]}-1
        while [ $index -ge 0 ] ; do
            rc=${pipes_rc[$index]}
            if [ $rc -ne 0 ] ; then
                echo "${backup_prog_shortnames[$index]}" > $FAILING_BACKUP_PROG_FILE
                echo "$rc" > $FAILING_BACKUP_PROG_RC_FILE
                if [ $rc -eq 1 ] && [ "${backup_prog_shortnames[$index]}" != "tar" ] ; then
                    rc=2
                fi
                # Exit the backup subshell with non-zero exit code:
                exit $rc
            fi
            # This pipe command succeeded, check the previous one
            let index--
        done
        # Success - exit the backup subshell with zero exit code:
        exit 0
    ;;
    (rsync)
        # make sure that the target is a directory
        mkdir -p $v "$backuparchive" >&2
        Log $BACKUP_PROG --verbose "${BACKUP_RSYNC_OPTIONS[@]}" --one-file-system --delete \
            --exclude-from=$TMP_DIR/backup-exclude.txt --delete-excluded \
            $(cat $TMP_DIR/backup-include.txt) "$backuparchive"
        $BACKUP_PROG --verbose "${BACKUP_RSYNC_OPTIONS[@]}" --one-file-system --delete \
            --exclude-from=$TMP_DIR/backup-exclude.txt --delete-excluded \
            $(cat $TMP_DIR/backup-include.txt) "$backuparchive" >&2
    ;;
    (*)
        Log "Using unsupported backup program '$BACKUP_PROG'"
        Log $BACKUP_PROG "${BACKUP_PROG_COMPRESS_OPTIONS[@]}" \
            $BACKUP_PROG_OPTIONS_CREATE_ARCHIVE $TMP_DIR/backup-exclude.txt \
            "${BACKUP_PROG_OPTIONS[@]}" "$backuparchive" \
            $(cat $TMP_DIR/backup-include.txt) $RUNTIME_LOGFILE > "$backuparchive"
        $BACKUP_PROG "${BACKUP_PROG_COMPRESS_OPTIONS[@]}" \
            $BACKUP_PROG_OPTIONS_CREATE_ARCHIVE $TMP_DIR/backup-exclude.txt \
            "${BACKUP_PROG_OPTIONS[@]}" "$backuparchive" \
            $(cat $TMP_DIR/backup-include.txt) $RUNTIME_LOGFILE > "$backuparchive"
    ;;
esac 2> "${TMP_DIR}/${BACKUP_PROG_ARCHIVE}.log"
# For the rsync and default case the backup prog is the last in the case entry
# and the case .. esac is the last command in the backup subshell.
# As a result the return code of the backup subshell is the return code of the backup prog.
# For the tar case (where tar is not the last program) special exit code logic is done.
) &
BackupPID=$!
# End backup subshell.

starttime=$SECONDS
# Give the backup software a good chance to start working:
sleep 1

# return disk usage in bytes
function get_disk_used() {
    let "$(stat -f -c 'used=(%b-%f)*%S' $1)"
    echo $used
}

# While the backup runs in a subshell, display some progress information to the user.
# ProgressInfo texts have a space at the end to get the 'OK' from ProgressStop shown separated.
test "$PROGRESS_WAIT_SECONDS" || PROGRESS_WAIT_SECONDS=1
case "$( basename $BACKUP_PROG )" in
    (tar)
        while sleep $PROGRESS_WAIT_SECONDS ; kill -0 $BackupPID 2>/dev/null; do
            #blocks="$(stat -c %b ${backuparchive})"
            #size="$((blocks*512))"
            size="$(stat -c %s ${backuparchive}* | awk '{s+=$1} END {print s}')"
            ProgressInfo "Archived $((size/1024/1024)) MiB [avg $((size/1024/(SECONDS-starttime))) KiB/sec] "
        done
        ;;
    (rsync)
        # since we do not want to do a $(du -s) run every second we count disk usage instead
        # this obviously leads to wrong results in case something else is writing to the same
        # disk at the same time as is very likely with a networked file system. For local disks
        # this should be good enough and in any case this is only some eye candy.
        # TODO: Find a fast way to count the actual transfer data, preferable getting the info from rsync.
        let old_disk_used="$(get_disk_used "$backuparchive")"
        while sleep $PROGRESS_WAIT_SECONDS ; kill -0 $BackupPID 2>/dev/null; do
            let disk_used="$(get_disk_used "$backuparchive")" size=disk_used-old_disk_used
            ProgressInfo "Archived $((size/1024/1024)) MiB [avg $((size/1024/(SECONDS-starttime))) KiB/sec] "
        done
        ;;
    (*)
        while sleep $PROGRESS_WAIT_SECONDS ; kill -0 $BackupPID 2>/dev/null; do
            size="$(stat -c "%s" "$backuparchive")" || {
                kill -9 $BackupPID
                ProgressError
                Error "$(basename $BACKUP_PROG) failed to create the archive file"
            }
            ProgressInfo "Archived $((size/1024/1024)) MiB [avg $((size/1024/(SECONDS-starttime))) KiB/sec] "
        done
        ;;
esac
ProgressStop
transfertime="$((SECONDS-starttime))"

# harvest return code from background job. The kill -0 $BackupPID loop above should
# have made sure that this wait won't do any real "waiting" :-)
wait $BackupPID
backup_prog_rc=$?

if [[ $BACKUP_INTEGRITY_CHECK =~ ^[yY1] && "$(basename ${BACKUP_PROG})" = "tar" ]] ; then
    (cd $(dirname "$backuparchive") && md5sum $(basename "$backuparchive") > "${backuparchive}".md5 || md5sum $(basename "$backuparchive").?? > "${backuparchive}".md5)
fi

# TODO: Why do we sleep here after 'wait $BackupPID'?
sleep 1

# Everyone should see this warning, even if not verbose:
case "$(basename $BACKUP_PROG)" in
    (tar)
        if (( $backup_prog_rc != 0 )); then
            prog="$(cat $FAILING_BACKUP_PROG_FILE)"
            # Suppress purely informational tar messages from output like
            #   tar: Removing leading / from member names
            #   tar: Removing leading / from hard link targets
            #   tar: /var/spool/postfix/private/discard: socket ignored
            # but keep actual tar error or warning messages like
            #    tar: /etc/grub.d/README: file changed as we read it
            # and show only messages that are prefixed with "$prog:" (like 'tar:' or 'dd:')
            # which works when 'tar' or 'dd' fail but falsely suppresses messages from 'openssl'
            # FIXME see https://github.com/rear/rear/pull/2466#discussion_r466347471
            if (( $backup_prog_rc == 1 )); then
                LogUserOutput "WARNING: $prog ended with return code 1 and below output (last 5 lines):
  ---snip---
$( sed -n -e '/^tar: .*\(socket ignored\|Removing leading\)/d;/^'"$prog"':/s/^/  /p' "${TMP_DIR}/${BACKUP_PROG_ARCHIVE}.log" | tail -n5 )
  ----------
This means that files have been modified during the archiving
process. As a result the backup may not be completely consistent
or may not be a perfect copy of the system. Relax-and-Recover
will continue, however it is highly advisable to verify the
backup in order to be sure to safely recover this system.
"
            else
                rc=$(cat $FAILING_BACKUP_PROG_RC_FILE)
                Error "$prog failed with return code $rc and below output (last 5 lines):
  ---snip---
$( sed -n -e '/^tar: .*\(socket ignored\|Removing leading\)/d;/^'"$prog"':/s/^/  /p' "${TMP_DIR}/${BACKUP_PROG_ARCHIVE}.log" | tail -n5 )
  ----------
This means that the archiving process ended prematurely, or did
not even start. As a result it is unlikely you can recover this
system properly. Relax-and-Recover is therefore aborting execution.
"
            fi
        fi
        ;;
    (*)
        if (( $backup_prog_rc > 0 )) ; then
            Error "$(basename $BACKUP_PROG) failed with return code $backup_prog_rc

This means that the archiving process ended prematurely, or did
not even start. As a result it is unlikely you can recover this
system properly. Relax-and-Recover is therefore aborting execution.
"
        fi
        ;;
esac

tar_message="$(tac $RUNTIME_LOGFILE | grep -m1 '^Total bytes written: ')"
if [ $backup_prog_rc -eq 0 -a "$tar_message" ] ; then
    LogPrint "$tar_message in $transfertime seconds."
elif [ "$size" ]; then
    LogPrint "Archived $((size/1024/1024)) MiB in $((transfertime)) seconds [avg $((size/1024/transfertime)) KiB/sec]"
fi

### Copy progress log to backup media
cp $v "${TMP_DIR}/${BACKUP_PROG_ARCHIVE}.log" "${opath}/${BACKUP_PROG_ARCHIVE}.log" >&2

# vim: set et ts=4 sw=4:
