#!/bin/bash
# Copyright (C) 2006-2008 Vladimir V. Kamarzin <vvk@altlinux.org>
# Copyright (C) 2006 Michael Shigorin <mike@altlinux.org>
# Copyright (C) 2007,2015 Aleksey Avdeev <solo@altlinux.org>
#
# Mirror any ALT repository via rsync
#
# This file 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

# we need libshell functions
. shell-error
. shell-args
. shell-getopt

# colors
. /etc/init.d/outformat

show_help()
{
        cat <<EOF
Usage: $PROG [options]
Valid options are:
	-i, --interactive       force output to console
	-L, --logdir            use specified directory for store logfiles
	-u, --url               use specified source mirror
	-c, --config            use specified config file as addition
	-l, --list              synchronize only given repositories
	-a, --arch              synchronize only given architectures
	-s, --skip-backup       skip backup procedure
	    --link-list         use specified directories for search hardlinks
	-h, --help              show this text and exit
EOF
exit 1
}

pidfile=
cleanup_handler()
{
	trap - EXIT
	[ -z "$pidfile" ] || rm -f "$pidfile"
	[ "$PARTIAL" = "$RSHOME" ] || rm -rf "$PARTIAL"
	exit "$@"
}

exit_handler()
{
	cleanup_handler $?
}

signal_handler()
{
	cleanup_handler 143
}

#parse command line options
TEMP=`getopt -n $PROG -o i,L:,u:,c:,l:,a:,s,h -l interactive,logdir:,url:,config:,list:,arch:,skip-backup,link-list:,help -- "$@"` || show_help
eval set -- "$TEMP"

while :; do
        case "$1" in
		--) shift; break
			;;
		-i|--interactive) cl_interactive=1
			[ -z "$logdir" ] ||
				show_usage "Options --interactive and --logdir are mutually exclusive."
			;;
		-L|--logdir) shift ; logdir="$1"
			[ -z "$cl_interactive" ] ||
				show_usage "Options --interactive and --logdir are mutually exclusive."
			[ -d "$logdir" ] || fatal "$logdir: not a directory."
			;;
		-u|--url) shift ; cl_SRCROOT="$1"
			;;
		-c|--config) shift ; cl_config="$1"
			;;
		-l|--list) shift ; cl_LIST="$1"
			;;
		-a|--arch) shift ; cl_ARCH="$1"
			;;
		-s|--skip-backup) skip_backup=1
			;;
		--link-list) shift ; cl_LINK_LIST="$1"
			;;
		-h|--help) show_help
			;;
        esac
	shift
done

# check for command-line defined config
[ -n "$cl_config" ] && {
	[ -s "$cl_config" ] && config="$cl_config" || fatal "Empty config file specified."
}

# check for user-wide config
[ -s "$HOME"/.sisyphus-mirror/config -a -z "$config" ] && config="$HOME/.sisyphus-mirror/config" ||:

# check for system-wide config
[ -s /etc/sisyphus-mirror/sisyphus-mirror.conf -a -z "$config" ] &&
	config="/etc/sisyphus-mirror/sisyphus-mirror.conf" ||:

# source config
[ -n "$config" ] && . "$config" || fatal "No config files found or specified."

# override INTERACTIVE if "-i" option passed
[ x"$cl_interactive" = x1 ] && INTERACTIVE=1

# override LIST if defined with "-i" option
[ -z "$cl_LIST" ] || LIST="$cl_LIST"

# override SRCROOT if defined with "-u" option
[ -z "$cl_SRCROOT" ] || SRCROOT="$cl_SRCROOT"

# override ARCH if defined with "-a" option
[ -z "$cl_ARCH" ] || ARCH="$cl_ARCH"

# override LINK_LIST if defined with "--link_list" option
[ -z "$cl_LINK_LIST" ] || LINK_LIST="$cl_LINK_LIST"

# if some vars absent in config, define it here
RSHOME="${RSHOME:-$HOME/.sisyphus-mirror}"
PARTIAL="${PARTIAL:-$RSHOME/partial}"
MAXATTEMPTS="${MAXATTEMPTS:-10}"
TMOUT2="${TMOUT2:-15}"
INTERACTIVE="${INTERACTIVE:-0}"
ARGS="${ARGS:- -rltmvH --delete-delay --delete-excluded --stats}"
SRCROOT="${SRCROOT:-rsync://rsync.altlinux.org/ALTLinux}"

# without this vars refuse to continue
[ -n "$LIST" ] || fatal "Config file doesn't contain LIST variable."
[ -n "$DESTROOT" ] || fatal "Config file doesn't contain DESTROOT variable."

# we need homedir
if [ ! -d "$RSHOME" ]; then
	message "Directory $RSHOME does not exists, creating..."
	mkdir -p "$RSHOME" || fatal "Cannot create RSHOME: $RSHOME"
fi

# create --partial-dir
[ -d "$PARTIAL" ] || mkdir -p "$PARTIAL"

# check for $DESTROOT
[ -d "$DESTROOT" ] || fatal "\$DESTROOT doesn't exist, refuse to continue!"

# signal handler
trap exit_handler EXIT
trap signal_handler HUP PIPE INT QUIT TERM

# check that we are not running already
pidfile="$TMPDIR/sisyphus-mirror.pid"
if [ -f "$pidfile" ]; then
	kill -0 "$(cat $pidfile)" 2>/dev/null && fatal "Another copy is running."
fi

# create pidfile
echo $$ > "$pidfile"

export RSYNC_PROXY

###############################################################################
# rsync arguments calculation block

# calculate arch-includes
if [ -n "$ARCH" ]; then
	for a in $ARCH
	do
		ARCH_INCLUDE="$ARCH_INCLUDE --include=$a/**"
	done
fi

# calculate --link-dest
#
# find all sisyphus-like repos in LIST
for mirror in $LIST
do
	[ -f "$DESTROOT/$mirror/files/.timestamp" ] && LINK_DEST="$LINK_DEST --link-dest=$DESTROOT/$mirror" ||:
done

# find all sisyphus-like repos in LINK_LIST
for mirror in $LINK_LIST
do
	for inlist in $LIST
	do
		[ "$mirror" = "$inlist" ] && continue 2 ||:
	done
	[ -f "$DESTROOT/$mirror/files/.timestamp" ] && LINK_DEST="$LINK_DEST --link-dest=$DESTROOT/$mirror" ||:
done

# find all sisyphus-like repos in BACKUP_DIR
if [ -d "$BACKUP_DIR" ]; then
	while read line
	do
		LINK_DEST="$LINK_DEST --link-dest=${line%%/files/.timestamp}"
	done < <(find "$BACKUP_DIR" -type f -name .timestamp | grep '/files/.timestamp' | sort -nr)
fi

# leave only 20 --link-dest args
LINK_DEST="$(echo $LINK_DEST | cut -d ' ' -f1-20)"


RSYNCARGS="$ARGS"
[ -z "$TMOUT1" ] || RSYNCARGS="$RSYNCARGS --timeout=$TMOUT1"
[ -z "$EXCLUDE_FILE" ] || RSYNCARGS="$RSYNCARGS --exclude-from=$EXCLUDE_FILE"
[ -z "$INCLUDE_FILE" ] || RSYNCARGS="$RSYNCARGS --include-from=$INCLUDE_FILE"
[ -z "$ARCH_INCLUDE" ] || RSYNCARGS="$RSYNCARGS --exclude=SRPMS*/ $ARCH_INCLUDE --include=*/ --include=/files/.timestamp --exclude=*"
[ -z "$SPEED" ] || RSYNCARGS="$RSYNCARGS --bwlimit=$SPEED"
[ -z "$PARTIAL" ] || RSYNCARGS="$RSYNCARGS --partial-dir=$PARTIAL"
[ -z "$LINK_DEST" ] || RSYNCARGS="$RSYNCARGS $LINK_DEST"

# END rsync arguments calculation block
###############################################################################

for mirror in $LIST
do
	OK=""
	FLAG="$DESTROOT/$mirror/__SYNCING__"

	# get logfile directory and name
	mirror_log="$(echo "$mirror" | tr "/" "_")"
	[ -n "$logdir" ] && LOG="$logdir/$mirror_log.log" || LOG="$RSHOME/$mirror_log.log"
	[ "$INTERACTIVE" = 1 ] || date >> "$LOG"

	# create directory for $mirror if needed
	mkdir -p -- "$DESTROOT/$mirror"

	# create flag
	touch "$FLAG"

	# backup mirror state if BACKUP variable set and -s option is not given
	if [ x"$skip_backup" != "x1" -a -n "$BACKUP_TYPE" ]; then
		[ -n "$BACKUP_DIR" ] || fatal "Backup is enabled, but BACKUP_DIR not set"
		case "$BACKUP_TYPE" in
			one)
				mkdir -p "$BACKUP_DIR/$mirror"
				if [ "$INTERACTIVE" = 1 ]; then
					SETCOLOR_SUCCESS
					message "Backing up $mirror into $BACKUP_DIR/$mirror"
					SETCOLOR_NORMAL
				fi

				rsync -aPH --delete-delay --link-dest="$DESTROOT/$mirror" \
					"$DESTROOT/$mirror/" "$BACKUP_DIR/$mirror" || exit 1
				;;
			snapshots)
				timestamp_file="$(find "$DESTROOT/$mirror" -type f -name release |head -1)"
				backup_date="$(date +%Y%m%d -r $timestamp_file)"
				if [ ! -z "$timestamp_file" ]; then
					mkdir -p "$BACKUP_DIR/$backup_date/$mirror"

					if [ "$INTERACTIVE" = 1 ]; then
						SETCOLOR_SUCCESS
						message \
						"Backing up $mirror into $BACKUP_DIR/$backup_date/$mirror"
						SETCOLOR_NORMAL
					fi

					rsync -aPH --delete-delay --link-dest="$DESTROOT/$mirror" \
						"$DESTROOT/$mirror/" "$BACKUP_DIR/$backup_date/$mirror"	|| exit 1
				fi
				;;
			*) fatal "Unrecognized backup type: $BACKUP_TYPE"
		esac

	fi

	# get destination dir for rsync
	[ -z "$TMPDEST" ] && DEST="$DESTROOT/$mirror" || DEST="$DESTROOT/$TMPDEST/$mirror"

	# create directories for $mirror
	if [ -n "$TMPDEST" ]; then
		mkdir -p "$DESTROOT/$TMPDEST/$mirror"
		cp -al "$DESTROOT/$mirror" "$DESTROOT/$TMPDEST/$mirror/../"
	fi

	# synchronization
	for attempt in `seq 1 "$MAXATTEMPTS"`; do
		if [ "$INTERACTIVE" = 1 ]; then
			SETCOLOR_SUCCESS
			message "Mirroring $mirror"
			SETCOLOR_NORMAL
			if rsync $RSYNCARGS "$SRCROOT/$mirror/" "$DEST"; then
				OK=1
				SETCOLOR_SUCCESS
				message "Successfuly mirrored $mirror"
				SETCOLOR_NORMAL
				break
			fi
		else
			if rsync $RSYNCARGS "$SRCROOT/$mirror/" "$DEST" &> "$LOG"; then
				OK=1
				break
			fi
		fi
		sleep "$TMOUT2"
	done

	# perform some actions after successful sync
	if [ "$OK" = 1 ]; then
		# move fresh mirror from temp dir to main place
		if [ -n "$TMPDEST" ]; then
			if [ "$INTERACTIVE" = 1 ]; then
				SETCOLOR_SUCCESS
				message "Moving $mirror from temp dir to main dir."
				SETCOLOR_NORMAL
			fi
			mv "$DESTROOT/$mirror" "$DESTROOT/$mirror.old"
			mv "$DEST" "$DESTROOT/$mirror"
			rm -rf "$DESTROOT/$mirror.old"
		fi
	else
		message "$mirror: synchronization failed."

	fi

	rm -f "$FLAG"
	[ "$INTERACTIVE" = 1 ] || date >> "$LOG"
done

if [ "$INTERACTIVE" = 1 ]; then
	SETCOLOR_SUCCESS
	message "All done"
	SETCOLOR_NORMAL
fi
