#!/bin/bash

CAN_BIND=${CAN_BIND:-1}

# Add a file or a directory to the list of paths to backup
#
# This helper can be used both in a system backup hook, and in an app backup script
#
# Details: ynh_backup writes SRC and the relative DEST into a CSV file. And it
# creates the parent destination directory
#
# If DEST is ended by a slash it complete this path with the basename of SRC.
#
# usage: ynh_backup --src_path=src_path [--dest_path=dest_path] [--is_big] [--not_mandatory]
# | arg: -s, --src_path - file or directory to bind or symlink or copy. it shouldn't be in the backup dir.
# | arg: -d, --dest_path - destination file or directory inside the backup dir
# | arg: -b, --is_big - Indicate data are big (mail, video, image ...)
# | arg: -m, --not_mandatory - Indicate that if the file is missing, the backup can ignore it.
# | arg: arg - Deprecated arg
#
# Example in the context of a wordpress app
#
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf"
# # => This line will be added into CSV file
# # "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf"
#
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf/nginx.conf"
# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/nginx.conf"
#
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf/"
# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf"
#
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf"
# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf"
#
# #Deprecated usages (maintained for retro-compatibility)
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "${backup_dir}/conf/nginx.conf"
# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/nginx.conf"
#
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "/conf/"
# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf"
#
# Requires YunoHost version 2.4.0 or higher.
ynh_backup() {
    # TODO find a way to avoid injection by file strange naming !

    # Declare an array to define the options of this helper.
    local legacy_args=sdbm
    declare -Ar args_array=( [s]=src_path= [d]=dest_path= [b]=is_big [m]=not_mandatory )
    local src_path
    local dest_path
    local is_big
    local not_mandatory
    # Manage arguments with getopts
    ynh_handle_getopts_args "$@"
    local dest_path="${dest_path:-}"
    local is_big="${is_big:-0}"
    local not_mandatory="${not_mandatory:-0}"

    BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0}
    test -n "${app:-}" && do_not_backup_data=$(ynh_app_setting_get --app=$app --key=do_not_backup_data)

    # If backing up core only (used by ynh_backup_before_upgrade),
    # don't backup big data items
    if [ $is_big -eq 1 ] && ( [ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ] ) 
    then
        if [ $BACKUP_CORE_ONLY -eq 1 ]; then
            ynh_print_warn --message="$src_path will not be saved, because 'BACKUP_CORE_ONLY' is set."
        else
            ynh_print_warn --message="$src_path will not be saved, because 'do_not_backup_data' is set."
        fi
        return 0
    fi

    # ==============================================================================
    # Format correctly source and destination paths
    # ==============================================================================
    # Be sure the source path is not empty
    [[ -e "${src_path}" ]] || {
        ynh_print_warn --message="Source path '${src_path}' does not exist"
        if [ "$not_mandatory" == "0" ]
        then
                # This is a temporary fix for fail2ban config files missing after the migration to stretch.
                if echo "${src_path}" | grep --quiet "/etc/fail2ban"
                then
                        touch "${src_path}"
                        ynh_print_info --message="The missing file will be replaced by a dummy one for the backup !!!"
                else
                        return 1
                fi
        else
                return 0
        fi
    }

    # Transform the source path as an absolute path
    # If it's a dir remove the ending /
    src_path=$(realpath "$src_path")

    # If there is no destination path, initialize it with the source path
    # relative to "/".
    # eg: src_path=/etc/yunohost -> dest_path=etc/yunohost
    if [[ -z "$dest_path" ]]; then

        dest_path="${src_path#/}"

    else
        if [[ "${dest_path:0:1}" == "/" ]]; then

            # If the destination path is an absolute path, transform it as a path
            # relative to the current working directory ($YNH_CWD)
            #
            # If it's an app backup script that run this helper, YNH_CWD is equal to
            # $YNH_BACKUP_DIR/apps/APP_INSTANCE_NAME/backup/
            #
            # If it's a system part backup script, YNH_CWD is equal to $YNH_BACKUP_DIR
            dest_path="${dest_path#$YNH_CWD/}"

            # Case where $2 is an absolute dir but doesn't begin with $YNH_CWD
            [[ "${dest_path:0:1}" == "/" ]] \
                && dest_path="${dest_path#/}"
        fi

        # Complete dest_path if ended by a /
        [[ "${dest_path: -1}" == "/" ]] \
            &&  dest_path="${dest_path}/$(basename $src_path)"
    fi

    # Check if dest_path already exists in tmp archive
    [[ ! -e "${dest_path}" ]] || {
        ynh_print_err --message="Destination path '${dest_path}' already exist"
        return 1
    }

    # Add the relative current working directory to the destination path
    local rel_dir="${YNH_CWD#$YNH_BACKUP_DIR}"
    rel_dir="${rel_dir%/}/"
    dest_path="${rel_dir}${dest_path}"
    dest_path="${dest_path#/}"
    # ==============================================================================

    # ==============================================================================
    # Write file to backup into backup_list
    # ==============================================================================
    local src=$(echo "${src_path}" | sed -r 's/"/\"\"/g')
    local dest=$(echo "${dest_path}" | sed -r 's/"/\"\"/g')
    echo "\"${src}\",\"${dest}\"" >> "${YNH_BACKUP_CSV}"

    # ==============================================================================

    # Create the parent dir of the destination path
    # It's for retro compatibility, some script consider ynh_backup creates this dir
    mkdir -p $(dirname "$YNH_BACKUP_DIR/${dest_path}")
}

# Restore all files that were previously backuped in a core backup script or app backup script
#
# usage: ynh_restore
#
# Requires YunoHost version 2.6.4 or higher.
ynh_restore () {
    # Deduce the relative path of $YNH_CWD
    local REL_DIR="${YNH_CWD#$YNH_BACKUP_DIR/}"
    REL_DIR="${REL_DIR%/}/"

    # For each destination path begining by $REL_DIR
    cat ${YNH_BACKUP_CSV} | tr -d $'\r' | grep -ohP "^\".*\",\"$REL_DIR.*\"$" | \
    while read line; do
        local ORIGIN_PATH=$(echo "$line" | grep -ohP "^\"\K.*(?=\",\".*\"$)")
        local ARCHIVE_PATH=$(echo "$line" | grep -ohP "^\".*\",\"$REL_DIR\K.*(?=\"$)")
        ynh_restore_file --origin_path="$ARCHIVE_PATH" --dest_path="$ORIGIN_PATH"
    done
}

# Return the path in the archive where has been stocked the origin path
#
# [internal]
#
# usage: _get_archive_path ORIGIN_PATH
_get_archive_path () {
    # For security reasons we use csv python library to read the CSV
    sudo python -c "
import sys
import csv
with open(sys.argv[1], 'r') as backup_file:
    backup_csv = csv.DictReader(backup_file, fieldnames=['source', 'dest'])
    for row in backup_csv:
        if row['source']==sys.argv[2].strip('\"'):
            print row['dest']
            sys.exit(0)
    raise Exception('Original path for %s not found' % sys.argv[2])
    "  "${YNH_BACKUP_CSV}" "$1"
    return $?
}

# Restore a file or a directory
#
# Use the registered path in backup_list by ynh_backup to restore the file at
# the right place.
#
# usage: ynh_restore_file --origin_path=origin_path [--dest_path=dest_path] [--not_mandatory]
# | arg: -o, --origin_path - Path where was located the file or the directory before to be backuped or relative path to $YNH_CWD where it is located in the backup archive
# | arg: -d, --dest_path - Path where restore the file or the dir, if unspecified, the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in the archive, the destination will be searched into backup.csv
# | arg: -m, --not_mandatory - Indicate that if the file is missing, the restore process can ignore it.
#
# examples:
#     ynh_restore_file "/etc/nginx/conf.d/$domain.d/$app.conf"
#     # You can also use relative paths:
#     ynh_restore_file "conf/nginx.conf"
#
# If DEST_PATH already exists and is lighter than 500 Mo, a backup will be made in
# /home/yunohost.conf/backup/. Otherwise, the existing file is removed.
#
# if apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf exists, restore it into
# /etc/nginx/conf.d/$domain.d/$app.conf
# if no, search for a match in the csv (eg: conf/nginx.conf) and restore it into
# /etc/nginx/conf.d/$domain.d/$app.conf
#
# Requires YunoHost version 2.6.4 or higher.
ynh_restore_file () {
    # Declare an array to define the options of this helper.
    local legacy_args=odm
    declare -Ar args_array=( [o]=origin_path= [d]=dest_path= [m]=not_mandatory )
    local origin_path
    local archive_path
    local dest_path
    local not_mandatory
    # Manage arguments with getopts
    ynh_handle_getopts_args "$@"
    local origin_path="/${origin_path#/}"
    local archive_path="$YNH_CWD${origin_path}"
    # Default value for dest_path = /$origin_path
    local dest_path="${dest_path:-$origin_path}"
    local not_mandatory="${not_mandatory:-0}"

    # If archive_path doesn't exist, search for a corresponding path in CSV
    if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]; then
        if [ "$not_mandatory" == "0" ]
        then
            archive_path="$YNH_BACKUP_DIR/$(_get_archive_path \"$origin_path\")"
        else
            return 0
        fi
    fi

    # Move the old directory if it already exists
    if [[ -e "${dest_path}" ]]
    then
        # Check if the file/dir size is less than 500 Mo
        if [[ $(du -sb ${dest_path} | cut -d"/" -f1) -le "500000000" ]]
        then
            local backup_file="/home/yunohost.conf/backup/${dest_path}.backup.$(date '+%Y%m%d.%H%M%S')"
            mkdir -p "$(dirname "$backup_file")"
            mv "${dest_path}" "$backup_file"    # Move the current file or directory
        else
            ynh_secure_remove --file=${dest_path}
        fi
    fi

    # Restore origin_path into dest_path
    mkdir -p $(dirname "$dest_path")

    # Do a copy if it's just a mounting point
    if mountpoint -q $YNH_BACKUP_DIR; then
        if [[ -d "${archive_path}" ]]; then
            archive_path="${archive_path}/."
            mkdir -p "$dest_path"
        fi
        cp -a "$archive_path" "${dest_path}"
    # Do a move if YNH_BACKUP_DIR is already a copy
    else
        mv "$archive_path" "${dest_path}"
    fi
}

# Deprecated helper since it's a dangerous one!
#
# [internal]
#
ynh_bind_or_cp() {
    local AS_ROOT=${3:-0}
    local NO_ROOT=0
    [[ "${AS_ROOT}" = "1" ]] || NO_ROOT=1
    ynh_print_warn --message="This helper is deprecated, you should use ynh_backup instead"
    ynh_backup "$1" "$2" 1
}

# Calculate and store a file checksum into the app settings
#
# $app should be defined when calling this helper
#
# usage: ynh_store_file_checksum --file=file
# | arg: -f, --file - The file on which the checksum will performed, then stored.
#
# Requires YunoHost version 2.6.4 or higher.
ynh_store_file_checksum () {
    # Declare an array to define the options of this helper.
    local legacy_args=f
    declare -Ar args_array=( [f]=file= )
    local file
    # Manage arguments with getopts
    ynh_handle_getopts_args "$@"

    local checksum_setting_name=checksum_${file//[\/ ]/_}    # Replace all '/' and ' ' by '_'
    ynh_app_setting_set --app=$app --key=$checksum_setting_name --value=$(sudo md5sum "$file" | cut -d' ' -f1)

    # If backup_file_checksum isn't empty, ynh_backup_if_checksum_is_different has made a backup
    if [ -n "${backup_file_checksum-}" ]
    then
        # Print the diff between the previous file and the new one.
        # diff return 1 if the files are different, so the || true
        diff --report-identical-files --unified --color=always $backup_file_checksum $file >&2 || true
    fi
    # Unset the variable, so it wouldn't trig a ynh_store_file_checksum without a ynh_backup_if_checksum_is_different before it.
    unset backup_file_checksum
}

# Verify the checksum and backup the file if it's different
# 
# This helper is primarily meant to allow to easily backup personalised/manually
# modified config files.
#
# usage: ynh_backup_if_checksum_is_different --file=file
# | arg: -f, --file - The file on which the checksum test will be perfomed.
# | ret: the name of a backup file, or nothing
#
# Requires YunoHost version 2.6.4 or higher.
ynh_backup_if_checksum_is_different () {
    # Declare an array to define the options of this helper.
    local legacy_args=f
    declare -Ar args_array=( [f]=file= )
    local file
    # Manage arguments with getopts
    ynh_handle_getopts_args "$@"

    local checksum_setting_name=checksum_${file//[\/ ]/_}    # Replace all '/' and ' ' by '_'
    local checksum_value=$(ynh_app_setting_get --app=$app --key=$checksum_setting_name)
    # backup_file_checksum isn't declare as local, so it can be reuse by ynh_store_file_checksum
    backup_file_checksum=""
    if [ -n "$checksum_value" ]
    then    # Proceed only if a value was stored into the app settings
        if ! echo "$checksum_value $file" | sudo md5sum -c --status
        then    # If the checksum is now different
            backup_file_checksum="/home/yunohost.conf/backup/$file.backup.$(date '+%Y%m%d.%H%M%S')"
            sudo mkdir -p "$(dirname "$backup_file_checksum")"
            sudo cp -a "$file" "$backup_file_checksum"    # Backup the current file
            ynh_print_warn "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file_checksum"
            echo "$backup_file_checksum"    # Return the name of the backup file
        fi
    fi
}

# Delete a file checksum from the app settings
#
# $app should be defined when calling this helper
#
# usage: ynh_remove_file_checksum file
# | arg: -f, --file= - The file for which the checksum will be deleted
#
# Requires YunoHost version 3.3.1 or higher.
ynh_delete_file_checksum () {
    # Declare an array to define the options of this helper.
    local legacy_args=f
    declare -Ar args_array=( [f]=file= )
    local file
    # Manage arguments with getopts
    ynh_handle_getopts_args "$@"

    local checksum_setting_name=checksum_${file//[\/ ]/_}    # Replace all '/' and ' ' by '_'
    ynh_app_setting_delete --app=$app --key=$checksum_setting_name
}

# Make a backup in case of failed upgrade
#
# usage:
# ynh_backup_before_upgrade
# ynh_clean_setup () {
#     ynh_restore_upgradebackup
# }
# ynh_abort_if_errors
#
# Requires YunoHost version 2.7.2 or higher.
ynh_backup_before_upgrade () {
    if [ ! -e "/etc/yunohost/apps/$app/scripts/backup" ]
    then
        ynh_print_warn --message="This app doesn't have any backup script."
        return
    fi
    backup_number=1
    local old_backup_number=2
    local app_bck=${app//_/-}    # Replace all '_' by '-'
    NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0}

    if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
    then
        # Check if a backup already exists with the prefix 1
        if sudo yunohost backup list | grep -q $app_bck-pre-upgrade1
        then
            # Prefix becomes 2 to preserve the previous backup
            backup_number=2
            old_backup_number=1
        fi

        # Create backup
        sudo BACKUP_CORE_ONLY=1 yunohost backup create --apps $app --name $app_bck-pre-upgrade$backup_number --debug
        if [ "$?" -eq 0 ]
        then
            # If the backup succeeded, remove the previous backup
            if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$old_backup_number
            then
                # Remove the previous backup only if it exists
                sudo yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null
            fi
        else
            ynh_die --message="Backup failed, the upgrade process was aborted."
        fi
    else
        ynh_print_warn --message="\$NO_BACKUP_UPGRADE is set, backup will be avoided. Be careful, this upgrade is going to be operated without a security backup"
    fi
}

# Restore a previous backup if the upgrade process failed
#
# usage:
# ynh_backup_before_upgrade
# ynh_clean_setup () {
#     ynh_restore_upgradebackup
# }
# ynh_abort_if_errors
#
# Requires YunoHost version 2.7.2 or higher.
ynh_restore_upgradebackup () {
    ynh_print_err --message="Upgrade failed."
    local app_bck=${app//_/-}    # Replace all '_' by '-'

    NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0}

    if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
    then
        # Check if an existing backup can be found before removing and restoring the application.
        if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number
        then
            # Remove the application then restore it
            sudo yunohost app remove $app
            # Restore the backup
            sudo yunohost backup restore $app_bck-pre-upgrade$backup_number --apps $app --force --debug
            ynh_die --message="The app was restored to the way it was before the failed upgrade."
        fi
    else
        ynh_print_warn --message="\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !"
    fi
}