#!/bin/ash
# James Budiono 2011-2015
# License: GNU GPL Version 3 or later
#
# Load / unload SFS on the fly.
# This is the system SFS loader for Fatdog64.
#
# version 8: convert to use ash/dash, Xdialog, sfs is shown in layer order
# version 9: minor fixes, replace losetup with cat /sys/block/loop$/loop/backing_file
#
# Fatdog64 SFS structure (since 630 or version 7):
# ----
# In addition to standard structure, Fatdog64 SFS stores some metadata in
# /tmp/sfs. This metadata isn't normally visible as /tmp is usually 
# over-mounted and hidden by tmpfs. 
# The metadata is:
# a) /tmp/sfs/autorun.sh - this is the load/unload script. 
#    This script will be run when the SFS is loaded/unloaded, with these:
#    $1-event ({system}load/unload/unload-error), $2-sfs branch path.
#    Autorun can be disabled, see DISABLE_AUTORUN below.
# b) /tmp/sfs/msg - this is the message that will be displayed when the SFS
#    is loaded.
# 
# Config file:    /etc/load_sfs.conf
# Auto-load file: /etc/load_sfs
#

### root only
if [ $(id -u) -ne 0 ]; then
	[ "$DISPLAY" ] && exec gtksu "System SFS Loader" "$0" "$@"
	exec su -c "$0 $@"
fi

### configuration
APP_TITLE="Fatdog SFS Loader"
MAXLOOP=250					# highest loop device number we can use
RESERVED=10					# reserve 10 loop devices for filemnt and others
SFS_DIR=/mnt/home			# default location of sfs files
BASE_SFS=fd64.sfs			# don't show base sfs
AUFS_ROOT=/aufs
PUP_RO=pup_ro
LISTFILE=/etc/load_sfs
XDIALOG_STD_OPTIONS="--separate-output --stdout --title '$APP_TITLE'"

### SFS metadata
SFS_DATA_ROOT=tmp/sfs # don't use absolute path
SFS_RUN_SCRIPT=$SFS_DATA_ROOT/autorun.sh
SFS_MSG=$SFS_DATA_ROOT/msg
DISABLE_AUTORUN= # make this non-blank to disable autorun

### post-load/unload hooks
# put it here so it can be overriden by config file
# $1-full path to just-loaded sfs
post_load_hook() { refresh_panel_menu; }
post_unload_hook() { refresh_panel_menu; }

### config file to override all settings and hooks
[ -e /etc/load_sfs.conf ] && . /etc/load_sfs.conf
export SFS_DIR BASE_SFS LISTFILE

### run-time variables with initial startup values
# boot-time configuration
[ -e $BOOTSTATE_PATH ] && . $BOOTSTATE_PATH
[ "$BASE_SFS_PATH" ] && BASE_SFS=${BASE_SFS_PATH##*/}
[ -z $AUFS_ROOT_ID ] && AUFS_ROOT_ID=$(sed -n '/^aufs/{s|.*si=|si_|;s| .*||;p;q}' /proc/mounts)

# insertion point - below tmpfs, pup_save and pup_multi
INSERT_POINT=1	# tmpfs / pup_save
[ "$TMPFS_MOUNT" -a "$SAVEFILE_MOUNT" ] && INSERT_POINT=2   # both tmpfs & pup_save
[ "$MULTI_MOUNT" ] && INSERT_POINT=$(( $INSERT_POINT + 1 )) # pup_multi is another layer


########## core load/unload functions ###########

refresh_panel_menu() {
	[ "$DISPLAY" ] && touch /usr/share/applications/abiword.desktop
	return 0
}

# $@ text to display
message() {
	local action="$1"
	shift
	if [ "$DISPLAY" ]; then
		Xdialog --title "$APP_TITLE" --backtitle "$action" --infobox "$*" 0 0 10000
	else
		echo -e "${action}: $@"
	fi
}

# output - FREELOOP = free loop device
find_free_loop() {
	local in_use 
	in_use=$(losetup -a | sed 's/:.*//; s|^.*/||' | tr '\n' '+')
	
	FREELOOP=$RESERVED
	while [ $FREELOOP -le $MAXLOOP ]; do
		case "$in_use" in
			*"loop${FREELOOP}+"*) 
				FREELOOP=$((FREELOOP+1)); 
				continue ;;
		esac
		break
	done
}

# $1 = loop device number
make_loop_device() {
	! [ -b /dev/loop$1 ] && mknod /dev/loop$1 b 7 $1
}

# $1 = pup_ro number
make_mount_point() {
	! [ -d $AUFS_ROOT/$PUP_RO$1 ] && mkdir -p $AUFS_ROOT/$PUP_RO$1
}

# $1-sfs, return: branch = aufs branch where $1 is loaded
get_branch() {
	local p
	branch=""
	for p in $(sed -n "/$PUP_RO/{s|=.*||;p}" /sys/fs/aufs/$AUFS_ROOT_ID/br[0-9]*); do
		case "$(cat /sys/block/loop${p#*$PUP_RO}/loop/backing_file 2> /dev/null)" in
			*"$1"*) branch=$p; break ;;
		esac
	done
}

# $1 = full path to sfs to load, $2 = system load (parameter for autorun-script)
sfs_load() {
	! [ -e "$1" ] && message "ERROR" "Can't find '$1' - can't load." && return 1

	get_branch "$1"
	if [ -z "$branch" ]; then # not loaded yet, so do it
		# find free loop device, re-using what we can, 
		# if we can't, then make new loop/pup_ro mountpoint
		# keep loop-N and pup_ro-N sync at all times	
		find_free_loop
		make_loop_device $FREELOOP
		make_mount_point $FREELOOP
		
		# now ready to load
		if losetup /dev/loop$FREELOOP "$1"; then
			if mount -o ro /dev/loop$FREELOOP $AUFS_ROOT/$PUP_RO$FREELOOP; then
				if busybox mount -i -t aufs -o remount,ins:$INSERT_POINT:$AUFS_ROOT/$PUP_RO$FREELOOP=rr aufs /; then
					# run the run-script if it exists & allowed
					[ -z "$DISABLE_AUTORUN" -a -x $AUFS_ROOT/$PUP_RO$FREELOOP/$SFS_RUN_SCRIPT ] && 
					$AUFS_ROOT/$PUP_RO$FREELOOP/$SFS_RUN_SCRIPT ${2}load "$AUFS_ROOT/$PUP_RO$FREELOOP" # run load script
					[ -e $AUFS_ROOT/$PUP_RO$FREELOOP/$SFS_MSG ] && 
					message "Load SFS" "$1 loaded successfully.\n$(cat $AUFS_ROOT/$PUP_RO$FREELOOP/$SFS_MSG)" ||
					message "Load SFS" "$1 loaded successfully."
					post_load_hook "$1"
				else
					message "ERROR" "Failed to remount aufs $1" 
					umount -d $AUFS_ROOT/$PUP_RO$FREELOOP
				fi
			else
				message "ERROR" "Failed to mount branch for $1" 
				losetup -d /dev/loop$FREELOOP
			fi
		else
			message "ERROR" "Failed to assign loop devices loop$FREELOOP for $1" 
		fi
	fi
}

# $1 = full-path sfs to unload, $2=system unload (parameter for autorun-script)
sfs_unload() {
	local p branch ok

	# find the branch where the sfs is
	get_branch "$1"
	if [ "$branch" ]; then
		ok=
		if ! busybox mount -i -t aufs -o remount,del:"$branch" unionfs /; then
			# failed to load, if allowed try running script to recover, then try again
			[ -z "$DISABLE_AUTORUN" -a -x $branch/$SFS_RUN_SCRIPT ] &&
			$branch/$SFS_RUN_SCRIPT ${2}unload-error "$branch" # run unload script
			busybox mount -i -t aufs -o remount,del:"$branch" unionfs / && ok=yes
		else
			ok=yes
		fi

		if [ $ok ]; then
			# if allowed, run the run-script after unload but before unmounting
			[ -z "$DISABLE_AUTORUN" -a -x $branch/$SFS_RUN_SCRIPT ] &&
			$branch/$SFS_RUN_SCRIPT ${2}unload "$branch" # run unload script
			
			umount -d "$branch" &&
			message "Unload SFS" "$1 unloaded successfully."
			post_unload_hook "$1"
		else
			message "ERROR" "Unable to unload $1"
		fi		
	fi
}

############ auto-load file management  #############
# $1 = path to add
add_to_list() {
	{ grep -Fv "$1" $LISTFILE; echo "$1"; } > $LISTFILE.tmp
	mv $LISTFILE.tmp $LISTFILE
}

# $1 = filename to remove
remove_from_list() {
	grep -Fv "$1" $LISTFILE > $LISTFILE.tmp 2>/dev/null
	mv $LISTFILE.tmp $LISTFILE
}

# get all sfs from the list
get_list() {
	cat $LISTFILE 2>/dev/null
}

############ GUI related stuff and helpers ############

# list in the layer order
list_loaded_sfs() {
	# all aufs branches, in reverse layer order (lowest first)
	local branches
	branches=""
	for p in $(seq $MAXLOOP -1 0); do
		[ -e /sys/fs/aufs/$AUFS_ROOT_ID/br$p ] && 
		branches="$branches /sys/fs/aufs/$AUFS_ROOT_ID/br$p"
	done
	sed -n "/$PUP_RO/{s|=.*||;s|.*$PUP_RO||;p}" $branches	|
	
	# get the sfs that correspond to these aufs branches
	while read -r p; do
		if [ "$p" ] && [ "$p" -ge $RESERVED ]; then
			cat /sys/block/loop$p/loop/backing_file
		fi		
	done
}

# $1-dir don't display already loaded sfs
list_available_sfs() {
	local loaded="$(list_loaded_sfs | tr '\n' '|')"
	local dir="$(readlink -f "$1")"
	for p in "$dir"/*.sfs; do
		case "$p" in *\*.sfs) return 1;; esac # nothing
		case "$loaded" in
			*"$p|"*) continue ;; # loaded, don't show
			*) echo "$p" ;;
		esac
	done
	return 0
}

# interactive load/unload sfs
interactive() {
	local items tmpfile tmpfile2 tmpfile3
	
	tmpfile=/tmp/load_sfs.$$ tmpfile2=/tmp/load_sfs.2.$$ tmpfile3=/tmp/load_sfs.3.$$
	while true; do # gui loop
		items=""
		while read -r p; do
			items="$items'$p' '$p' on "
		done << EOF
$(list_loaded_sfs)
EOF
		while read -r p; do
			items="$items'$p' '$p' off "
		done << EOF
$(list_available_sfs "$SFS_DIR")
EOF
		eval Xdialog $XDIALOG_STD_OPTIONS --cancel-label "Exit" --ok-label "Apply" \
		--check \'"To change SFS directory, tick the box and click Apply."\' \
		--buildlist \'"Current SFS directory:\n$SFS_DIR"\' \
		30 140 10 $items > $tmpfile || break
		
		if ! grep -q unchecked $tmpfile; then
			# change dir
			if p=$(eval Xdialog $XDIALOG_STD_OPTIONS --no-buttons --dselect \'"$SFS_DIR"\' 0 0); then
				SFS_DIR="${p%/}"
				#echo do it $SFS_DIR
			fi
		else
			# process
			# 1. find which sfs we need to load/unload
			sed -i -e '/unchecked/ d' $tmpfile
			list_loaded_sfs > $tmpfile2
			diff -u $tmpfile2 $tmpfile > $tmpfile3

			# 2. load/unload them
			while read -r p; do
				case "$p" in
					---*|+++*) ;; # diff header, ignore
					+*) sfs_load   "${p#+}" ;;
					-*) sfs_unload "${p#-}" ;;
				esac
			done < $tmpfile3
			
			# 3. ask whether we want to keep the setting permanent
			[ -s $tmpfile3 ] && if eval Xdialog $XDIALOG_STD_OPTIONS --default-no \
			--yesno \'"Do you want to keep this configuration\nfor the next boot?"\' 0 0; then
				mv -f $tmpfile $LISTFILE
			fi
		fi
	done
	rm -f $tmpfile $tmpfile2 $tmpfile3
	return 0
}


############## main ##############
if [ -z "$1" ]; then
	# no parameter passed - use interactive gui
	[ "$DISPLAY" ] && interactive || $0 --help
else
	case "$1" in
		start)
			#echo start service - loadall
			for a in $(get_list); do
				sfs_load "$a" system
			done 
			exit 0 ;;
		stop)
			#echo stop service - unloadall
			for a in $(get_list); do
				sfs_unload "${a##*/}" system
			done
			exit 0 ;;
		--loaded)
			list_loaded_sfs 
			exit 0 ;;
		--help|-h|help)
			cat << EOF
Usage: 
 ${0##*/} [start|stop|--loaded|help|--help|-h]
 ${0##*/} [--load|--unload|--add|--remove /path/to/sfsfile]

Without parameters, ${0##*/} will run the interactive GUI.

"start" will load all the sfs found in the auto-load file.
"stop" will unload all the sfs found in the auto-load file.
They are useful when ${0##*/} is symlink-ed to /etc/init.d as a service.
The auto-load file is located at $LISTFILE.

"--loaded" : all currently loaded sfs-es.
"--load"   : load the given sfs
"--unload" : unload the given sfs
"--add"    : add the given sfs to the auto-load list
"--remove" : remove the sfs from the auto-load list
EOF
		exit 0 ;;
	esac
	[ -z "$2" ] && exec $0 --help
	
	sfs=$(readlink -f "$2") # always canonicalise	
	case "$1" in
		--load)
			sfs_load "$sfs" ;;
		--unload)
			sfs_unload "$sfs" ;;
		--add)
			add_to_list "$sfs"; message "Auto-load" "$sfs" added. ;;
		--remove)
			remove_from_list "$sfs"; message "Auto-load" "$sfs" removed ;;
		*) exec $0 --help ;;
	esac
fi
