#!/bin/sh

# Privat-in: receive a backup image via blocksync-fast
# Copyright (C) 2025  Sven-Ola Tuecke <sven-ola@gmx.de>
#
# See https://privat-in.de

export PATH=/usr/local/bin:/usr/bin:/bin

IMG_DIR=/images
if [ -s /etc/default/privat-in-receive-backup ];then
	# To overwrite IMG_DIR
	# shellcheck disable=SC1091
	. /etc/default/privat-in-receive-backup
fi

get_digest_md5()
{
	case ${1} in "")
		echo "Need digest filename." >&2
		exit 1
	;;esac
	if [ -s "${1}" ];then
	(
		dd if="${1}" bs=8 count=6 status=none
		dd if=/dev/zero bs=8 count=1 status=none
		dd if="${1}" bs=8 skip=7 count=57 status=none
		dd if="${1}" skip=1 status=none
	) | md5sum | sed 's, .*,,'
	fi
}

format_time()
{
	# shellcheck disable=SC3043
	{
		local time=${1}
	}
	if [ ${time} -lt 60 ];then
		time="${time} second(s)"
	elif [ ${time} -lt 600 ];then
		time=$(printf "%02d:%02d minutes:seconds" $(( time / 60 )) $(( time % 60 )))
	elif [ ${time} -lt 3600 ];then
		time="$(( time / 60 )) minutes"
	elif [ ${time} -lt 36000 ];then
		time=$(printf "%02d:%02d hours:minutes" $(( time / 3600 )) $(( time % 3600 / 60 )))
	elif [ ${time} -lt 86400 ];then
		time="$(( time / 3600 )) hours"
	elif [ ${time} -lt 864000 ];then
		time=$(printf "%02d days %02d hours" $(( time / 86400 )) $(( time % 86400 / 3600 )))
	else
		time="$(( time / 86400 )) days"
	fi
	echo ${time}
}

# Specific sequence is allowed, but may also not repeat fast.
no_repeat_until()
{
	# shellcheck disable=SC3043
	{
		local cmds="${1}"
		local ends
		local sequ
		local diff
	}

	ends=$(head -n1 ${LAST_FILE} 2>/dev/null)
	sequ=$(head -n1 ${SEQU_FILE} 2>/dev/null)
	if ! test ${ends} -gt 0 2>/dev/null;then
		# Invalid time stamps
		rm -f ${LAST_FILE} ${SEQU_FILE}
		return
	fi
	if ! test ${sequ} -gt 0 2>/dev/null;then
		# Invalid last command sequ number
		sequ=0
	fi
	if [ 1 -lt ${cmds} ] && [ ${sequ} -lt ${cmds} ];then
		# Fast path: correct sequence can proceed
		rm -f ${LAST_FILE} ${SEQU_FILE}
		return
	fi
	if [ 0 -lt ${cmds} ];then
		echo ${cmds} > ${SEQU_FILE}
	else
		rm -f ${SEQU_FILE}
	fi
	diff=$(( ends + 10 - $(date +%s) ))
	if [ ${diff} -gt 0 ];then
		echo "Please wait $(format_time ${diff}) before retry." >&2
		trap - EXIT
		rm -f ${LOCK_FILE}
		exit 1
	fi
	rm -f ${LAST_FILE} ${SEQU_FILE}
	return
}

exit_trap()
{
	# shellcheck disable=SC3043
	{
		local diff=${1}
		local time
	}
	time=$(date +%s)
	if test ${diff} -gt 0 2>/dev/null;then
		diff=$(( time - diff ))
	else
		diff=0
	fi
	if test ${diff} -lt 10;then
		diff=10
	fi
	echo $(( time + diff * PENAL_MUL ))
}

usage()
{
	cat <<- EOF
		Blocksync-Fast only account. Supported command words are:

		apply-delta:    receive delta for your image via stdin
		create-digest:  create and store digest for your image
		get-digest:     get stored digest
		get-digest-md5: show md5sum of stored digest
		get-image:      send your image on stdin
		get-image-tar:  send your image as tar with sparse
		ls-l-image:     shows your image file
		stat-image:     get image file information
		cancel:         terminates a running session

		On "create-digest", a new digest will be created only if none
		is stored or the digest file is older than the image file. On
		"get-digest" or "get-digest-md5" the output is empty if no
		digest is stored. On "apply-delta", a stored digest is removed.
		For transfers, *-gzip command words exist, e.g. get-image-gzip.
		This site re-generates your digest from time to time to make
		sure the backup is still intact (meant to trigger a resync).

		Note: Successive repeated / parallel costly operations such as
		"create-digest" may trigger DDOS counteractions on this site.
	EOF
}

case ${1} in -h|--help)
	usage
	exit 0
;;"")
	echo "Need identifier param." >&2
	exit 1
;;esac

PENAL_MUL=1
LOCK_FILE=${IMG_DIR}/${1}.lock
LAST_FILE=${LOCK_FILE%.*}.last
SEQU_FILE=${LOCK_FILE%.*}.sequ

if (set -o noclobber;(echo $$;date +%s) > ${LOCK_FILE}) 2>/dev/null;then
	# shellcheck disable=SC2154
	trap 'exit=$?;set +e;exit_trap $(tail -n1 ${LOCK_FILE}) > ${LAST_FILE};rm -f ${LOCK_FILE};trap - EXIT;exit ${exit}' HUP INT TERM EXIT

	case ${SSH_ORIGINAL_COMMAND} in apply-delta)
		PENAL_MUL=2
		no_repeat_until 4
		rm -f ${IMG_DIR}/${1}.digest
		# Backup image size may change, e.g. on reinstall source device, hence --force
		blocksync-fast --apply-delta --force --dst=${IMG_DIR}/${1}.img
		exit $?
	;;apply-delta-gzip)
		PENAL_MUL=3
		no_repeat_until 4
		rm -f ${IMG_DIR}/${1}.digest
		zcat | blocksync-fast --apply-delta --force --dst=${IMG_DIR}/${1}.img
		exit $?
	;;create-digest)
		PENAL_MUL=2
		no_repeat_until 2
		if [ -s ${IMG_DIR}/${1}.img ];then
			if [ ! -s ${IMG_DIR}/${1}.digest ] || [ ${IMG_DIR}/${1}.img -nt ${IMG_DIR}/${1}.digest ];then
				blocksync-fast --make-digest --src=${IMG_DIR}/${1}.img --digest=${IMG_DIR}/${1}.digest --force
				exit $?
			fi
			stat -c %s ${IMG_DIR}/${1}.digest
			exit $?
		fi
	;;get-digest)
		no_repeat_until 3
		if [ -s ${IMG_DIR}/${1}.digest ];then
			cat ${IMG_DIR}/${1}.digest
			exit $?
		fi
	;;get-digest-gzip)
		PENAL_MUL=2
		no_repeat_until 3
		if [ -s ${IMG_DIR}/${1}.digest ];then
			gzip -c --fast --rsyncable ${IMG_DIR}/${1}.digest
			exit $?
		fi
	;;get-digest-md5)
		no_repeat_until 1
		get_digest_md5 ${IMG_DIR}/${1}.digest
		exit $?
	;;get-image)
		no_repeat_until 0
		cat ${IMG_DIR}/${1}.img
		exit $?
	;;get-image-gzip)
		PENAL_MUL=2
		no_repeat_until 0
		gzip -c --fast --rsyncable ${IMG_DIR}/${1}.img
		exit $?
	;;get-image-tar)
		no_repeat_until 0
		tar --directory ${IMG_DIR} --sparse -c ${1}.img
		exit $?
	;;get-image-tar-gzip)
		PENAL_MUL=2
		no_repeat_until 0
		tar --directory ${IMG_DIR} --sparse -c ${1}.img | gzip -c --fast --rsyncable
		exit $?
	;;ls-l-image)
		no_repeat_until 0
		LANG=C ls -l ${IMG_DIR}/${1}.img
		exit $?
	;;stat-image)
		no_repeat_until 0
		LANG=C stat ${IMG_DIR}/${1}.img
		exit $?
	;;help)
		no_repeat_until 0
		usage
		exit 0
	;;"")
		no_repeat_until 0
		echo "Empty \${SSH_ORIGINAL_COMMAND}." >&2
		exit 1
	;;cancel)
		no_repeat_until 0
		trap - EXIT
		rm -f ${LOCK_FILE} ${LAST_FILE} ${SEQU_FILE}
		exit 0
	;;*)
		no_repeat_until 0
		echo "Unknown command: ${SSH_ORIGINAL_COMMAND}." >&2
		exit 1
	;;esac
else
	pid=$(head -n1 ${LOCK_FILE})
	if ! ps -p ${pid} > /dev/null;then
		pid=
	fi
	time=$(tail -n1 ${LOCK_FILE})
	now=$(date +%s)
	diff=$(( now - time ))
	case ${SSH_ORIGINAL_COMMAND} in cancel)
		if [ ${diff} -lt 60 ];then
			echo "You need to wait a minute." >&2
			exit 1
		else
			test -n "${pid}" && kill ${pid}
			rm -f ${LOCK_FILE}
		fi
	;;*)
		case ${pid} in "")
			echo "Was running since $(format_time ${diff}), removing stray lock." >&2
			rm -f ${LOCK_FILE}
		;;*)
			echo "Already running since $(format_time ${diff})." >&2
			ps h ${pid} >&2
		;;esac
		exit 1
	;;esac
fi
