#!/usr/bin/env bash

OS="`uname`"

NTOPNG_CONFIG_PATH="/etc/ntopng"

case $OS in
  'FreeBSD')
    NTOPNG_CONFIG_PATH="/usr/local/etc"
    ;;
  'Linux') ;;
  *) ;;
esac

DEFAULT_CONF_FILE="${NTOPNG_CONFIG_PATH}/ntopng.conf"

DEFAULT_DATADIR="/var/lib/ntopng"
OLD_DEFAULT_DATADIR="/var/tmp/ntopng"
if [ -d "${OLD_DEFAULT_DATADIR}" ]; then
    DEFAULT_DATADIR="${OLD_DEFAULT_DATADIR}"
fi

REDIS_DUMP_DIR="/var/lib/redis"
REDIS_DUMP_FILENAME="dump.rdb"
REDIS_DUMP_PATH="${REDIS_DUMP_DIR}/${REDIS_DUMP_FILENAME}"

DATADIR="${DEFAULT_DATADIR}"
DATADIR_SET=false

REDIS_INFO=""

ARCHIVE_NAME="conf.tar.gz"
ARCHIVE=""
ARCHIVE_SET=false

STAGING_DIR=""

SAVED_SUFFIX=".ntopngsaved"

ACTION=""
QUIET=true

INTERFACE=""

INCLUDE_REDIS_BACKUP=false

NEDGE="@NEDGE@"

function print_usage() {
    echo "ntopng configuration backup/restore utility"
    echo ""
    echo "Usage:"
    echo "`basename $0` -a restore [-d datadir] [-c archive.tar.gz] [-r]"
    echo "`basename $0` -a backup  [-d datadir] [-c archive.tar.gz] [-r]"
    echo "`basename $0` -a check-restore [-d datadir] [-c archive.tar.gz]"
    echo "`basename $0` -a install-n2disk-conf [-d datadir] -i interface"
    echo "`basename $0` -a install-n2n-conf [-d datadir]"
    echo "`basename $0` -a print-datadir"
    echo "`basename $0` -a print-redis-info"
    echo ""
    echo "[-d datadir]"
    echo "  The path to the ntopng data directory."
    echo "  When no path is specified, ntopng configuration file ${DEFAULT_CONF_FILE}"
    echo "  is read in an attempt to determine the data directory. If the data directory cannot"
    echo "  be determined from the ntopng configuration file, the default ${DEFAULT_DATADIR} is used."
    echo "  The data directory must exist on the system."
    echo ""
    echo "[-c archive.tar.gz]"
    echo "  The path to a compressed archive used to backup or restore ntopng files."
    echo "  When no path is specified, file ${ARCHIVE_NAME} inside the data directory is used."
    echo ""
    echo "[-a restore]"
    echo "`basename $0` -a restore [-c archive.tar.gz] [-d datadir]"
    echo "  Restore places the files contained in the archive into the current system."
    echo "  Resored data directory files will take the ownership (user and group) of exisining the data directory."
    echo "  The only compressed archives accepted are those created from the web interface or with this script."
    echo "  Add -r to also restore redis from the backup."
    echo ""
    echo "[-a backup]"
    echo "`basename $0` -a backup [-c archive.tar.gz] [-d datadir] [-r]"
    echo "  Backup creates a compressed archive using files contained into the current system."
    echo "  Add -r to include also redis to the backup."
    echo ""
    echo "[-a check-restore]"
    echo "`basename $0` -a check-restore [-c archive.tar.gz] [-d datadir]"
    echo "  Check restore scans the data directory and looks for a valid compressed archive there."
    echo "  Returns true when the archive is found and false otherwise."
    echo ""
    echo "[-a install-n2disk-conf]"
    echo "`basename $0` -a install-n2disk-conf [-d datadir] -i interface"
    echo "  Installs a n2disk configuration file generated by ntopng to /etc/n2disk/ creating a symlink"
    echo ""
    echo "[-a install-n2n-conf]"
    echo "`basename $0` -a install-n2n-conf [-d datadir]"
    echo "  Installs a n2n edge configuration file generated by ntopng to /etc/n2n/ creating a symlink"
    echo ""
    echo "[-a print-datadir]"
    echo "`basename $0` -a print-datadir"
    echo "  Print the path of the ntopng data directory."
    echo ""
    echo "[-a print-redis-info]"
    echo "`basename $0` -a print-redis-info"
    echo "  Print the redis connection details used by ntopng."
    echo ""
}

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -a|--action)
    ACTION="$2"
    shift
    shift
    ;;
    -c|--archive)
    ARCHIVE="$2"
    ARCHIVE_SET=true
    shift
    shift
    ;;
    -d|--data-dir)
    DATADIR="$2"
    DATADIR_SET=true
    shift
    shift
    ;;
    -i|--interface)
    INTERFACE="$2"
    shift
    shift
    ;;
    -v|--verbose)
    QUIET=false
    shift
    ;;
    -r|--redis)
    INCLUDE_REDIS_BACKUP=true
    shift
    ;;
    *)
    POSITIONAL+=("$1")
    shift
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

function cleanup() {
    if [ "$?" = "0" ]; then
	# exited with SUCCESS
	if [ $ACTION == "restore" ]; then
		rm -rf "${ARCHIVE}"
	fi
    else
	# exited with ERROR
	:
    fi

    rm -rf "${STAGING_DIR}"
}

trap cleanup EXIT

if [[ ( $ACTION != "backup" && $ACTION != "restore" && $ACTION != "check-restore" && $ACTION != "install-n2disk-conf" && $ACTION != "install-n2n-conf" && $ACTION != "print-datadir" && $ACTION != "print-redis-info" ) ]]; then
    print_usage
    exit 1
fi

if [[ ( $ACTION == "restore" ) && ( ${#POSITIONAL[@]} -gt 0 ) ]]; then
    print_usage
    exit 1
fi

if [ $ACTION == "check-restore" ]; then
    QUIET=true
fi

function prepare_redis_info() {
    if [ -f "${DEFAULT_CONF_FILE}" ]; then
	# make sure there's at most one among -r and --redis
	local REDIS_COUNT="`cat "${DEFAULT_CONF_FILE}" | grep -v "#" | grep "\-r[= ]\|\-\-redis"  | wc -l`"

	if [ "${REDIS_COUNT}" -eq "1" ]; then
	    # use awk to split on spaces or = signs and remove " and ' s, and take the last column that should be the path to the data directory
	    REDIS_INFO=`cat "${DEFAULT_CONF_FILE}" | grep -v "#" | grep "\-r[= ]\|\-\-redis" | awk -F"[= ]" '{print $NF}' | sed s/\"//g | sed s/\'//g`
	fi
    fi
}

function prepare_cur_data_dir_path() {
    if [ "${DATADIR_SET}" = false ] && [ -f "${DEFAULT_CONF_FILE}" ]; then
	# make sure there's at most one among -d and --data-dir
	local DATADIR_COUNT="`cat "${DEFAULT_CONF_FILE}" | grep -v "#" | grep "\-d[= ]\|\-\-data\-dir"  | wc -l`"

	if [ "${DATADIR_COUNT}" -gt "1" ]; then
	    [ $QUIET = false ] && echo "Cannot reliably determine the data dir from ${DEFAULT_CONF_FILE}"
	    exit 1
	elif [ "${DATADIR_COUNT}" -eq "1" ]; then
	    # use awk to split on spaces or = signs and remove " and ' s, and take the last column that should be the path to the data directory
	    local CUR_DATADIR=`cat "${DEFAULT_CONF_FILE}" | grep -v "#" | grep "\-d[= ]\|\-\-data\-dir" | awk -F"[= ]" '{print $NF}' | sed s/\"//g | sed s/\'//g`

	    if [ ! -d "${CUR_DATADIR}" ]; then
		[ $QUIET = false ] && "Data dir ${CUR_DATADIR} read from ${DEFAULT_CONF_FILE} is not a directory on the system"
		exit 1
	    fi

	    # set the data dir to the one extracted from the configuration file
	    DATADIR="${CUR_DATADIR}"
	fi
    fi

    if [ ! -d "${DATADIR}" ]; then
	[ $QUIET = false ] && echo "Data dir ${DATADIR} is not a directory on the system."
	exit 1
    fi

    [ $QUIET = false ] && echo "Using data dir ${DATADIR}"
}

function prepare_tar_file_path() {
    if [ "${ARCHIVE_SET}" = false ]; then
	ARCHIVE="${DATADIR}/${ARCHIVE_NAME}"
    fi
}

function setup_staging_dir() {
    # the temp directory used, within $DIR
    # omit the -p parameter to create a temporal directory in the default location
    STAGING_DIR=`mktemp -d`

    # check if tmp dir was created
    if [[ ! "$STAGING_DIR" || ! -d "$STAGING_DIR" ]]; then
	echo "Could not create temp directory"
	exit 1
    fi
}

function copy_to_staging_dir() {
    local PATHNAME="$1"
    local DST="$2"

    if [ ! -f "${PATHNAME}" ] && [ ! -d "${PATHNAME}" ]; then
	return
    fi

    if [[ "${PATHNAME}" != /* ]]; then
	echo "Skipping ${PATHNAME}. Only absolute paths allowed"
	return
    fi

    # recreate the tree into the staging directory
    local STAGED="${STAGING_DIR}/${DST}"
    mkdir -p "${STAGED}"

    # copy to the staging area
    if [ -f "${PATHNAME}" ]; then
	cp -Pp "${PATHNAME}" "${STAGED}"
    else
	cp -RPp "${PATHNAME}/." "${STAGED}"
    fi

    # minimum permissions to move files (e.g. otherwise sanitize fails)
    chmod u+rw "${STAGED}"

    # echo "${STAGING_DIR}"
    # find ${STAGING_DIR}
}

function manifest_to_staging_dir() {
    local DST="${STAGING_DIR}/MANIFEST"

    # write a timestamp and the original data directory to a manifest file
    echo "Time: $(date)" >> "${DST}"
    echo "__datadir: ""${DATADIR}" >> "${DST}"
}

function backup_to_staging_dir() {
    copy_to_staging_dir "/etc/ntopng" "__etc_ntopng"
    copy_to_staging_dir "${DATADIR}/runtimeprefs.json" "__datadir"

    if [ "${NEDGE}" == "0" ]; then
	copy_to_staging_dir "/etc/ntopng.license" "__license"
    else
	copy_to_staging_dir "/etc/nedge.license" "__license"
	copy_to_staging_dir "${DATADIR}/system.config" "__datadir"
    fi

    if [ $INCLUDE_REDIS_BACKUP = true ]; then
        NTOPNG_IS_ACTIVE=$(/bin/systemctl is-active ntopng 2>/dev/null)
	if [ "${NTOPNG_IS_ACTIVE}" == "active" ]; then
		echo "[i] Stopping service"
		systemctl stop ntopng
	fi

	echo "[i] Saving configuration"
	LASTSAVE=`/usr/bin/redis-cli LASTSAVE|cut -f2`
	/usr/bin/redis-cli BGSAVE

	sleep 2

	echo "[i] Creating backup"
	CURRSAVE=`/usr/bin/redis-cli LASTSAVE|cut -f2`

	copy_to_staging_dir ${REDIS_DUMP_PATH} "__redis"

	echo "[!] New redis backup saved [time=${CURRSAVE}]"

	if [ "${NTOPNG_IS_ACTIVE}" == "active" ]; then
		echo "[i] Restarting service"
		systemctl restart ntopng
	fi
    fi

    manifest_to_staging_dir
}

function sanitize_staging_dir() {
    # Throw away any symlink
    find "${STAGING_DIR}" -type l -exec rm -rf {} \;

    # Throw away any possible executable file from the staging area
    # executables include binaries but also shell scripts
    if ! `find "${STAGING_DIR}" -executable -type f -exec rm -rf {} \; 2> /dev/null`; then
	# some versions of find don't support -executable and require the
	# executable permissions bits to be specified explicitly
	find "${STAGING_DIR}" -perm +0111 -type f -exec rm -rf {} \;
    fi

    # Throw away backup files
    find "${STAGING_DIR}" -type f -name "*~" -exec rm -rf {} \;

    # Throw away previously backed up ntopng files
    find "${STAGING_DIR}" -type f -name "*${SAVED_SUFFIX}" -exec rm -rf {} \;

    # Throw away empty files
    find "${STAGING_DIR}" -type f -size 0 -exec rm -rf {} \;

    # Throw away everything else BUT pure ASCII files
    # or PNG images when nEdge
    find "${STAGING_DIR}" -type f -name '*.*' -print0 |
    while IFS= read -r -d '' FL; do
	if ! `file "${FL}" | grep -qc "\(ASCII text\|JSON data\)";`; then
	    if [ "${NEDGE}" == "1" ]; then
		if `file "${FL}" | grep -qc "PNG image data";`; then
		    continue
		fi
	    fi
	    
	    if [ $INCLUDE_REDIS_BACKUP = true ]; then
		if `file "${FL}" | grep -qc "Redis RDB file";`; then
		    continue
		fi
	    fi

	    rm -rf "${FL}"
	    echo "File ""${FL}"" ignored"
	fi
    done

    # echo "${STAGING_DIR}"
    # find ${STAGING_DIR}
}

function staging_dir_to_archive() {
    # remove the archive if it exists
    rm -rf "${ARCHIVE}"
    # compress the staging dir to a compressed archive
    # by discaring the current path to the staging dir
    tar cfz "${ARCHIVE}" -C "${STAGING_DIR}" .
}

function check_tar_file() {
    if [ ! -f "${ARCHIVE}" ]; then
	[ $QUIET = false ] && echo "Missing ${ARCHIVE}: unable to restore"
	exit 1
    fi

    # make sure it's a tar archive
    if ! `file "${ARCHIVE}" | grep -qc "gzip compressed data";`; then
	[ $QUIET = false ] && echo "Unrecognized file format for ${ARCHIVE}: expecting gzip compressed data"
	exit 1
    fi
}

function check_tar_contents() {
    # must contain a manifest file
    if ! `tar tf "${ARCHIVE}" ./MANIFEST >/dev/null 2>&1`; then
	[ $QUIET = false ] && echo "Compressed archive $1 does not contain MANIFEST"
	exit 1
    fi
}

function tar_file_to_staging_dir() {
    echo "Extracting compressed archive contents to ${STAGING_DIR}"
    tar xfz "${ARCHIVE}" -C "${STAGING_DIR}" || exit 1
}

function check_current_system_dirs() {
    # DATADIR is empty when system.config and runtimeprefs.json are not
    # going to be restored
    if [ ! -z "${DATADIR}" ] && [ ! -d "${DATADIR}" ]; then
	echo "Directory ${DATADIR} missing from the system, unable to restore."
	exit 1
    fi
}

function restore_and_save_file() {
    local SRC="$1"  # full path to the source file
    local DST_DIR="$2" # destination directory
    local OWNERSHIP="$3" # ownership to execute chown
    local PERMISSIONS="$4" # permission to execute chmod (e.g. "660")

    if [ ! -z "${SRC}" ] && [ -f "${SRC}" ] && [ ! -z "${DST_DIR}" ] && [ -d "${DST_DIR}" ]; then
	local SRC_BASENAME=`basename "${SRC}"`
	local DST="${DST_DIR}/${SRC_BASENAME}"

	if [ -f "${DST}" ]; then
	    # make a backup copy of the file before overwriting it
	    local SAV="${DST}${SAVED_SUFFIX}"

	    rm -rf "${SAV}" || exit 1
	    echo "Old file ""${SAV}"" removed"
	    cp -Rp "${DST}" "${SAV}" || exit 1
	    echo "File ""${DST}"" saved to ""${SAV}"
	fi

	# put the recovered file in place
	cp -Rp "${SRC}" "${DST}" || exit 1
	echo "File ""${DST}"" restored"

	# possibly change the ownership
	if [ ! -z "${OWNERSHIP}" ]; then
	    chown "${OWNERSHIP}" "${DST}" || exit 1
	    echo "File ""${DST}"" owner set to ""${OWNERSHIP}"
	fi

	# possibly change the permissions
	if [ ! -z "${PERMISSIONS}" ]; then
	    chmod "${PERMISSIONS}" "${DST}" || exit 1
	    echo "File ""${DST}"" permissions set to ""${PERMISSIONS}"
	fi
    fi
}

function restore_files() {
    # restore /etc/ntopng files
    if [ -d "${STAGING_DIR}/__etc_ntopng" ]; then
	find "${STAGING_DIR}/__etc_ntopng"  -maxdepth 1 -type f -name '*.*' -print0 |
	    while IFS= read -r -d '' FL; do
		restore_and_save_file "${FL}" "/etc/ntopng"
	    done
    fi

    # restore redis, if requested
    if [ $INCLUDE_REDIS_BACKUP = true ]; then

        NTOPNG_IS_ACTIVE=$(/bin/systemctl is-active ntopng 2>/dev/null)
	if [ "${NTOPNG_IS_ACTIVE}" == "active" ]; then
		echo "[i] Stopping ntopng service"
		systemctl stop ntopng
	fi

	echo "[i] Stopping redis service"
	/usr/bin/redis-cli FLUSHALL
	systemctl stop redis-server

	echo "[i] Restoring configuration"

	mv ${REDIS_DUMP_PATH} ${REDIS_DUMP_PATH}.old
	restore_and_save_file "${STAGING_DIR}/__redis/${REDIS_DUMP_FILENAME}" "${REDIS_DUMP_DIR}" "redis:redis" "660"

	echo "[i] Restarting redis service"
	systemctl start redis-server
	if [ "${NTOPNG_IS_ACTIVE}" == "active" ]; then
		echo "[i] Restarting ntopng service"
		systemctl restart ntopng
	fi
    fi

    # restore license
    if [ "${NEDGE}" == "0" ] && [ -f "${STAGING_DIR}/__license/ntopng.license" ]; then
	restore_and_save_file "${STAGING_DIR}/__license/ntopng.license" "/etc" "root:root"
    fi

    if [ "${NEDGE}" == "1" ] && [ -f "${STAGING_DIR}/__license/nedge.license" ]; then
	restore_and_save_file "${STAGING_DIR}/__license/nedge.license" "/etc" "root:root"
    fi

    if [ -z "${DATADIR}" ] || [ ! -d "${DATADIR}" ]; then
	# nothing to do, the either not existing or not a directory
	return
    fi

    # Get the ownership of the data directory
    # extracted files will be chowned to make sure they have the same
    # owner and group of the data directory
    local USER_GROUP=`ls -ld ${DATADIR} | awk '{print $3":"$4}'`
    # will be something like root:root nobody:nogroup

    if [ -z "${USER_GROUP}" ]; then
	echo "Unable to retrieve user and group of ${DATADIR}"
	exit 1
    fi

    if [ "${NEDGE}" == "1" ] && [ -f "${STAGING_DIR}/__datadir/system.config" ]; then
	restore_and_save_file "${STAGING_DIR}/__datadir/system.config" "${DATADIR}" "${USER_GROUP}"
    fi

    if [ -f "${STAGING_DIR}/__datadir/runtimeprefs.json" ]; then
	restore_and_save_file "${STAGING_DIR}/__datadir/runtimeprefs.json" "${DATADIR}" "${USER_GROUP}"
    fi
}

function post_restore_actions() {
    # in case we are restoring nedge, we touch a file to indicate
    # a fresh first start.
    if [ "${NEDGE}" == "1" ]; then
	touch "${DATADIR}/system.config.reload"
    fi
}

function install_n2disk_conf() {
    if [ "${INTERFACE}" == "" ]; then
        return
    fi

    local SOURCE_FILE="${DATADIR}/n2disk/n2disk-${INTERFACE}.conf"

    if [ ! -f "${SOURCE_FILE}" ]; then
	return
    fi

    local DEST_PATH="/etc/n2disk"
    local DEST_FILE="${DEST_PATH}/n2disk-ntopng-${INTERFACE}.conf"

    mkdir -p "${DEST_PATH}"

    rm -rf "${DEST_FILE}"
    ln -sf "${SOURCE_FILE}" "${DEST_FILE}"
}

function install_disk2disk_conf() {
    if [ "${INTERFACE}" == "" ]; then
        return
    fi

    local SOURCE_FILE="${DATADIR}/disk2disk/disk2disk-${INTERFACE}.conf"

    if [ ! -f "${SOURCE_FILE}" ]; then
	return
    fi

    local DEST_PATH="/etc/disk2disk"
    local DEST_FILE="${DEST_PATH}/disk2disk-ntopng-${INTERFACE}.conf"

    mkdir -p "${DEST_PATH}"

    rm -rf "${DEST_FILE}"
    ln -sf "${SOURCE_FILE}" "${DEST_FILE}"
}

function install_n2n_conf() {
    local SOURCE_FILE="${DATADIR}/n2n/edge.conf"

    if [ ! -f "${SOURCE_FILE}" ]; then
	return
    fi

    local DEST_PATH="/etc/n2n"

    mkdir -p "${DEST_PATH}"

    if [ "${INTERFACE}" != "" ]; then
	DEST_PATH="${DEST_PATH}/edge-${INTERFACE}.conf"
    fi

    ln -sf "${SOURCE_FILE}" "${DEST_PATH}"
}

prepare_cur_data_dir_path
prepare_tar_file_path
prepare_redis_info

if [ $ACTION == "backup" ]; then
    echo "Backing up to ${ARCHIVE} ..."

    setup_staging_dir
    backup_to_staging_dir
    sanitize_staging_dir
    staging_dir_to_archive

elif [ $ACTION == "restore" ]; then
    echo "Restoring from ${ARCHIVE} ..."

    # preliminary checks
    check_tar_file
    check_tar_contents

    # actual restore
    setup_staging_dir
    tar_file_to_staging_dir
    check_current_system_dirs
    sanitize_staging_dir
    restore_files
    post_restore_actions

elif [ $ACTION == "check-restore" ]; then

    check_tar_file
    check_tar_contents

elif [ $ACTION == "install-n2disk-conf" ]; then
    [ $QUIET = false ] && echo "Installing n2disk configuration for ${INTERFACE} ..."

    install_n2disk_conf
    install_disk2disk_conf

elif [ $ACTION == "install-n2n-conf" ]; then
    [ $QUIET = false ] && echo "Installing n2n configuration ..."

    install_n2n_conf

elif [ $ACTION == "print-datadir" ]; then

    echo "${DATADIR}"

elif [ $ACTION == "print-redis-info" ]; then

    echo "${REDIS_INFO}"

else
    # never reached
    echo "Unknown action $ACTION"
    exit 1
fi

exit 0
