#!/bin/bash
# *******************************************************************
# **  @file eisman_install.sh - install package
# **
# **  Creation:     2013-01-13 dv
# **  Last update:  $Id: eisman_install.sh 44743 2017-01-24 19:21:48Z dv $
# **
# **  Copyright (c) 2013-2017 the eisfair team, team(at)eisfair(dot)org
# **
# **  This program is free software; you can redistribute it and/or modify
# **  it under the terms of the GNU General Public License as published by
# **  the Free Software Foundation; either version 2 of the License, or
# **  (at your option) any later version.
# *******************************************************************

. /var/install/include/eislib
. /var/install/include/eismanlib

FLAGS=''
AUTO='no'
INFO='no'
REQUIRED_SPACE='0'    # is the space required for installation


if tar --help | grep -q 'no-overwrite-dir'
then
    taroption1='--no-overwrite-dir'
else
    taroption1=''
fi

if tar --help | grep -q 'keep-directory-symlink'
then
    taroption2='--keep-directory-symlink'
else
    taroption2=''
fi

# *******************************************************************
# **  show help on arguments and possible options
# **  @param  n/a
# **  @return n/a
# *******************************************************************

show_help()
{
cat <<EOF

Install one or more eisfair packages

usage:
    eisman install [OPTIONS] {PACKAGE[=VERSION]}*

options:
    --help          show this help screen
    --unstable      allow unstable packages to be included
    --auto          non interactive mode
    --info          do not install, only show packages
    --no-wait-anim  do not show wait animation
    --output=<file> redirect result into a file
    --url=<url>     limit root package search to this base URL

EOF
}


# *******************************************************************
# **  remove package from database of installed files
# **  @param  $1  package name
# **  @return n/a
# *******************************************************************

remove_from_database()
{
    local package="${1}"

    # grep all packages other then "$package" into temporary file
    grep -v -e "^\[.*\]\[${package}\]\[.*\]\[.*\]\[.*\]" \
       "${EISMAN_PATH}/installed.db" > "${EISMAN_PATH}/installed.db.tmp"

    if [ "$?" -eq 0 ]
    then
        # remove database file
        rm -f "${EISMAN_PATH}/installed.db"

        # move temporary file to new database
        mv "${EISMAN_PATH}/installed.db.tmp" "${EISMAN_PATH}/installed.db"
        return 0
    else
        echo "error: unable to delete packate from database!" >&2
        return 1
    fi
}


# *******************************************************************
# **  list the packages returned by eisman_tree to show all
# **  requirements prior to installation
# **  @param  $1  list of packages
# **  @return n/a
# *******************************************************************

list_packages()
{
    local packages="$1"
    local count
    local line

    # show table header if not in auto mode
    if [ "${AUTO}" = "no" ]
    then
        echo " "
        echo "The following packages will be installed:"
        echo " "
        echo "version  status   name              source"
        echo "---------------------------------------------------------"
    fi

    # redefine separator
    IFS_BAK=$IFS
    IFS="
"

    # split package list into lines
    count=0
    for line in $packages
    do
        # if this is the space entry remember it's value
        # otherwise print the line onto the screen
        if [ "${line:0:8}" = "space = " ]
        then
            space=${line:8}
        elif [ "${line:0:8}" = "warning:" ]
        then
            mecho --warn "$line"
        else
            if [ "${AUTO}" = "no" ]
            then
                if [ "${#line}" -gt 72 ]
                then
                    echo "${line:0:72}..."
                else
                    echo "${line}"
                fi
            fi
            count=$(($count + 1))
        fi
    done

    # restore separator
    IFS=${IFS_BAK}

    # show statistics (number of packages and disk space)
    if [ "${AUTO}" = "no" ]
    then
        echo " "
        echo "${count} package(s) using approx. ${space} MB of disk space."
        echo " "
    fi

    # store required space in global var
    REQUIRED_SPACE="${space}"
}


# *******************************************************************
# **  download required packages to the package cache. Only if all
# **  packages have been successfully downloaded installation can
# **  begin
# **  @param  $1  list of packages
# **  @return n/a
# *******************************************************************

download_packages()
{
    local packages="$1"
    local ifs_bak=$IFS

    mecho --info 'Downloading required packages ...'

    IFS="
"

    for line in ${packages}
    do
        if [ "${line:0:8}" != "space = " ]
        then
            local version=""
            local package=""
            local status=""
            local url=""
            local token
            local idx=0

            IFS=" "
            for token in $line
            do
                case $idx in
                0)
                    version="${token}"
                    ;;
                1)
                    status="${token}"
                    ;;
                2)
                    package="${token}"
                    ;;
                3)
                    url="${token}"
                esac
                idx=$(($idx + 1))
            done
            IFS="
"

            ${EISMAN_BIN_PATH}/eisman_download.sh \
                "${package}"                      \
                "${version}"                      \
                "${status}"                       \
                "${url}"
            if [ "$?" != 0 ]
            then
                echo 'Installation aborted!' >&2
                IFS=${ifs_bak}
                exit 1
            fi
        fi
    done

    IFS=${ifs_bak}

    mecho --info 'Done!'
}


# *******************************************************************
# **  uninstall packages that are replaced by the package that is
# **  going to be installed next
# **  @param  $1  list of replacements
# **  @return n/a
# *******************************************************************

uninstall_replaced_packages()
{
    local replacelist="${1}"
    local line
    local ifs_bak=${IFS}

    IFS="
"

    for line in ${replacelist}
    do
        local package=$(echo ${line} | cut -d ' ' -f 1)

        local state=$(${EISMAN_BIN_PATH}/eisman_check_version.sh ${package} "0.0.0")
        if [ "${state}" != "not-installed" ]
        then
            ${EISMAN_BIN_PATH}/eisman_uninstall.sh --quiet --update --auto --force "${package}"
            res="${?}"

            # evaluate result from uninstall script
            if [ "${res}" -ne 0 ]
            then
                echo "error: aborted during update of outdated package ${package}!" >&2
                exit 1
            else
                remove_from_database "${package}"
            fi
        fi
    done

    IFS=${ifs_bak}
}


# *******************************************************************
# **  restore the permissions of the temp folder since these seem to
# **  get lost some times
# **  @param  n/a
# **  @return n/a
# *******************************************************************

restore_tmp_folder()
{
    chown root.root /tmp
    chmod 1777 /tmp
}


# *******************************************************************
# **  install one single package by first executing /tmp/preinstall
# **  then extracting the whole archive and then executing /tmp/install
# **  @param  $1  name of package
# **  @param  $2  version of package
# **  @param  $3  status (u|s)
# **  @param  $4  URL
# **  @param  $5  archive file name (and path)
# **  @return n/a
# *******************************************************************

install_single_package()
{
    local package="${1}"
    local version="${2}"
    local status="${3}"
    local url="${4}"
    local filename="${5}"

    local base_url="$(get_base_url ${url})"

    # read package date from packages.db
    # Make sure only one entry is read even if the packages is listed more than once
    local header=$((${FIELDHEADER} + 1))

    # perform query
    local instdate=$(query_package_db      \
        ""                \
        ""                \
        "${package}"      \
        "${version}"      \
        "${status}"       \
        "${base_url}"     \
        "date"            \
        "${EISMAN_PATH}/packages.db" |
            cut  -c "${header}-"     |
            head -n 1)

    local replacelist=$(query_package_db      \
        ""                \
        ""                \
        "${package}"      \
        "${version}"      \
        "${status}"       \
        "${base_url}"     \
        "replaces"        \
        "${EISMAN_PATH}/packages.db" |
            cut  -c "${header}-")

    # if this package replaces other packages and one or more of this
    # packages are still installed on this system (in any version),
    # this packages need to be uninstalled prior to installation
    if [ -n "${replacelist}" ]
    then
        uninstall_replaced_packages "${replacelist}"
    fi

    # change into root directory
    cd /

    # get preinstall script first
    rm -f /tmp/preinstall.sh
    tar ${taroption1} ${taroption2} -x -f "${filename}" tmp/preinstall.sh 2>/dev/null

    # some packages modify /tmp folder  permissions and owner -> restore
    restore_tmp_folder

    # now execute preinstall script if present
    if [ -f /tmp/preinstall.sh ]
    then
        # remove old symbolic link
        rm -f "/tmp/$(basename ${filename})"

        # create new symbolic link
        ln -s "${filename}" "/tmp/$(basename ${filename})"

        # execute preinstall script
        sh /tmp/preinstall.sh "${url}"
        res="${?}"
        rm -f "/tmp/preinstall.sh"

        # remove symbolic link
        rm -f "/tmp/$(basename ${filename})"

        # evaluate result from preinstall script
        if [ "${res}" -ne 0 ]
        then
            echo "error: installation of ${package} aborted by /tmp/preinstall.sh!"
            exit 1
        fi
    else
        ${EISMAN_BIN_PATH}/eisman_uninstall.sh --quiet --update --auto "${package}"
        res="${?}"

        # evaluate result from uninstall script
        if [ "${res}" -ne 0 ]
        then
            echo "error: installation of ${package} aborted by uninstall!" >&2
            exit 1
        fi
    fi

    # now extract the complete package
    rm -f /tmp/install.sh
    if ! tar ${taroption1} ${taroption2} -x -f "${filename}" 2>/tmp/install.log
    then
        echo "error: failed to extract archive!" >&2
        exit 1
    fi

    # remove again, included in the tarball
    rm -f /tmp/preinstall.sh

    if [ ! -s /tmp/install.log ]
    then
        rm -f /tmp/install.log
    fi

    # some packages modify /tmp folder  permissions and owner -> restore
    restore_tmp_folder

    # now execute install script
    if [ -f /tmp/install.sh ]
    then
        local res

        # execute install script
        sh /tmp/install.sh "${url}"
        res="$?"

        # remove install script
        rm -f /tmp/install.sh

        # eval and show result. A non zero result is meant for packages,
        # that are able to undo their installation during execution of
        # /tmp/install.sh. Only known candidates for packages like that
        # are eiskernel-* packages
        # base package installation also can be incomplete due to their
        # incremental nature. If the base installation is canceled, the
        # result also is non zero and the package must not be added to
        # the database
        if [ "${res}" -ne "0" ]
        then
            mecho --warn -n 'Installation canceled by /tmp/install.sh: '
            mecho           "${package} (${version})!"
            exit 1
        fi
    fi

    # log installation via syslog
    {
        echo "package ${package} (${version}|${instdate}|$(basename ${filename}))"
        echo "(${url})"
    } | /usr/bin/logger -t 'eis-install' -p 'local5.info'

    # restore original directory
    cd - > /dev/null
}


# *******************************************************************
# **  install the list of packages one by one following the precise
# **  order returned by eisman_tree.sh
# **  @param  $1  list of packages
# **  @return n/a
# *******************************************************************

install_packages()
{
    local packages="$1"

    IFS_BAK=$IFS
    IFS="
"

    for line in $packages
    do
        if [ "${line:0:8}" != "space = " ]
        then
            local version=""
            local package=""
            local token
            local filename
            local idx=0
            local status

            IFS=" "
            for token in $line
            do
                case $idx in
                0)
                    version="${token}"
                    ;;
                1)
                    if [ "${token}" = "stable" ]
                    then
                        status="s"
                    else
                        status="u"
                    fi
                    ;;
                2)
                    package="${token}"
                    ;;
                3)
                    url="${token}"
                    ;;
                esac
                idx=$(($idx + 1))
            done
            IFS="
"

            filename=$(ls "${EISMAN_PATH}/cache/${package}/${version}")

            # prompt action to perform
            mecho --info -n "Installation of: "
            mecho        -n "${package} (${version}) "
            mecho --info    "..."

            # check if file name is valid
            if [ -z "${filename}" ]
            then
                echo "error: unable to find package file for \"${package}\"!" >&2
                exit 1
            fi

            # call subroutine to perform the installation task
            install_single_package \
               "${package}" \
               "${version}" \
               "${status}"  \
               "${url}/${filename}" \
               "${EISMAN_PATH}/cache/${package}/${version}/${filename}"
            if [ "$?" = 0 ]
            then
                # special handling of base updates: check if installation
                # has been completely completed. If the incremental update
                # has been aborted, the base system's version dosn't match
                # the version of the package "base". In this case the new
                # database entry is not added. Hopefully this hack can be
                # removed in the future
                # Due to the checking of result codes from /tmp/install.sh
                # this code could immediately be removed and remains here only
                # for security reasons.
                if [ "${package}" = "base" ]
                then
                    local baseversion="$(cat /etc/version)"
                    local res="$(diff_version ${baseversion}  ${version})"
                    if [ "${res}" = "old" ]
                    then
                        mecho --warn -n 'Installation incomplete: '
                        mecho           "${package} (${version})!"
                        exit 1
                    fi
                fi

                # just in case there is still a database entry for this
                # package left, remove it first
                if remove_from_database "${package}"
                then
                    # remove general kernel entry from database
                    # if this entry still exists. Only the specialized
                    # kernel package names are allowed
                    if [ "${package:0:10}" = "eiskernel-" ]
                    then
                        remove_from_database "eiskernel"
                    fi

                    # convert package url to base url
                    local base_url="$(get_base_url ${url}/dummy)"

                    # copy package record to installed db by fetching the specific record
                    # from the packages database. Make sure only one entry is copied
                    # even if the packages is listed more than once
                    grep -E "^\[.*\]\[${package}\]\[${version}\]\[${status}\]\[${base_url}.*\]" \
                        "${EISMAN_PATH}/packages.db"          |
                        sed -e ":a;N;\$!ba;s/\\n/||/g"  \
                            -e "s/||${EISMAN_SEPARATOR}||/||${EISMAN_SEPARATOR}\\n/g" |
                        head -n 1                             |
                        sed 's/||/\n/g'                       \
                        >> "${EISMAN_PATH}/installed.db"

                    # remove file from cache
                    rm -rf "${EISMAN_PATH}/cache/${package}"

                    # show result
                    mecho --info -n 'Successfully installed: '
                    mecho        -n "${package} (${version})"
                    mecho --info    '!'
                else
                    mecho --warn -n 'Failed to install: '
                    mecho           "${package} (${version})!"
                    exit 1
                fi
            else
                mecho --warn -n 'Failed to install: '
                mecho           "${package} (${version})!"
                exit 1
            fi
        fi
    done

    IFS=${IFS_BAK}
}


# *******************************************************************
# **  main program
# *******************************************************************

main()
{
    local wait_animation="yes"
    local outfile=""

    while [ "${1:0:2}" = "--" ]
    do
        case $1 in
        --help)
            show_help
            exit 0
            ;;
        --unstable)
            shift
            FLAGS="${FLAGS} --unstable" # use unstable packages too
            ;;
        --auto)
            shift
            AUTO='yes'        # non interactive installation
            ;;
        --listonly|--info)
            shift
            INFO='yes'        # not a real installation only show what would be done
            ;;
        --url)
            shift
            FLAGS="${FLAGS} --url=${1}" # set filter url
            shift
            ;;
        --url=*)
            FLAGS="${FLAGS} --url=${1:6}" # set filter url
            shift
            ;;
        --output)
            shift
            outfile="${1}"
            shift
            ;;
        --output=*)
            outfile="${1:9}"
            shift
            ;;
        --no-wait-anim)
            wait_animation="no"
            shift
            ;;
        *)
            echo "error: unknown option \"$1\"!" >&2
            echo ""
            show_help
            exit 1
            ;;
        esac
    done

    # read parameters
    while [ "$#" -ge 1 ]
    do
        PACKAGELIST="${PACKAGELIST} ${1}"
        shift
    done

    # exit upon empty package list
    if [ -z "${PACKAGELIST}" ]
    then
        exit 0
    fi

    # echo "Resolving package requirements..."
    ${EISMAN_BIN_PATH}/eisman_tree.sh ${FLAGS} ${PACKAGELIST} \
        > "/tmp/tmpresult$$" &

    # show wait animation and fetch exit code
    if [ "${wait_animation}" = "yes" ]
    then
        show_wait "searching..."
    fi
    wait $!

    if [ "$?" != 0 ]
    then
        echo 'error: installation aborted!' >&2
        rm -f "/tmp/tmpresult$$"
        exit 1
    fi

    packages="$(cat /tmp/tmpresult$$)"
    rm -f "/tmp/tmpresult$$"

    # list packages found in query
    if [ -z "${outfile}" ]
    then
        list_packages "$packages"
    else
        list_packages "$packages" > "${outfile}"
    fi

    # ask user to continue
    if [ "${AUTO}" = "no" -a "${INFO}" = "no" ]
    then
        res=$(/var/install/bin/ask "Continue" "yes")

        # if ask break, ask returned 255
        if [ $? = 255 ]
        then
            res="no"
        fi

        if [ "${res}" != "yes" ]
        then
            echo 'Installation aborted by user!' >&2
            exit 1
        fi
    fi

    if [ "${INFO}" != "yes" ]
    then
        # download listed packages
        download_packages "$packages"

        # check disk space
        if [ -n "${REQUIRED_SPACE}" ]
        then
            space_avail=$(df -m / | awk '/\/$/ { print $4 }')
            if [ "${space_avail}" -lt "${REQUIRED_SPACE}" ]
            then
                echo "Space required : ${REQUIRED_SPACE} MB"
                echo "Space available: ${space_avail} MB"
                echo
                echo 'error: installation aborted due to low disk space!' >&2
                exit 1
            fi
        fi

        # install packages listed in $packages
        install_packages "${packages}"
    fi

    exit 0
}

main "${@}"
