#!/bin/sh
# Copyright (C) 2015  Etersoft
# Copyright (C) 2015  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)

# templates for multi part archive names
STZ="000000"
ST1="000001"
STN="??????"

DESCR="eterbackup version 0.5 (c) Etersoft 2015"

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
}

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

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

list_dirs()
{
#echo "List dirs from $BACKUPDIR, with $EXCLUDEDIR exclude."
# Получаем список каталогов, которые мы хотим превратить в файлы
find $BACKUPDIR -depth -maxdepth $DEPTH -mindepth $DEPTH -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_files()
{
	# TODO: exclude? or it is in zpaq?
	# Список файлов:
	find $BACKUPDIR -depth -maxdepth $DEPTH -mindepth 1 -type f -printf "%P\n"
}

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 ..."
	echo zpaq add "$DUMPDIR/$bdir.$STN.zpaq" "$bdir" -to . $CHECKSUM $EXCLUDEARG
	zpaq add "$DUMPDIR/$bdir.$STN.zpaq" "$bdir" -to . $CHECKSUM $EXCLUDEARG || { cd - ; exit 1 ; }
	cd - >/dev/null

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

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

}

update_dump()
{
# Получаем список каталогов, которые мы хотим превратить в файлы
find $BACKUPDIR -depth -maxdepth $DEPTH -mindepth $DEPTH -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: удалённые каталоги будут незамечены и останутся.
	# Создавать новый слой, копируя со старого, и оставляя тот старыми датами?

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

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

	echo
	echo "Packing $reldir ..."
	# TODO: use -to for rename internal path
	echo zpaq add "$DUMPDIR/$reldir/$bdir.$STN.zpaq" "$bdir" $CHECKSUM $EXCLUDEARG
	zpaq add "$DUMPDIR/$reldir/$bdir.$STN.zpaq" "$bdir" $CHECKSUM $EXCLUDEARG || { cd - ; exit 1 ; }
	cd - >/dev/null

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

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

# 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" ] && rhas "$EXCLUDEDIR" "^/" ; then
		EXCLUDEARG="-not $(clean_path "$EXCLUDEDIR")"
	fi

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

	attrstore clear

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

cd - >/dev/null

}

extract_dump()
{
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" || exit
done

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

compare_dump()
{
	[ -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 ..."
		# Note! $reldir without quotes therefore it can be empty
		zpaq list "$DUMPDIR/$relfile" $reldir -to "$DESTDIR/$reldir" -not = $CHECKSUM || exit
		# TODO: unpack metadata during compare??
		#if [ -z "$NOATTRIBUTE" ] ; then
		#fi
	done || return
}

test_dump()
{
	# 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 || exit
	done || return

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

COMMAND=$1
shift

DEPTH=1
if [ "$1" = "-depth" ] || [ "$1" = "--depth" ] ; then
	shift
	DEPTH=$1
	shift
fi

# TODO: allow repeatable --exclude
EXCLUDEDIR=
if [ "$1" = "--exclude" ] ; then
	shift
	EXCLUDEDIR="$1"
	shift
fi

EXECUTEAFTER=
if [ "$1" = "--execute" ] ; then
	shift
	EXECUTEAFTER="$1"
	shift
fi

CHECKSUM=
if [ "$1" = "--checksum" ] ; then
	shift
	CHECKSUM="-force"
fi

NOATTRIBUTE=
if [ "$1" = "--noattribute" ] ; then
	shift
	NOATTRIBUTE=1
fi

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

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

		if [ "$DEPTH" = "0" ] ; then
			update_archive
			exit
		fi

		update_dump || exit
		# TODO: это второй прогон. Надо встроить внутрь. Но там будет дописывание в файл
		list_dirs >$DUMPDIR/dirs.list
		list_files >$DUMPDIR/files.list
	;;
	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_files
	;;
	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
	;;
	-h|--help)
		echo $DESCR
		echo "Run with $0 command [options] args"

		echo
		echo "Create/Update backup:"
		echo "	$ eterbackup 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 "	$ eterbackup restore /path/backup /path/to"

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

		echo
		echo "Test backup integrity:"
		echo "	$ eterbackup test /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 "	--noattribute      - do not extra save owner/group, permissions and special files"
		echo "(use options strict in this order)"
	;;
	*)
		echo "$DESCR" >&2
		echo "Run with -h or --help for help" >&2
		exit 1
	;;
esac

