#!/bin/ash
# James Budiono 2011-2020
# 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
# version 10: refuse to load SFS if dirmode is 700
# version 11: (step) comment lines begin with '#'; line order is preserved.
# version 12: better cmdline interface
# version 13: better load/unload return value
# version 14: (Jake) save SFS_DIR and escape path
#
# 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
CONFFILE=/etc/load_sfs.conf
XDIALOG_STD_OPTIONS="--separate-output --stdout --title '$APP_TITLE'"
QUIET=${QUIET}

### 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 "$CONFFILE" ] && . "$CONFFILE"
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" ] &&
	test "$(echo /tmp/rebuild-lxqt-panel-menu*)" != "/tmp/rebuild-lxqt-panel-menu*" &&
	touch /tmp/rebuild-lxqt-panel-menu*
	return 0
}

# $@ text to display
message() {
	[ $QUIET ] && return
	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() {
	local dirmode
	[ -z "$1" ] && return 0 # ignore empty path
	! [ -e "$1" ] && message "ERROR" "Can't find '$1' - can't load." && return 1

	get_branch "$1"
	if [ "$branch" ]; then
		return 0 # acknowledge it's loaded
	else # 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 -r /dev/loop$FREELOOP "$1"; then
			if mount -o ro /dev/loop$FREELOOP $AUFS_ROOT/$PUP_RO$FREELOOP; then
				dirmode=$(stat -c%a $AUFS_ROOT/$PUP_RO$FREELOOP)
				case $dirmode in
					*700)
						message "ERROR" "$1 bad dirmode ($dirmode)" 
						umount -d $AUFS_ROOT/$PUP_RO$FREELOOP
						;;
					*)
						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"
							return 0
						else
							message "ERROR" "Failed to remount aufs $1" 
							umount -d $AUFS_ROOT/$PUP_RO$FREELOOP
						fi
						;;
				esac
			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
	return 1
}

# $1 = full-path sfs to unload, $2=system unload (parameter for autorun-script)
sfs_unload() {
	local p branch ok
	[ -z "$1" ] && return 0 # ignore empty path

	# 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"
			return 1
		fi		
	fi
}

############ auto-load file management  #############
# $1 = path to add
# If it's already in the list it isn't added again.
add_to_list() {
	awk -v Z="$1" '
BEGIN { found = 0 }
{
	if($0 == Z) found = 1
	else a[++n] = $0
}
END {
	if(!found) {
		a[++n] = Z
		for(i = 1; i <= n; i++) {
			print a[i] > FILENAME
		}
		close(FILENAME)
	}
}
' "$LISTFILE"
}

# $1 = filename to remove
# Keep non-matching lines. That includes empty lines and '#' comment lines.
remove_from_list() {
	awk -v Z="$1" '
BEGIN { found = 0 }
{
	if($0 == Z) found = 1
	else a[++n] = $0
}
END {
	if(found) {
		for(i = 1; i <= n; i++) {
			print a[i] > FILENAME
		}
		close(FILENAME)
	}
}
' "$LISTFILE"
}

# get all sfs from the list
# Filter out empty lines and comment lines, leave the rest.
get_list() {
	[ -s "$LISTFILE" ] &&
	awk '$0 !~ /^[ \t]*#/ && $0 !~ /^[ \t]*$/' "$LISTFILE"
}

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

# $1-string. escape single quotes
escape() {
	echo "$1" | sed "s/'/'\"'\"'/g"
}

# 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
}

# $1 - path. Save SFS dir location in config file
savesfsdir() {
	local sfsdest="$1" _

	# remove exising variable with location
	if [ -e "$CONFFILE" ]; then
		sed -i "/SFS_DIR=.*/d" "$CONFFILE"
	fi

	# make sure there's a newline at the end
	if [ -s "$CONFFILE" ]; then
		tail -n1 "$CONFFILE" | read -r _ || echo >> "$CONFFILE"
	fi

	sfsdest="$(escape "$sfsdest")"
	echo "SFS_DIR='$sfsdest'" >> "$CONFFILE"
}

# interactive load/unload sfs
interactive() {
	local p 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
			p="$(escape "$p")"
			items="$items'$p' '$p' on "
		done << EOF
$(list_loaded_sfs)
EOF
		while read -r p; do
			p="$(escape "$p")"
			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$(escape "$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 \
				--check \'"Set as default SFS directory"\' off \
				--dselect \'"$(escape "$SFS_DIR")"\' 0 0)
			then
				oldIFS="$IFS"; IFS='
'
				set -- $p
				IFS="$oldIFS"
				SFS_DIR="${1%/}"
				[ "$2" = 'checked' ] && savesfsdir "$SFS_DIR"
				#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 ##############
[ "$NOT_USING_AUFS" ] && exit 1
if [ -z "$1" ]; then
	# no parameter passed - use interactive gui
	[ "$DISPLAY" ] && interactive || $0 --help
else
	while [ "$1" ]; do
		case "$1" in
			--quiet|-q)
				QUIET=yes
				;;

			start)
				#echo start service - loadall
				get_list | while read -r p; do
					sfs_load "$p" system
				done
				exit 0
				;;

			stop)
				#echo stop service - unloadall
				get_list | while read -r p; do
					sfs_unload "${p##*/}" system
				done
				exit 0
				;;

			--loaded|--list)
				list_loaded_sfs
				exit 0
				;;

			--help|-help|-h|help)
				message "" "\
Usage:
 ${0##*/} [--quiet|-q] [start|stop|--loaded|help|--help|-h]
 ${0##*/} [--quiet|-q] [--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 or --list : list 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
--quiet  : don't output any message
"
			exit 0
			;;

			*)  ## commands that requires parameters
				cmd=$1; shift; parms="" RETCODE=0
				[ -z "$1" ] && exec $0 --help
				while [ "$1" ]; do
					sfs=$(readlink -f "$1")
					case $cmd 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
					RETCODE=$(($? | $RETCODE))  # capture status of last command executed
					shift
				done
				exit $RETCODE # return error if any command failed
				;;
		esac
		shift
	done
fi
