#!/bin/bash
# Copyright (C) 2015-2018  Etersoft
# Copyright (C) 2015-2018  Vitaly Lipatov <lav@etersoft.ru>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

export LANG=C

PROGDIR=$(dirname $0)
[ "$PROGDIR" = "." ] && PROGDIR=$(pwd)

VERSION="0.8"
NEEDZPAQ="7.15"
BASEPACK=root
# templates for multi part archive names
STZ="000000"
ST1="000001"
STN="??????"

DESCR="eterpack version $VERSION (c) Etersoft 2015-2018"

# realpath workaround
if ! which realpath 2>/dev/null >/dev/null ; then
realpath()
{
	readlink -f "$@"
}
fi

print_message()
{
	local DESC="$1"
	shift
	echo "$DESC in $(basename $0): $@"
}

# Print error message and stop the program
fatal()
{
	print_message Error "$@" >&2
	exit 1
}

info()
{
	echo "$*"
}

fatal_status()
{
	if [ -s "$DUMPDIR/.status" ] ; then
		echo "$1" > "$DUMPDIR/.status"
	fi
	if [ -n "$2" ] ; then
		echo "$2" > "$DUMPDIR/.status_ext"
	fi
	fatal "Exited with status $1. $2"
}

done_status()
{
	rm -f "$DUMPDIR/.status"
	rm -f "$DUMPDIR/.status_ext"
}


# CHECKME: the same like estrlist has ?
# Note: used egrep! write '[0-9]+(first|two)', not '[0-9]\+...'
rhas()
{
	echo "$1" | grep -E -q -- "$2"
}

# //path//path1 -> path/path1
clean_path()
{
	echo "$1" | sed -e \
		"s|^//||g
		 s|^/||g
		 s|///|/|g
		 s|//|/|g"
}

list_dirs()
{
local reldir
#echo "List dirs from $BACKUPDIR, with $EXCLUDEDIR exclude."
# Получаем список каталогов, которые мы хотим превратить в файлы
find $FOLLOWLINKS $BACKUPDIR -depth -maxdepth $MAXDEPTH -mindepth $MINDEPTH -type d -printf "%P\n" | \
while read reldir ; do

	bdir=$(basename "$reldir")

	if rhas "$EXCLUDEDIR" "^/" ; then
		if [ "$reldir" = "$(clean_path "$EXCLUDEDIR")" ] ; then
			continue
		fi
	else
		if  [ "$bdir" = "$EXCLUDEDIR" ] ; then
			continue
		fi
	fi

	echo "$reldir"
done
}

list_rootfiles()
{
	# TODO: exclude? or it is in zpaq?
	# TODO: not dirs from list_dirs
	# Список файлов:
	find $BACKUPDIR -depth -maxdepth $MAXDEPTH -mindepth 1 -type f -printf "%P\n"
	# Список каталогов, чтобы запаковать пустые каталоги
	# тут проблема: там могут быть ненужные вложенные каталоги
	#MINDEPTH=1 MAXDEPTH=$MAXDEPTH list_dirs
}

attrstore()
{
	$PROGDIR/eterattrstore "$@"
}

# direct run for single zpaq archive
update_archive()
{
	bdir="$(basename "$BACKUPDIR")"

	if [ -z "$NOATTRIBUTE" ] ; then
		echo
		echo "Save attrs and special files..."
		cd "$BACKUPDIR" || exit
		attrstore save
		cd - >/dev/null
	fi

	local EXCLUDEARG=
	[ -n "$EXCLUDEDIR" ] && EXCLUDEARG="-not $bdir/$(clean_path "$EXCLUDEDIR")"

	cd "$BACKUPDIR/.." || fatal
	echo
	echo "Packing single $BACKUPDIR ..."
	mkdir -p "$DUMPDIR/"
	echo zpaq add "$DUMPDIR/$BASEPACK.$STN.zpaq" "$bdir" -to . $CHECKSUM $EXCLUDEARG -index "$DUMPDIR/$BASEPACK.$STZ.zpaq"
	zpaq add "$DUMPDIR/$BASEPACK.$STN.zpaq" "$bdir" -to . $CHECKSUM $EXCLUDEARG $PASSWORDARG -index "$DUMPDIR/$BASEPACK.$STZ.zpaq" || { cd - ; exit 1 ; }
	cd - >/dev/null

	cd "$BACKUPDIR" || fatal
	attrstore clear
	cd - >/dev/null

	if [ -n "$EXECUTEAFTER" ] ; then
		$EXECUTEAFTER "$DUMPDIR/$BASEPACK.$STN.zpaq" || fatal "Executed command $EXECUTEAFTER failed"
	fi

}

update_dump()
{
local reldir
# Получаем список каталогов, которые мы хотим превратить в файлы
find $FOLLOWLINKS $BACKUPDIR -depth -maxdepth $MAXDEPTH -mindepth $MINDEPTH -type d -printf "%P\n" | \
while read reldir ; do

	bdir="$(basename "$reldir")"

	local EXCLUDEARG=
	if [ -n "$EXCLUDEDIR" ] ; then
		if rhas "$EXCLUDEDIR" "^/" ; then
			if [ "$reldir" = "$(clean_path "$EXCLUDEDIR")" ] ; then
				echo "Skip excluded $reldir"
				continue
			fi
			if rhas "$reldir" "^$(clean_path "$EXCLUDEDIR")/" ; then
				echo "Skip excluded $reldir (part)"
				continue
			fi
			if rhas "$EXCLUDEDIR" "^/$reldir/" ; then
				EXCLUDEARG="-not $bdir/$(clean_path "$(echo "$EXCLUDEDIR" | sed -e "s|^/$reldir||g")")"
				echo "Use exclude rule: $EXCLUDEARG"
			fi
		else
			if [ "$bdir" = "$EXCLUDEDIR" ] ; then
				echo "Skip excluded $bdir"
				continue
			fi
		fi
	fi

	# TODO: удалённые каталоги будут незамечены и останутся.
	# Создавать новый слой, копируя со старого, и оставляя тот старыми датами?

	# zpaq does not support symlink, so workaround it
	if [ -n "$FOLLOWLINKS" ] ; then
		babsdir="$(basename "$(realpath "$BACKUPDIR/$reldir")")"
		babsreldir="$(realpath "$BACKUPDIR/$reldir/..")"
	else
		babsdir="$bdir"
		babsreldir="$BACKUPDIR/$reldir/.."
	fi

	# workaround for correct internal path
	cd "$babsreldir" || exit


	if [ -n "$SINGLEARCHIVE" ] ; then
		local archname="$DUMPDIR/$BASEPACK.$STN.zpaq"
		local zarchname="$DUMPDIR/$BASEPACK.$STZ.zpaq"
	else
		# FIXME: copy dir permissions
		mkdir -p "$DUMPDIR/$reldir"

		local archname="$DUMPDIR/$reldir/$bdir.$STN.zpaq"
		local zarchname="$DUMPDIR/$reldir/$bdir.$STZ.zpaq"
	fi

	echo
	echo "Packing $reldir to $archname ..."

	# TODO: improve all that tmpoutput things
	local tmpoutput="$(mktemp)"
	# TODO: use -to for rename internal path
	echo zpaq add "$archname" "$babsdir" $CHECKSUM $EXCLUDEARG -index "$zarchname"
	(zpaq add "$archname" "$babsdir" $CHECKSUM $EXCLUDEARG $PASSWORDARG -index "$zarchname" 2>&1 && touch $tmpoutput.ok) | tee $tmpoutput
	local res=0
	[ -r "$tmpoutput.ok" ] || res=1
	if [ "$res" = 1 ] ; then
		local errfile=''
		if grep -q "zpaq: archive exists" $tmpoutput ; then
			errfile="$(grep "zpaq: archive exists" $tmpoutput | sed -e 's|^\(/.*\.zpaq\).*|\1|g')"
			mv -v $errfile $errfile.broken || fatal "can't move"
			fatal_status "recoverable error" "Run again to continue"
		fi
		if grep -q "zpaq: Skipping block" $tmpoutput ; then
			errfile="$(grep "zpaq: Skipping block" $tmpoutput | sed -e 's|^\(/.*\.zpaq\).*|\1|g')"
			info "Ignore error with $errfile ..."
			res=0
		fi
	fi
	# TODO: remove in fatal
	rm -f $tmpoutput $tmpoutput.ok
	[ "$res" = "0" ] || fatal_status "unrecoverable error" "Error during packaging to $archname"

	cd - >/dev/null

	if [ -n "$EXECUTEAFTER" ] ; then
		$EXECUTEAFTER "$archname" || fatal "Executed command $EXECUTEAFTER failed"
	fi
done || return

# Получаем список файлов в корне
# TODO: Здесь бы надо просто выключить рекурсию, но неизвестно как
# Также неизвестно, как раскрыть
# FIXME: broken with paths with spaces
# TODO: не поддерживается удаление файлов
# FIXME: длина строки ограничена!
local FILES="$(list_rootfiles)"

# workaround for correct internal path
cd "$BACKUPDIR" || exit

if [ -z "$NOATTRIBUTE" ] ; then
	echo
	echo "Save attrs and special files..."
	attrstore save
	FILES="$FILES $(attrstore metafiles)"
fi

if [ -n "$FILES" ] ; then

	#bdir="$(basename "$BACKUPDIR")"

	# FIXME: copy dir permissions
	mkdir -p "$DUMPDIR"

	local EXCLUDEARG=
	if [ -n "$EXCLUDEDIR" ] ; then
		if rhas "$EXCLUDEDIR" "^/" ; then
			EXCLUDEARG="-not $(clean_path "$EXCLUDEDIR")"
		#else
		#	EXCLUDEARG="-not $(clean_path "$EXCLUDEDIR")"
		fi
	fi

	echo
	echo "Packing extra files ..."
	# TODO: use -to for rename internal path
	echo zpaq add "$DUMPDIR/$BASEPACK.$STN.zpaq" $FILES $CHECKSUM $EXCLUDEARG -index "$DUMPDIR/$BASEPACK.$STZ.zpaq"
	zpaq add "$DUMPDIR/$BASEPACK.$STN.zpaq" $FILES $CHECKSUM $EXCLUDEARG $PASSWORDARG -index "$DUMPDIR/$BASEPACK.$STZ.zpaq" || { cd - ; return 1 ; }

	attrstore clear

	if [ -n "$EXECUTEAFTER" ] ; then
		$EXECUTEAFTER "$DUMPDIR/$BASEPACK.$STN.zpaq" || fatal "Executed command $EXECUTEAFTER failed"
	fi
fi

cd - >/dev/null

}

extract_dump()
{
local relfile
mkdir -p "$DESTDIR" || fatal "Can't create output directory $DESTDIR"
# Note: it is important to use ???.zpaq during extract (000.zpaq has no files, 001.zpaq will unpack only this archive).
find "$DUMPDIR" -type f -name "*.$STZ.zpaq" -printf "%P\n" | sed -e "s|\.$STZ.zpaq$|.$STN.zpaq|g" | \
while read relfile ; do
	reldir="$(dirname "$relfile")"
	# FIXME: copy dir permissions
	# TODO: хранить структуру каталогов отдельно, сразу и проверка?

	# workaround for root archive
	if [ "$reldir" = "." ] ; then
		tdir="$DESTDIR"
	else
		tdir="$(realpath -m "$DESTDIR/$reldir/..")"
	fi

	echo
	echo "Extract files from $relfile to $tdir ..."
	zpaq extract "$DUMPDIR/$relfile" -to "$tdir" $PASSWORDARG || exit
done

# TODO check it in some manner
#[ -n "$reldir" ] || fatal "No internal zpaq archive is found"

	if [ -z "$NOATTRIBUTE" ] ; then
		cd "$DESTDIR" || return
		echo
		echo "Restore attrs ..."
		attrstore restore
		attrstore clear
		cd - >/dev/null
	fi
}

compare_dump()
{
	local relfile
	[ -d "$DESTDIR" ] || fatal "Can't open directory $DESTDIR"
	# Note: it is important to use ???.zpaq during extract (000.zpaq has no files, 001.zpaq will unpack only this archive).
	find "$DUMPDIR" -type f -name "*.$STZ.zpaq" -printf "%P\n" | sed -e "s|\.$STZ.zpaq$|.$STN.zpaq|g" | \
	while read relfile ; do
		reldir="$(dirname "$relfile")"
		# workaround for root archive
		if [ "$reldir" = "." ] ; then
			reldir=""
		fi

		echo
		echo "Compare files from $relfile with $DESTDIR/$reldir ..."
		echo zpaq list "$DUMPDIR/$relfile" $reldir -to "$DESTDIR/$reldir" -not = $CHECKSUM
		# Note! $reldir without quotes therefore it can be empty
		zpaq list "$DUMPDIR/$relfile" $reldir -to "$DESTDIR/$reldir" $PASSWORDARG -not = $CHECKSUM || exit
		# TODO: unpack metadata during compare??
		#if [ -z "$NOATTRIBUTE" ] ; then
		#fi
	done || return

	# TODO
	#[ -n "$reldir" ] || fatal "No internal zpaq archive is found"

}

test_dump()
{
	local relfile
	# Note: it is important to use ???.zpaq during extract (000.zpaq has no files, 001.zpaq will unpack only this archive).
	find "$DUMPDIR" -type f -name "*.$STZ.zpaq" -printf "%P\n" | sed -e "s|\.$STZ.zpaq$|.$STN.zpaq|g" | \
	while read relfile ; do
		reldir="$(dirname "$relfile")"

		echo
		echo "Checking $relfile ..."
		zpaq extract "$DUMPDIR/$relfile" -test $PASSWORDARG || exit
	done || return

	# TODO
	#[ -n "$reldir" ] || fatal "No internal zpaq archive is found"

	# TODO: add checking by dirs.lists (correct structure)
}

print_status()
{
	if [ -e "$DUMPDIR/.status" ] ; then
		echo "Archive is incomplete, current status: $(cat "$DUMPDIR/.status")"
		exit 1
	fi
	echo "Archive is completed"
	exit 0
}

# zpaq version
ZPAQVERSION=$(zpaq | head -n1 | sed -e "s|.* v\([0-9]\.[0-9]*\) .*|\1|")
[ "$ZPAQVERSION" = "$NEEDZPAQ" ] || fatal "eterpack $VERSION supports only zpaq v$NEEDZPAQ"


COMMAND="$1"
[ -n "$COMMAND" ] && shift

MINDEPTH=1
MAXDEPTH=1
EXCLUDEDIR=''
EXECUTEAFTER=''
CHECKSUM=''
PASSWORDARG=''
NOATTRIBUTE=''
SINGLEARCHIVE=''
FOLLOWLINKS=''

while true ; do
	case "$1" in
	--depth)
		shift
		MINDEPTH=$1
		MAXDEPTH=$1
		shift
		;;

	--exclude)
		shift
		# TODO: allow repeatable --exclude
		[ -n "$EXCLUDEDIR" ] && fatal "TODO: A few --exclude not yet supported"
		EXCLUDEDIR="$1"
		shift
		;;

	--execute)
		shift
		EXECUTEAFTER="$1"
		shift
		;;

	--checksum)
		shift
		CHECKSUM="-force"
		;;

	--password)
		shift
		PASSWORDARG="-key $1"
		shift
		;;

	--noattribute|--noattributes)
		shift
		NOATTRIBUTE=1
		;;

	--singlearchive)
		shift
		SINGLEARCHIVE="1"
		;;

	--followlinks)
		shift
		FOLLOWLINKS="-L"
		;;
	*)
		break
		;;
esac
done

case $COMMAND in
	update|backup)
		# from
		BACKUPDIR="$(realpath -e "$1")" || exit

		# to
		DUMPDIR="$(realpath -m "$2")"

		# Note: without -p, do not create full path in suddenly place
		[ -d "$DUMPDIR" ] || mkdir "$DUMPDIR" || exit

		info "eterpack: packing to $DUMPDIR ..."

		if [ -s "$DUMPDIR/.status" ] ; then
			info "Previous interrupted task had status $(cat "$DUMPDIR/.status")"
			#"
		fi

		echo "in progress" >"$DUMPDIR/.status"
		[ -n "PASSWORDARG" ] && echo "encrypted" >"$DUMPDIR/.encrypted"

		if [ "$MAXDEPTH" = "0" ] ; then
			update_archive
			done_status
			# TODO: status
			exit
		fi

		update_dump || exit
		# TODO: это второй прогон. Надо встроить внутрь. Но там будет дописывание в файл
		list_dirs >"$DUMPDIR/dirs.list"
		list_rootfiles >"$DUMPDIR/files.list"
		done_status
		# TODO: status
	;;
	repack)
		fatal "do not realized"
		# from
		BACKUPDIR="$(realpath -e "$1")" || exit

		# to
		DUMPDIR="$(realpath -m "$2")"
		repack_dump
	;;
	compare)
		DUMPDIR="$(realpath -e "$1")"

		DESTDIR="$(realpath -e "$2")"
		compare_dump
	;;
	inlist)
		# from
		BACKUPDIR="$(realpath -e "$1")" || exit
		echo "Dirs:"
		list_dirs
		echo
		echo "Files:"
		list_rootfiles
	;;
	extract|restore)
		DUMPDIR="$(realpath -e "$1")" || exit
		# source dirname for target by default
		DESTDIR="$(basename $DUMPDIR)"
		[ -n "$2" ] && DESTDIR="$(realpath -m "$2")"
		extract_dump
	;;
	test|check)
		DUMPDIR="$(realpath -e "$1")" || exit
		test_dump
	;;
	status)
		DUMPDIR="$(realpath -e "$1")" || exit
		print_status
	;;
	-h|--help)
		echo $DESCR
		echo "Run with $0 command [options] args"

		echo
		echo "Create/Update backup:"
		echo "	$ eterpack update /path/from /path/to"

		#echo
		#echo "Repack backup (refresh zpaq files for minimize):"
		#echo "	$ eterbackup repack /path/from /path/to"

		echo
		echo "Restore backup:"
		echo "	$ eterpack restore /path/backup /path/to"

		echo
		echo "Compare backup with local files:"
		echo "	$ eterpack compare /path/backup /path/to"

		echo
		echo "Test backup integrity:"
		echo "	$ eterpack test /path/backup"

		echo
		echo "Print backup status:"
		echo "	$ eterpack status /path/backup"

		echo
		echo "Options:"
		echo "	--depth N          - set depth for subdirs (1 by default) (update only)"
		echo "	--exclude name     - exclude dir 'name' from packing (full path or level dir name)"
		echo "	--execute command  - execute 'command' after every archive"
		echo "	--checksum         - force checking file contains, not date only"
		echo "	--password         - use password for encryption/decryption"
		echo "	--noattribute      - do not extra save owner/group, permissions and special files"
		echo "	--singlearchive    - pack all subdirs to one package"
		echo "	--followlinks      - follow links for primary dirs"
	;;
	*)
		echo "$DESCR" >&2
		echo "Run with -h or --help for help" >&2
		exit 1
	;;
esac

