#!/bin/sh

# lbu - utility to create local backups.
# Copyright (c) 2006 Natanael Copa
# May be distributed under GPL2

VERSION=2.0_rc6-1-g5572e8a
sysconfdir=/etc/lbu

if [ ! -f ${libalpine:="./libalpine.sh"} ]; then
	libalpine=/usr/share/lbu/libalpine.sh
	if [ ! -f "$libalpine" ]; then
		libalpine=/lib/libalpine.sh
	fi
fi
. $libalpine || exit 1

EXCLUDE_LIST="$sysconfdir"/exclude
INCLUDE_LIST="$sysconfdir"/include

DEFAULT_CIPHER="aes-256-cbc"

LBU_CONF="$sysconfdir"/lbu.conf
if [ -f "$LBU_CONF" ]; then
	. "$LBU_CONF"
fi

UMOUNT_LIST=

usage() {
	echo "$PROGRAM $VERSION"
	echo "usage: $PROGRAM <subcommand> [options] [args]

Available subcommands:
  commit (ci)
  exclude (ex, delete)
  include (inc, add)
  list (ls)
  package (pkg)
  status (stat, st)
  list-backup (lb) 
  revert

Common options:
 -h	Show help for subcommand.
 -q	Quiet mode.
 -v	Verbose mode.
"
	exit 1
}

cleanup() {
	local i
	for i in $UMOUNT_LIST; do
		umount $i
	done
}

exit_clean() {
	cleanup
	exit 1
}

mount_once() {
	if ! grep $1 /proc/mounts >/dev/null; then
		mount $1 && UMOUNT_LIST="$1 $UMOUNT_LIST"
	fi
}

# create backupfile
backup_apkovl() {
	local outfile="$1"
	local d=$( date -u -r "$outfile" "+%Y%m%d%H%M%S" )
	local backup=$(echo "$outfile" | sed "s/\.apkovl\.tar\.gz/.$d.tar.gz/")
	vecho "Creating backup $backup"
	if [ -z "$DRYRUN" ]; then
		mv "$outfile" "$backup"
		APKOVL_BACKUP="$backup"
	fi
}

restore_apkovl() {
	local outfile="$1"
	if [ -n "$DRYRUN" ] || [ -z "$APKOVL_BACKUP" ]; then
		return 0
	fi
	mv "$APKOVL_BACKUP" "$outfile"
}

# verify we have openssl if we want to encrypt
check_openssl() {
	[ -z "$ENCRYPTION" ] && return 0
	OPENSSL=$(which openssl 2>/dev/null) || die "openssl was not found"

	$OPENSSL list-cipher-commands | grep "^$ENCRYPTION$" > /dev/null \
		|| die "Cipher $ENCRYPTION is not supported"
}

# list_add(char *listfile, char* file...)
list_add() {
	local list="$1"
	shift
	mkdir -p `dirname "$list"`
	while [ $# -gt 0 ] ; do
		filename=`echo "$1" | sed 's:^/\+::'`
		if grep "^$filename$" "$list" >/dev/null 2>&1 ; then
			vecho "$filename is already in $list."
		else
			vecho "Adding $filename to $list."
			echo "$filename" >> "$list"
		fi
		shift
	done
}

# list_delete(char *listfile, char *file...)
list_delete() {
	local list="$1"
	local tmp="$list.old"
	shift
	[ -f "$list" ] || return 1
	while [ $# -gt 0 ] ; do
		filename=`echo "$1" | sed 's:^/\+::'`
		mv "$list" "$tmp"
		vecho "Removing $filename from list."
		grep -v "^$filename$" "$tmp" > "$list"
		rm "$tmp"
		shift
	done
}

# unpack archive on LBU_MEDIA to given dir
unpack_apkovl() {
	local f="$(hostname).apkovl.tar.gz"
	local dest="$1"
	local mnt="/media/$LBU_MEDIA"
	local count=0
	mkdir -p "$dest"
	mount_once "$mnt"
	if [ ! -f "$mnt/$f" ]; then
		return 1
	fi
	if [ -z "$ENCRYPTION" ]; then
		tar -C "$dest" -zxf "$mnt/$f"
		return
	fi
	f="$f.$ENCRYPTION"
	check_openssl
        while [ $count -lt 3 ]; do
		$OPENSSL enc -d -$ENCRYPTION -in "$mnt/$f" | tar \
			-C "$dest" -zx 2>/dev/null && return 0
		count=$(( $count + 1 ))
	done
	cleanup
	die "Failed to unpack $mnt/$f"
}		

#
# lbu_include - add/remove files to include list
#
usage_include() {
	echo "$PROGRAM $VERSION
Add filename(s) to include list ($sysconfdir/include)

usage: $PROGRAM include|inc|add [-rv] <file> ...
       $PROGRAM include|inc|add [-v] -l

Options:
  -l	List contents of include list.
  -r	Remove specified file(s) from include list instead of adding.
  -v	Verbose mode.
"
	exit 1
}

cmd_include() {
	if [ "$LIST" ] ; then
		[ $# -gt 0 ] && usage_include
		show_include
		return
	fi

	[ $# -lt 1 ] && usage_include
	if [ "$REMOVE" ] ; then
		list_delete "$INCLUDE_LIST" "$@"
	else
		list_add "$INCLUDE_LIST" "$@"
		list_delete "$EXCLUDE_LIST" "$@"
	fi
}

show_include() {
	if [ -f "$INCLUDE_LIST" ] ; then
		vecho "Include files:"
		cat "$INCLUDE_LIST"
	fi
}

#
# lbu_package - create a package
#
usage_package() {
	echo "$PROGRAM $VERSION
Create backup package.

usage: $PROGRAM package|pkg -v [<dirname>|<filename>]

Options:
  -v	Verbose mode.

If <dirname> is a directory, a package named <hostname>.apkovl.tar.gz will
be created in the specified directory.

If <filename> is specified, and is not a direcotry, a package with the
specified name willbe created.

If <dirname> nor <filename> is not specified, a package named
<hostname>.apkovl.tar.gz will be created in current work directory.
"
	exit 1
}

cmd_package() {
	local pkg="$1"
	local rc=0
	local owd="$PWD"
	local suff="apkovl.tar.gz"
	local tmpdir tmppkg

	check_openssl
	init_tmpdir tmpdir

	[ -n "$ENCRYPTION" ] && suff="$suff.$ENCRYPTION"

	# find filename
	if [ -d "$pkg" ] ; then
		pkg="$pkg/$(hostname).$suff"
	elif [ -z "$pkg" ]; then
		pkg="$PWD/$(hostname).$suff"
	fi

	tmppkg="$tmpdir/$(basename $pkg)"

	cd "${ROOT:-/}"
	# remove old package.list
	if [ -f etc/lbu/packages.list ] && [ -f var/lib/apk/world ]; then
		echo "Note: Removing /etc/lbu/packages.list."
		echo "      /var/lib/apk/world will be used."
		rm -f etc/lbu/packages.list
	fi
	currentlist=$(apk audit --backup -q)
	if [ -f var/lib/apk/world ]; then
		currentlist="$currentlist var/lib/apk/world"
	fi

	# create tar archive
	[ -f "$EXCLUDE_LIST" ] && excl="-X $EXCLUDE_LIST"
	[ -f "$INCLUDE_LIST" ] && incl="-T $INCLUDE_LIST"
	if [ -n "$VERBOSE" ]; then
		echo "Archiving the following files:" >&2
		# we dont want to mess the tar output with the
		# password prompt. Lets get the tar output first.
		tar  $excl $incl -c -v $currentlist > /dev/null
		rc=$?
	fi
	if [ $rc -eq 0 ]; then
		if [ -z "$ENCRYPTION" ]; then
			tar $excl $incl -c $currentlist | gzip -c  >"$tmppkg"
			rc=$?
		else
			set -- enc "-$ENCRYPTION" -salt
			[ -n "$PASSWORD" ] && set -- "$@" -pass pass:"$PASSWORD"
			tar $excl $incl -c $currentlist | gzip -c \
				| $OPENSSL "$@" > "$tmppkg"
			rc=$?
		fi
	fi
	cd "$owd"

	# actually commit unless dryrun mode
	if [ $rc -eq 0 ]; then
		if [ -z "$DRYRUN" ]; then
			if [ "x$pkg" = "x-" ]; then
				cat "$tmppkg"
			else
				if cp "$tmppkg" "$pkg.new"; then
					mv "$pkg.new" "$pkg"
					rc=$?
				else
					rm -f "$pkg.new"
					rc=1
				fi
			fi
		fi
		[ $rc -eq 0 ] && vecho "Created $pkg"
	fi
	return $rc
}

#
# lbu list - list files that would go to archive
#
usage_list() {
	echo "$PROGRAM $VERSION
Lists files that would go to tar package. Same as: 'lbu package -v /dev/null'

usage: $PROGRAM list|ls
"
	exit 1
}

cmd_list() {
	VERBOSE="-v"
	cmd_package /dev/null
}

#
# lbu_commit - commit config files to writeable media
#
usage_commit() {
	echo "$PROGRAM $VERSION
Create a backup of config to writeable media.

usage: $PROGRAM commit|ci [-nv] [<media>]

Options:
  -d	Remove old apk overlay files.
  -e	Protect configuration with a password.
  -n	Don't commit, just show what would have been commited.
  -p <password>	Give encryption password on the command-line
  -v	Verbose mode.

The following values for <media> is supported: floppy usb
If <media> is not specified, the environment variable LBU_MEDIA will be used.

Password protection will use $DEFAULT_CIPHER encryption. Other ciphers can be
used by setting the DEFAULT_CIPHER or ENCRYPTION environment variables.
For possible ciphers, try: openssl -v

The password used to encrypt the file, can either be specified with the -p
option or using the PASSWORD environment variable.

The environment varialbes can also be set in $LBU_CONF
"
	exit 1
}

cmd_commit() {
	local media mnt statuslist tmplist currentlist
	local incl excl outfile ovls lines

	check_openssl

	# turn on verbose mode if dryrun
	[ -n "$DRYRUN" ] && VERBOSE="-v"

	# find what media to use
	media="${1:-$LBU_MEDIA}"
	[ -z "$media" ] && usage_commit

	# mount media unles its already mounted
	mnt=/media/$media
	[ -d "$mnt" ] || usage
	mount_once "$mnt" || die "failed to mount $mnt"

	# find the outfile
	outfile="$mnt/$(hostname).apkovl.tar.gz"
	if [ -n "$ENCRYPTION" ]; then
		outfile="$outfile.$ENCRYPTION"
	fi

	# remove old config files
	if [ -n "$DELETEOLDCONFIGS" ] ; then
		local rmfiles=$(ls "$mnt/"*.apkovl.tar.gz* 2>/dev/null)
		if [ -n "$rmfiles" ] ; then
			if [ -n "$VERBOSE" ]; then
				echo "Removing old apk overlay files:" >&2
				echo "$rmfiles"
				echo "" >&2
			fi
			[ -z "$DRYRUN" ] && rm "$mnt/"*.apkovl.tar.gz*
		fi
	else
       		lines=$(ls -1 "$mnt"/*.apkovl.tar.gz* 2>/dev/null)
		if [ "$lines" = "$outfile" ]; then
			backup_apkovl "$outfile"
		elif [ -n "$lines" ]; then
	               	# More then one apkovl, this is a security concern
			cleanup
			eecho "The following apkovl file(s) were found:"
			eecho "$lines"
			eecho ""
	               	die "Please use -d to replace."
		fi
	fi

	# create package
	if ! cmd_package "$outfile"; then
		restore_apkovl "$outfile"
		cleanup
		die "Problems creating archive. aborting"
	fi

	# delete old backups if needed
	# poor mans 'head -n -N' done with awk.
	ls "$mnt"/$(hostname).[0-9][0-9][0-9][0-9]*[0-9].tar.gz 2>/dev/null \
		| awk '{ a[++i] = $0; } END { 
			print a[0]; 
			while (i-- > '"${BACKUP_LIMIT:-0}"') { 
				print a[++j] 
			}
		}' | xargs rm 2>/dev/null

	# remove obsolete file. some older version of alpine needs this
	# to be able to upgrade
	if [ -z "$DRYRUN" ] && [ -f $mnt/packages.list ]; then
		echo "Note: Removing packages.list from $(basename $mnt)."
		echo "      /var/lib/apk/world will be used."
		rm -f $mnt/packages.list
	fi

	# make sure data is written
	sync
	[ "$media" = "floppy" ] && sleep 1

	# move current to commited.
	vecho "Successfully saved apk overlay files"
}

#---------------------------------------------------------------------------
# lbu_exclude - add remove file(s) from exclude list

usage_exclude() {
	echo "$PROGRAM $VERSION
Add filename(s) to exclude list ($sysconfdir/exclude)

usage: $PROGRAM exclude|ex|delete [-rv] <file> ...
       $PROGRAM exclude|ex|delete [-v] -l

Options:
  -l	List contents of exclude list.
  -r	Remove specified file(s) from exclude list instead of adding.
  -v	Verbose mode.
"
	exit 1
}

cmd_exclude() {
	if [ "$LIST" ] ; then
		[ $# -gt 0 ] && usage_exclude
		show_exclude
		return
	fi

	[ $# -lt 1 ] && usage_exclude
	if [ "$REMOVE" ] ; then
		list_delete "$EXCLUDE_LIST" "$@"
	else
		list_delete "$INCLUDE_LIST" "$@"
		list_add "$EXCLUDE_LIST" "$@"
	fi
}

show_exclude() {
	if [ -f "$EXCLUDE_LIST" ] ; then
		vecho "Exclude files:"
		cat "$EXCLUDE_LIST"
	fi
}

#---------------------------------------------------------------------------
# lbu_listbackup - Show old commits
usage_listbackup() {
	cat <<EOF
$PROGRAM $VERSION
Show old commits.

usage: $PROGRAM list-backup [<media>]

EOF
	exit 1
}

cmd_listbackup() {
	local media=${1:-"$LBU_MEDIA"}
	local mnt="/media/$media"
	[ -z "$media" ] && usage_listbackup

	mount_once "$mnt" || die "failed to mount $mnt"
	ls -1 "$mnt"/*.[0-9][0-9]*[0-9][0-9].tar.gz* 2>/dev/null | sed 's:.*/::'
}

#---------------------------------------------------------------------------
# lbu_revert - revert to old config
usage_revert() {
	cat <<EOF
$PROGRAM $VERSION
Revert to older commit.

usage: $PROGRAM revert <REVISION> [<media>]

The revision should be one of the files listed by 'lbu list-backup'.

EOF
}

cmd_revert() {
	local media=${2:-"$LBU_MEDIA"}
	[ -z "$media" ] && usage_revert
	local mnt="/media/$media"
	local revertto="$mnt/$1"
	local current="$mnt/$(hostname).apkovl.tar.gz"

	if [ -n "$ENCRYPTION" ]; then
		current="$current.$ENCRYPTION"
	fi
	mount_once "$mnt" || die "failed to mount $mnt"
	[ -f "$revertto" ] || die "file not found: $revertto"
	backup_apkovl "$current"
	vecho "Reverting to $1"
	[ -z "$DRYRUN" ] && mv "$revertto" "$current"
}

#---------------------------------------------------------------------------
# lbu_status - check what files have been changed since last save
usage_status() {
	echo "$PROGRAM $VERSION
Check what files have been changed since last commit.

usage: $PROGRAM status|st [-av]

Options:
  -a	Compare all files, not just since last commit.
  -v	Also show include and exclude lists.
"
	exit 1
}

cmd_status() {
	if [ -n "$USE_DEFAULT" ]; then
		apk audit --backup
		return 0
	fi
	LBU_MEDIA=${1:-"$LBU_MEDIA"}
	[ -z "$LBU_MEDIA" ] && usage_status
	local tmp
	init_tmpdir tmp
	mkdir -p "$tmpdir/a" "$tmp/b"

	# unpack last commited apkovl to tmpdir/a
	unpack_apkovl "$tmp/a"

	# generate new apkovl and extract to tmpdir/b
	cmd_package - | tar -C "$tmp/b" -zx

	# show files that exists in a but not in b as deleted
	local f
	( cd "$tmp"/a && find ) | while read f; do
		f=${f#./}
		[ "$f" = "." ] && continue
		[ -e "$tmp/b/$f" ] || echo "D $f"
	done
	
	# compare files in b with files in a
	( cd "$tmp"/b && find ) | while read f; do
		f=${f#./}
		[ "$f" = "." ] && continue
		local a="$tmp/a/$f"
		local b="$tmp/b/$f"
		if [ ! -e "$a" ]; then
			echo "A $f"
		elif [ -f "$a" ] && [ -f "$b" ] && [ "$b" -nt "$a" ] \
		     && ! cmp -s "$a" "$b"; then
			echo "U $f"
		fi
	done
}


#-----------------------------------------------------------
# lbu_diff - run a diff against last commit
usage_diff() {
	echo "$PROGRAM $VERSION
Run a diff against last commit

usage: $PROGRAM diff [<media>]
"
	exit 1
}

cmd_diff() {
	LBU_MEDIA=${1:-"$LBU_MEDIA"}
	[ -z "$LBU_MEDIA" ] && usage_diff
	local tmp
	init_tmpdir tmp
	mkdir -p "$tmpdir/a" "$tmp/b"
	unpack_apkovl "$tmp/a"
	cmd_package - | tar -C "$tmp/b" -zx
	cd "$tmp" && diff -ruN a b 
}
	


#-----------------------------------------------------------
# Main

cmd=`echo "$PROGRAM" | cut -s -d_ -f2`
PROGRAM=`echo "$PROGRAM" | cut -d_ -f1`
if [ -z "$cmd" ] ; then
	cmd="$1"
	[ -z "$cmd" ] && usage
	shift
fi

# check for valid sub command
case "$cmd" in
	include|inc|add)	SUBCMD="include";;
	commit|ci)		SUBCMD="commit";;
	exclude|ex|delete)	SUBCMD="exclude";;
	list|ls)		SUBCMD="list";;
	package|pkg)		SUBCMD="package";;
	status|stat|st)		SUBCMD="status";;
	list-backup|lb)		SUBCMD="listbackup";;
	revert)			SUBCMD="revert";;
	diff)			SUBCMD="diff";;
	*)			usage;;
esac

# parse common args
while getopts "adehlM:np:qrv" opt ; do
	case "$opt" in
		a) 	[ $SUBCMD = status ] || usage_$SUBCMD
			USE_DEFAULT="-a"
			;;
		d)	DELETEOLDCONFIGS="yes"
			;;
		e)	[ -z "$ENCRYPTION" ] && ENCRYPTION="$DEFAULT_CIPHER"
			;;
		h) 	usage_$SUBCMD
			;;
		l)	LIST="-l"
			;;
		n) 	[ $SUBCMD = commit ] || usage_$SUBCMD
			DRYRUN="-n"
			;;
		p)	PASSWORD="$OPTARG"
			;;
		q)	QUIET="$QUIET -q"
			;;
		r)	REMOVE="-r"
			;;
		v) 	VERBOSE="$VERBOSE -v"
			;;
	esac
done
shift `expr $OPTIND - 1`

trap exit_clean SIGINT SIGTERM
cmd_$SUBCMD "$@"
retcode=$?

cleanup
exit $retcode
