#!/bin/bash

#
# Anything-sync-daemon by graysky <graysky AT archlinux DOT us>
# Inspired by some code originally written by Colin Verot
#

BLD="\e[01m"
RED="\e[01;31m"
GRN="\e[01;32m"
BLU="\e[01;34m"
NRM="\e[00m"
VERS="5.74"

ASDCONF=${ASDCONF:-"/etc/asd.conf"}
DAEMON_FILE=${DAEMON_FILE:-"/run/asd"}

# Setup check /etc/asd.conf
if [[ -f /etc/asd.conf ]]; then
	. "$ASDCONF"
else
	echo -e " ${BLD}Cannot find $ASDCONF so bailing."${NRM}
	echo -e " ${BLD}Reinstall package to use anything-sync-daemon."${NRM}
	exit 1
fi

# if asd is active, source the snapshot of /etc/asd.conf preferentially
if [[ -f "${DAEMON_FILE}.conf" ]]; then
	ASDCONF="${DAEMON_FILE}.conf"
	. "$ASDCONF"
fi

[[ -z "$VOLATILE" ]] && VOLATILE=/tmp

# simple function to determine user intent rather than using a null value
case "${USE_OVERLAYFS,,}" in
	y|yes|true|t|on|1|enabled|enable|use)
		OLFS=1
		;;
	*)
		OLFS=0
		;;
esac

# since the default for this one is a yes, need to force a null value to yes
[[ -z "${USE_BACKUPS,,}" ]] && USE_BACKUPS="yes"

case "${USE_BACKUPS,,}" in
	y|yes|true|t|on|1|enabled|enable|use)
		CRRE=1
		;;
	*)
		CRRE=0
		;;
esac

# determine is we are using overlayfs (v22 and below) or overlay (v23 and above)
# overlay FS v23 and later requires both an upper and a work directory, both on
# the same filesystem, but not part of the same subtree.
#
# ubuntu 15.04 has both overlay and overlayfs so prefer version 23
if [[ $OLFS -eq 1 ]]; then
	# first test if either module is manually loaded manually or hardcoded
	[[ $(grep -ciE "overlayfs$" /proc/filesystems) -eq 1 ]] && OLFSVER=22
	[[ $(grep -ciE "overlay$" /proc/filesystems) -eq 1 ]] && OLFSVER=23
	if [[ -z $OLFSVER ]]; then
		# since mount should call modprobe on invocation, check to see if either
		# module is in the tree using modinfo
		modinfo overlayfs &>/dev/null
		[[ $? -eq 0 ]] && OLFSVER=22
		modinfo overlay &>/dev/null
		[[ $? -eq 0 ]] && OLFSVER=23
	fi
fi

# get distro name
# first try os-release
if [[ -f /etc/os-release ]]; then
	source /etc/os-release
	if [[ -n "$PRETTY_NAME" ]]; then
		distro="$PRETTY_NAME"
	elif [[ -n "$NAME" ]]; then
		distro="$NAME"
	fi
else
	# if not os-release try issue
	if [[ -n $(sed 's| \\.*$||' /etc/issue | head -n 1) ]]; then
		distro="$(sed 's| \\.*$||' /etc/issue | head -n 1)"
	else
		# fuck it
		distro=
	fi
fi

header() {
	[[ -z "$distro" ]] && echo -e "${BLD}Anything-sync-daemon v$VERS"${NRM} ||
		echo -e "${BLD}Anything-sync-daemon v$VERS${NRM}${BLD} on $distro"${NRM}
	echo
}

dep_check() {
	# Function is used to insure all dependencies are installed
	command -v rsync >/dev/null 2>&1 || {
	echo "I require rsync but it's not installed. Aborting." >&2
	exit 1; }
	command -v awk >/dev/null 2>&1 || {
	echo "I require awk but it's not installed. Aborting." >&2; exit 1; }
	if [[ $OLFS -eq 1 ]]; then
		[[ $OLFSVER -ge 22 ]] || {
		youare=$(logname)
		echo -e " ${BLD}Your kernel requires either the ${BLU}overlay${NRM}${BLD} or ${BLU}overlayfs${NRM}${BLD} module to use"${NRM}
		echo -e " ${BLD}to use asd's in overlay mode. Cannot find either in your kernel so compile it in and"${NRM}
		echo -e " ${BLD}try again or remove the option from ${BLU}$ASDCONF${NRM}${BLD}. ${RED}Aborting!"${NRM} >&2; exit 1;}
	fi
}

config_check() {
	# nothing to do if these are empty
	if [[ -z "${WHATTOSYNC[0]}" ]]; then
		echo -e " ${BLD}Must define at least one directory in ${NRM}${BLU}$ASDCONF"${NRM}
		exit 1
	fi

	# make sure the user defined real dirs and crap out if not
	for DIR in "${WHATTOSYNC[@]}"; do
		if [[ ! -d "$DIR" ]]; then
			BACKUP="${DIR%/*}/.${DIR##*/}-backup_asd"
			if [[ ! -d "$BACKUP" ]]; then
				echo -e "${BLD}Bad entry in your WHATTOSYNC array detected:"${NRM}
				echo -e "${BLD}${RED}$DIR"${NRM}
				echo -e "${BLD}Edit ${BLU}$ASDCONF${NRM}${BLD} correcting the mistake and try again."${NRM}
				exit 1
			fi
		fi
	done
}

root_check() {
	# we call this to ensure that only the root user is calling the
	# function why care? both the sync and unsync functions require
	# root access to $DAEMON_FILE Running as unprivileged user will
	# fuck up the sync process resulting in unhappy users

	if [[ $EUID -ne 0 ]]; then
		echo -e " ${BLD}This function must be called as root!"${NRM} 1>&2
		exit 1
	fi
}

ungraceful_state_check() {
	# if the machine was ungracefully shutdown then the backup will be
	# on the filesystem and the link to tmpfs will be on the filesystem
	# but the contents will be empty we need to simply remove the link
	# and rotate the backup into place
	local DIR USER BACKUP TMP
	for DIR in "${WHATTOSYNC[@]}"; do
		# did user define a real dir
		# this is the hdd bound backup in case of power failure
		BACKUP="${DIR%/*}/.${DIR##*/}-backup_asd"
		if [[ -d "$BACKUP" ]]; then
			USER=$(stat -c %U "$BACKUP")
		else
			USER=$(stat -c %U "$DIR")
		fi
		TMP="$VOLATILE/asd-$USER$DIR"

		if [[ -e "$TMP"/.flagged ]]; then
			# all is well so continue
			return
		else
			NOW=$(date +%Y%m%d_%H%M%S)
			[[ -h "$DIR" ]] && unlink "$DIR"
			if [[ -d "$BACKUP" ]]; then
				[[ $CRRE -eq 1 ]] &&
					cp -a --reflink=auto "$BACKUP" "$BACKUP-crashrecovery-$NOW"
				mv "$BACKUP" "$DIR"
			fi
		fi
	done
}

cleanup() {
	local DIR USER GROUP BACKUP TMP
	for DIR in "${WHATTOSYNC[@]}"; do
		# this is the hdd bound backup in case of power failure
		BACKUP="${DIR%/*}/.${DIR##*/}-backup_asd"
		USER=$(stat -c %U "$DIR")
		GROUP=$(id -g "$USER")
		TMP="$VOLATILE/asd-$USER$DIR"
		TMPRW="$VOLATILE/asd-$USER$DIR-rw"
		TMPWK="$VOLATILE/.asd-$USER$DIR"

		if [[ -d "$DIR" ]]; then
			ls "$BACKUP"-crashrecovery* &>/dev/null
			if [[ $? -eq 0  ]]; then
				CRASHArr=("$BACKUP"-crashrecovery*)
				echo -e ${BLD}"Deleting ${#CRASHArr[@]} crashrecovery dir(s) for sync target ${BLU}$DIR"${NRM}
				for backup in "${CRASHArr[@]}"; do
					echo -e ${BLD}${RED}" $backup"${NRM}
					rm -rf "$backup"
				done
				unset CRASHArr
			else
				echo -e ${BLD}"Found no crashrecovery dirs for: ${BLU}$DIR${NRM}${BLD}"${NRM}
			fi
			echo
		fi
	done
}

do_sync() {
	touch "$DAEMON_FILE"

	# make a snapshot of /etc/asd.conf and redefine its location to tmpfs while
	# asd is running to keep any edits made to the live /etc/asd.conf from
	# potentially orphaning the tmpfs copies thus preserving the data
	[[ ! -f "${DAEMON_FILE}.conf" ]] && cp "$ASDCONF" "${DAEMON_FILE}.conf"

	# sync to tmpfs and back again
	local DIR USER GROUP BACKUP TMP
	for DIR in "${WHATTOSYNC[@]}"; do
		# this is the hdd bound backup in case of power failure
		BACKUP="${DIR%/*}/.${DIR##*/}-backup_asd"
		USER=$(stat -c %U "$DIR")
		GROUP=$(id -g "$USER")
		TMP="$VOLATILE/asd-$USER$DIR"
		TMPRW="$VOLATILE/asd-$USER$DIR-rw"
		TMPWK="$VOLATILE/.asd-$USER$DIR"

		# make tmpfs container
		if [[ -d "$DIR" ]]; then
			# retain permissions on sync target
			PREFIXP=$(stat -c %a "$DIR")
			[[ -r "$TMP" ]] || install -dm$PREFIXP --owner="$USER" --group="$GROUP" "$TMP"

			if [[ $OLFS -eq 1 ]]; then
				if [[ $OLFSVER -eq 23 ]]; then
					[[ -r "$TMPRW" ]] || install -dm$PREFIXP --owner="$USER" --group="$GROUP" "$TMPRW"
					[[ -r "$TMPWK" ]] || install -dm$PREFIXP --owner="$USER" --group="$GROUP" "$TMPWK"
				elif [[ $OLFSVER -eq 22 ]]; then
					[[ -r "$TMPRW" ]] || install -dm$PREFIXP --owner="$USER" --group="$GROUP" "$TMPRW"
				fi
			fi

			# backup target and link to tmpfs container
			if [[ $(readlink "$DIR") != "$TMP" ]]; then
				mv "$DIR" "$BACKUP"
				ln -s "$TMP" "$DIR"
				chown -h "$USER":"$GROUP" "$DIR"
			fi

			# sync the tmpfs targets to the disc
			if [[ -e "$TMP"/.flagged ]]; then
				rsync -aogX --delete-after --inplace --no-whole-file --exclude .flagged "$DIR/" "$BACKUP/"
			else
				# initial sync
				if [[ $OLFS -eq 1 ]]; then
					if [[ $OLFSVER -eq 23 ]]; then
						mount -t overlay overlaid -olowerdir="$BACKUP",upperdir="$TMPRW",workdir="$TMPWK" "$TMP"
					elif [[ $OLFSVER -eq 22 ]]; then
						mount -t overlayfs overlaid -olowerdir="$BACKUP",upperdir="$TMPRW" "$TMP"
					fi
				else
					rsync -aogX --inplace --no-whole-file "$BACKUP/" "$TMP/"
				fi
				touch "$DIR"/.flagged
			fi
		fi
	done
	echo -e "${BLD}Sync successful"${NRM}
}

do_unsync() {
	rm -f "$DAEMON_FILE" "${DAEMON_FILE}.conf"

	local DIR USER BACKUP TMP
	for DIR in "${WHATTOSYNC[@]}"; do
		# this is the hdd bound backup in case of power failure
		BACKUP="${DIR%/*}/.${DIR##*/}-backup_asd"
		USER=$(stat -c %U "$DIR")
		GROUP=$(id -g "$USER")
		TMP="$VOLATILE/asd-$USER$DIR"
		TMPRW="$VOLATILE/asd-$USER$DIR-rw"
		TMPWK="$VOLATILE/.asd-$USER$DIR"

		# remove link and move data from tmpfs to disk
		if [[ -h "$DIR" ]]; then
			unlink "$DIR"
			# this assumes that the backup is always
			# updated so be sure to invoke a sync before an unsync

			# restore original dirtree
			[[ -d "$BACKUP" ]] && mv "$BACKUP" "$DIR"
			if [[ $OLFS -eq 1 ]] && mountpoint -q "$TMP"; then
				umount -l "$TMP"
				rm -rf "$VOLATILE/asd-$USER" "$VOLATILE/asd-$USER-rw" "$VOLATILE/.asd-$USER" &>/dev/null
			else
				[[ -d "$TMP" ]] && rm -rf "$VOLATILE/asd-$USER"
			fi
		fi
	done
	echo -e "${BLD}Unsync successful"${NRM}
}

parse() {
	if [[ -f /usr/lib/systemd/system/asd.service ]]; then
		# running systemd
		asd_state=$(systemctl is-active asd)
		resync_state=$(systemctl is-active asd-resync.timer)
		[[ "$asd_state" = "active" ]] && asd_color="${GRN}" || asd_color="${RED}"
		[[ "$resync_state" = "active" ]] && resync_color="${GRN}" || resync_color="${RED}"
		echo -e " ${BLD}Systemd service is currently ${asd_color}$asd_state${NRM}${BLD}."${NRM}
		echo -e " ${BLD}Systemd resync service is currently ${resync_color}$resync_state${NRM}${BLD}."${NRM}
	else
		# using other init system + cron job for resync
		[[ -x /etc/cron.hourly/asd-update ]] && resync_state="present" || resync_state="not present"
		[[ "$resync_state" = "present" ]] && resync_color=${GRN} || resync_color=${RED}
		echo -e " ${BLD}Daemon pid file is $([[ -f $DAEMON_FILE ]] &&
			echo -e ${GRN}present${NRM}${BLD} || echo -e ${RED}not present${NRM}${BLD})."${NRM}
		echo -e " ${BLD}Resync cronjob is ${resync_color}${resync_state}${NRM}${BLD}."${NRM}
	fi
	[[ $OLFS -eq 1 ]] &&
		echo -e "${BLD} Overlayfs v$OLFSVER is currently ${GRN}active${NRM}${BLD}."${NRM} ||
		echo -e "${BLD} Overlayfs technology is currently ${RED}inactive${NRM}${BLD}."${NRM}
	echo
	echo -e "${BLD}Asd will manage the following per ${BLU}${ASDCONF}${NRM}${BLD} settings:"${NRM}
	echo
	local DIR USER GROUP BACKUP TMP
	for DIR in "${WHATTOSYNC[@]}"; do
		# this is the hdd bound backup in case of power failure
		BACKUP="${DIR%/*}/.${DIR##*/}-backup_asd"
		USER=$(stat -c %U "$DIR")
		GROUP=$(id -g "$USER")
		TMP="$VOLATILE/asd-$USER$DIR"
		TMPRW="$VOLATILE/asd-$USER$DIR-rw"
		TMPWK="$VOLATILE/.asd-$USER$DIR"

		# sync target dir size
		psize=$(du -Lh --max-depth=0 "$DIR" 2>/dev/null | awk '{ print $1 }')
		if [[ -d "$DIR" ]]; then
			# first count up any crashrecovery dirs
			ls "$BACKUP"-crashrecovery* &>/dev/null
			[[ $? -eq 0  ]] && CRASHArr=("$BACKUP"-crashrecovery*)
			echo -en " ${BLD}owner/group id:"
			echo -e $(tput cr)$(tput cuf 20) "$USER"/"$GROUP"${NRM}
			echo -en " ${BLD}target to manage:"
			echo -e $(tput cr)$(tput cuf 20) ${GRN}"$DIR"${NRM}
			echo -en " ${BLD}sync target:"
			echo -e $(tput cr)$(tput cuf 20) ${BLU}"$BACKUP"${NRM}
			echo -en " ${BLD}tmpfs target:"
			echo -e $(tput cr)$(tput cuf 20) ${RED}"$TMP"${NRM}
			echo -en " ${BLD}dir size:"
			echo -e "$(tput cr)$(tput cuf 20) $psize"${NRM}
			if [[ $OLFS -eq 1 ]]; then
				rwsize=$(du -Lh --max-depth=0 "$TMPRW" 2>/dev/null | awk '{ print $1 }')
				echo -en " ${BLD}overlayfs size:"
				echo -e "$(tput cr)$(tput cuf 20) $rwsize"${NRM}
			fi
			echo -en " ${BLD}recovery dirs:"
			if [[ "${#CRASHArr[@]}" -eq 0 ]]; then
				echo -e "$(tput cr)$(tput cuf 20) none"${NRM}
			else
				echo -e "$(tput cr)$(tput cuf 20) ${RED}${#CRASHArr[@]}${NRM}${BLD} <- delete with the c option"${NRM}
				for backup in "${CRASHArr[@]}"; do
					psize=$(du -Lh --max-depth=0 "$backup" 2>/dev/null | awk '{ print $1 }')
					echo -en " ${BLD} dir path/size:"
					echo -e "$(tput cr)$(tput cuf 20) ${BLU}$backup ${NRM}${BLD}($psize)"${NRM} 
				done
			fi
			unset CRASHArr
			echo
		fi
	done
}

case "$1" in
	p|P|Parse|parse|Preview|preview|debug)
		dep_check && config_check && header && parse
		;;
	c|C|clean|Clean)
		dep_check && config_check && header && cleanup
		;;
	sync)
		[[ ! -f "$DAEMON_FILE" ]] && root_check && dep_check && config_check &&
			ungraceful_state_check && do_sync
		;;
	resync)
		[[ -f "$DAEMON_FILE" ]] && root_check && do_sync
		;;
	unsync)
		# make sure the daemon ran to setup the links
		[[ -f "$DAEMON_FILE" ]] && root_check && do_sync && do_unsync
		;;
	*)
		echo -e "${BLD}Anything-sync-daemon v$VERS${NRM}"
		echo
		echo -e " ${BLD}$0 ${NRM}${GRN}[option]${NRM}"
		echo -e " ${BLD} ${NRM}${GRN}preview${NRM}${BLD}  Parse config file (${NRM}${BLU}${ASDCONF}${NRM}${BLD}) to see what will be managed."${NRM}
		echo -e " ${BLD} ${NRM}${GRN}clean${NRM}${BLD}		Clean (delete without prompting) ALL crashrecovery dirs."${NRM}
		echo -e " ${BLD} ${NRM}${GRN}resync${NRM}${BLD} Synchronize the tmpfs and media bound copy. Must be run as root user."${NRM}
		echo -e " ${BLD} ${NRM}${RED}sync${NRM}${BLD}   Force a manual sync. Must be run as root user and NOT recommended."${NRM}
		echo -e " ${BLD} ${NRM}${RED}unsync${NRM}${BLD} Force a manual unsync. Must be run as root user and NOT recommended."${NRM}
		echo
		echo -e " ${BLD}It is ${RED}HIGHLY DISCOURAGED${NRM}${BLD} to directly call $0 to sync or to unsync."${NRM}
		if [[ -f /usr/lib/systemd/system/asd.service ]]; then
			echo -e " ${BLD}Instead, use systemd to start/stop anything-sync-daemon."${NRM}
			echo
			echo -e " ${BLD}systemctl ${NRM}${GRN}[option]${NRM}${BLD} asd asd-resync"${NRM}
			echo -e " ${BLD} ${NRM}${GRN}start${NRM}${BLD}    Turn on daemon; make symlinks and actively manage targets in tmpfs."${NRM}
			echo -e " ${BLD} ${NRM}${GRN}stop${NRM}${BLD}   Turn off daemon; remove symlinks and rotate tmpfs data back to disc."${NRM}
			echo -e " ${BLD} ${NRM}${GRN}enable${NRM}${BLD} Autostart daemon when system comes up."${NRM}
			echo -e " ${BLD} ${NRM}${GRN}disable${NRM}${BLD}  Remove daemon from the list of autostart daemons."${NRM}
		elif [[ -f /etc/init.d/asd ]]; then
			echo -e " ${BLD}Instead, use the init system to start/stop anything-sync-daemon."${NRM}
			echo
			echo -e " ${BLD}sudo service asd ${NRM}${GRN}[option]${NRM}${BLD} or /etc/init.d/asd ${NRM}${GRN}[option]"${NRM}
			echo -e " ${BLD} ${NRM}${GRN}start${NRM}${BLD}  Turn on daemon; make symlinks and actively manage targets in tmpfs."${NRM}
			echo -e " ${BLD} ${NRM}${GRN}stop${NRM}${BLD} Turn off daemon; remove symlinks and rotate tmpfs data back to disc."${NRM}
		fi
		;;
esac
exit 0

#vim:set ts=2 sw=2 et:
