#!/bin/ksh
#
# Copyright (c) 2018-2021 Stuart Henderson <sthen@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

err()
{
	logger -t reposync -p user.err "${synchost}: $*"
	echo "${0##*/}: $*" >&2
	exit 1
}

warn()
{
	logger -t reposync -p user.notice "${synchost}: $*"
	echo "${0##*/}: $*" >&2
}

usage()
{
	echo "usage: ${0##*/} [-46fq] [-p | -l username] [-s sets] [-w warntime]" >&2
	echo "                rsync://upstream/path [destination]" >&2
	exit 1
}

csumflag=
force=false
flags=-i
ipflags=
fwduser=anoncvs
quiet=false
sets=www,xenocara,ports,src
warntime=
while getopts "46fl:pqs:w:" c; do
	case $c in
	4) ipflags=-4		;;
	6) ipflags=-6		;;
	q) quiet=true
	   flags=-q		;;
	f) force=true
	   forced=" [forced]"
	   csumflag=-c		;;
	l) fwduser=$OPTARG	;;
	p) fwduser=		;;
	s) if [[ -n $OPTARG ]] && echo "$OPTARG" |
	     sed -E 's/(www|xenocara|ports|src)(,|$)//g' |
	     grep -q '^$'; then
		sets=$OPTARG
	   else
		err "invalid sets"
	   fi			;;
	w) if [[ -n ${OPTARG##*[!0-9]*} ]]; then
		warntime=$OPTARG
	   else
		err "invalid warning time"
	   fi			;;
	*) usage		;;
	esac
done
shift $((OPTIND-1))
[[ $# == [12] ]] || usage

synchost=$1
repodir=$(readlink -f "${2:-/cvs}")
rundir=/var/db/reposync

oldhash=invalid
hashfile=$rundir/reposync.hash
lockfile=$rundir/reposync.lock
sockfile=$rundir/reposync.sock

run_rsync()
{
	if [[ -n $fwduser ]]; then
		# reach rsync on the server via an ssh port-forward
		sshopts="$ipflags -S $sockfile"
		sshopts="$sshopts -o ControlMaster=auto"
		sshopts="$sshopts -o ControlPersist=1m"
		sshopts="$sshopts -o BatchMode=Yes"
		sshopts="$sshopts -o UserKnownHostsFile='$rundir/known_hosts /usr/local/share/reposync/ssh_known_hosts'"
		sshopts="$sshopts -W localhost:rsync"
		sshopts="$sshopts -l $fwduser"
		/usr/local/bin/rsync -e "ssh $sshopts" "$@" 2>&1
	else
		# plain rsync-over-TCP
		/usr/local/bin/rsync $ipflags "$@" 2>&1
	fi
}

for i in "$rundir" "$repodir"; do
	[[ ! -d $i ]] || [[ ! -w $i ]] &&
	    err "$i must exist as a writable directory"
done

if [[ $(id -u) != $(stat -L -f "%u" "$repodir") ]]; then
	err "should be run by the uid owning the repository"
fi

cd $rundir || err "could not cd to $rundir"

if [[ -h $lockfile ]]; then
	# read the pid from $lockfile symlink target
	lockedpid=$(stat -f %Y $lockfile)

	# exit if it's A) still running and B) looks like this script
# shellcheck disable=SC2009
	if ps -o command -p "$lockedpid" | grep -q "${0##*/}"; then
		err "already running?"
	fi

	# not still running, the lock must be stale (machine panicked, etc) so zap it
	rm -f $lockfile
fi

ln -s $$ $lockfile || err "could not lock $lockfile"

if [[ -n $fwduser ]]; then
	sshtrap="ssh -Fnone -S $sockfile -O exit -q -l $fwduser erewhon"
else
	sshtrap=true
	$quiet || warn "using non-authenticated cleartext rsync protocol."
fi
# shellcheck disable=SC2064
trap "$sshtrap; rm -f $lockfile" 0 1 2 15

if [[ ! -e $rundir/known_hosts && -r ~/.ssh/known_hosts ]]; then
	cp ~/.ssh/known_hosts $rundir/known_hosts
fi

# check CVSROOT directory listing to identify updates; primarily for
# ChangeLog but val-tags may also be updated after a checkout was done
# using a new tag. ignore "history" (lists read-only operations).
_t=$(run_rsync --exclude='history*' "${synchost}/CVSROOT/")
_e=$?
[[ -n $fwduser ]] && case $_t in
	"stdio forwarding failed"*|"Stdio forwarding request failed"*)
		err "mirror does not support ssh port-forwarding, or rsync not running" ;;
	"Host key verification failed"*)
		err "host key verification failed - see $rundir/known_hosts" ;;
esac
[[ $_e -eq 0 ]] || err "rsync error: $_t"
# shellcheck disable=SC2086
newhash="${synchost} ${sets} $(echo $_t | sha256)"

if [[ -r $hashfile ]]; then
	age=$(($(date +%s) - $(stat -t %s -f %m $hashfile)))
	# don't entirely rely on CVSROOT files; not all tree operations
	# result in a change there so also do a full update run at least
	# every 6h.
	if ((age < 6*60*60)); then
		oldhash=$(< $hashfile)
	else
		reason="${age}s old, "
	fi
else
	reason="new fetch, "
fi

[[ $oldhash != "$newhash" ]] && reason="new files seen, "

finish() {
	success=$1
	shift
	length=$SECONDS
	if $success; then
		outcome=""
		level="info"
	else
		outcome=", failed"
		level="err"
	fi
	if [[ -n ${warntime} && ${length} -gt ${warntime} ]]; then
		$success && level="notice"
		logger -t reposync -p user.${level} "${synchost}: $*, ${length}s${forced}${outcome} (slow)"
		warn "slow ${outcome} rsync, $((length/60))m$((length%60))s"
		exit 1
	else
		logger -t reposync -p user.${level} "${synchost}: $*, ${length}s${forced}${outcome}"
		$success || err "rsync failed"
	fi
}

if $force || [[ $oldhash != "$newhash" ]]; then
	# only update saved hash if sync was successful; otherwise leave
	# the old one so sync is reattempted next run
# shellcheck disable=SC2086
	if run_rsync -rlptz $flags $csumflag --omit-dir-times --delete \
	    --exclude='#cvs.*' --exclude='CVSROOT/history*' \
	    ${synchost}/{CVSROOT,${sets}} "$repodir"/; then
		echo "$newhash" > $hashfile
		finish true "${reason}updated"
	else
		finish false "${reason}failed"
	fi
else
	finish true "no update"
fi
