#!/bin/bash
### Copyright 1999-2024. WebPros International GmbH. All rights reserved.

shopt -s nullglob
export LC_ALL="C" PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
unset GREP_OPTIONS
umask 022

PRODUCT_ROOT_D="/usr/local/psa"

prog="`basename $0`"
action="$1"

err()
{
	echo "ERROR: $*" >&2
}

die()
{
	err "$@" 2>&1 | result "failed"
	exit 0
}

success()
{
	echo -n "$*" | result "ok"
	exit 0
}

usage()
{
	echo "Usage: $prog { --check | --repair }" >&2
	exit 1
}

result()
{
	local status="$1"
	python3 -c 'import sys, json
message = sys.stdin.read()
status = sys.argv[1] if message.strip() else "ok"
print(json.dumps({"status": status, "message": message}))
' "$status"
}

get_mysql_option_value()
{
	local section="$1"
	local name="$2"
	local default="$3"

	local value="`/usr/bin/my_print_defaults "$section" | sed -n "s/^--$name=//p" | tail -n 1`"
	echo "${value:-$default}"
}

get_mysql_datadir()
{
	get_mysql_option_value mysqld datadir "/var/lib/mysql"
}

mysql_action()
{
	"$PRODUCT_ROOT_D/admin/sbin/pleskrc" mysql "$1" >&2
}

check()
{
	mysql_action "stop" || die "Could not stop the database service."

	{
		local rc=0
		local datadir="`get_mysql_datadir`"
		# Default system tablespace data file is 'ibdata1', assume others (if any) are named similarly.
		# Alternatively we should parse innodb_data_file_path value, which may include many paths.
		for file in "$datadir"/ibdata* "$datadir"/*/*.ibd; do
			/usr/bin/innochecksum "$file" || {
				rc="$?"
				err "InnoDB tablespace file '$file' is corrupted."
			}
		done
		[ "$rc" -ne 0 ] || mysql_action "start" || err "Could not start the database service."
	} 2>&1 | result "corrupted"
	exit 0
}

change_recovery_mode()
{
	# This assumes that create_my_cnf_d() was called during installation, if needed.
	local mode="$1"

	local my_cnf_d="/etc/my.cnf.d"
	local config="$my_cnf_d/plesk-innodb-recovery.cnf"

	if [ -n "$mode" ] && [ "$mode" -gt 0 ]; then
		cat > "$config" <<-EOT
			# Temporarily added by Plesk Repair Kit
			[mysqld]
			innodb_force_recovery = $mode
			EOT
	else
		rm -f "$config"
	fi

	mysql_action "restart"
}

repair()
{
	# Warning: this is initial implementation, with a lot of corners cut.

	local recovery_modes=(1 2 3 4 5 6)
	local datadir="`get_mysql_datadir`"
	local corrupted_ibdata=()
	local corrupted_dbs=()
	local db_dump=

	# 0. check which databases are affected
	mysql_action "stop" || die "Could not stop the database service."

	for file in "$datadir"/ibdata*; do
		/usr/bin/innochecksum "$file" >/dev/null 2>&1 ||
			corrupted_ibdata+=("`basename "$file"`")
	done
	for file in "$datadir"/*/*.ibd; do
		/usr/bin/innochecksum "$file" >/dev/null 2>&1 ||
			corrupted_dbs+=("$(basename "`dirname "$file"`")")
	done
	corrupted_dbs=(`echo "${corrupted_dbs[@]}" | xargs -n1 | sort -u`)

	# note: probably corrupted_dbs mapping to entities in DB is more complex
	err "Before repair corrupted_ibdata: ${corrupted_ibdata[*]}, corrupted_dbs: ${corrupted_dbs[*]}."
	
	[ -n "${corrupted_ibdata[*]}" -o -n "${corrupted_dbs[*]}" ] ||
		success "There are no corrupted InnoDB tablespace files - nothing to repair."

	for mode in "${recovery_modes[@]}"; do
		# 2. try innodb_force_recovery
		change_recovery_mode "$mode" || {
			err "Failed to start the database server in recover mode $mode."
			continue
		}

		# 3. dump databases (not tables?) / each, one by one
		db_dump="`"$PRODUCT_ROOT_D/admin/sbin/mysqldump.sh" --title "repair-innodb" --all-databases`" || {
			err "Failed to dump all databases."
			continue
		}

		# 4. drop databases
		# May need to revert change_recovery_mode for older MySQL/MariaDB versions.
		for db in "${corrupted_dbs[@]}"; do
			MYSQL_PWD="`cat /etc/psa/.psa.shadow`" mysql -uadmin -e "DROP DATABASE IF EXISTS $db;" ||
				err "Failed to drop corrupted DB '$db'."
		done

		if [ -n "${corrupted_ibdata[*]}" ]; then
			mysql_action "stop" || die "Could not stop the database service."
			rm -f "$datadir"/ibdata* "$datadir"/ib_logfile*
		fi

		# 5. restart w/o innodb_force_recovery
		change_recovery_mode ||
			die "Failed to start the database server in normal mode."

		break
	done

	# 6. restore databases from backup / daily dump / skip
	[ -f "$db_dump" ] ||
		die "Didn't manage obtain databases dump for restoration."

	gunzip < "$db_dump" | MYSQL_PWD="`cat /etc/psa/.psa.shadow`" mysql -uadmin ||
		die "Failed to restore databases from backup."

	# 7. verify
	mysql_action "stop" || die "Could not stop the database service."

	corrupted_ibdata=()
	corrupted_dbs=()
	for file in "$datadir"/ibdata*; do
		/usr/bin/innochecksum "$file" >/dev/null 2>&1 ||
			corrupted_ibdata+=("`basename "$file"`")
	done
	for file in "$datadir"/*/*.ibd; do
		/usr/bin/innochecksum "$file" >/dev/null 2>&1 ||
			corrupted_dbs+=("$(basename "`dirname "$file"`")")
	done
	corrupted_dbs=(`echo "${corrupted_dbs[@]}" | xargs -n1 | sort -u`)

	err "After repair corrupted_ibdata: ${corrupted_ibdata[*]}, corrupted_dbs: ${corrupted_dbs[*]}."

	[ -z "${corrupted_ibdata[*]}" -a -z "${corrupted_dbs[*]}" ] ||
		die "Failed to fully repair InnoDB tablespace files."

	mysql_action "start" || die "Failed to start the database service after repair."
	success
}

case "$action" in
	--check)
		check
	;;
	--repair)
		repair
	;;
	*)
		usage
	;;
esac

# vim:ft=sh
