#!/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/>.
#

# Saved meta info for the current dir with use metastore and pax

export LANG=C

DESCR="eterattrstore - save&restore attrs and special files (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
}

PAXFILE=.eterpack.special.files.pax
METAFILE=.metadata
FOLLOWDIR=.eterpack.follow
# with --metadir, metafiles are kept in a separate dir (eterpack keeps them out of the source tree)
METADIR=''
# file with the list of followed symlinks to dirs (relative paths, one per line)
FOLLOWLIST=''
# file with the list of excluded paths (relative paths, one per line)
EXCLUDELIST=''

set_metadir()
{
	METADIR="$1"
	PAXFILE="$METADIR/$PAXFILE"
	METAFILE="$METADIR/$METAFILE"
	FOLLOWDIR="$METADIR/$FOLLOWDIR"
}

print_followed()
{
	[ -n "$FOLLOWLIST" ] && [ -s "$FOLLOWLIST" ] && cat "$FOLLOWLIST"
	return 0
}

# special files in the current dir and inside the followed dirs (but not the followed links itself)
list_special_files()
{
	local link
	find . -depth "(" -type b -or -type c -or -type p -or -type l ")" -printf "%P\n"
	print_followed | while read -r link ; do
		[ -d "$link/" ] || continue
		find "$link/" -depth -mindepth 1 "(" -type b -or -type c -or -type p -or -type l ")" -printf "%p\n"
	done
}

# skip the excluded paths and everything below them
skip_excluded()
{
	if [ -n "$EXCLUDELIST" ] && [ -s "$EXCLUDELIST" ] ; then
		awk -v list="$EXCLUDELIST" '
			BEGIN { while ((getline l < list) > 0) if (l != "") ex[l] = 1 }
			{ for (l in ex) if ($0 == l || index($0, l "/") == 1) next ; print }'
	else
		cat
	fi
	return 0
}

# the followed links are packed as dirs, so do not save them as symlinks
skip_followed()
{
	if [ -n "$FOLLOWLIST" ] && [ -s "$FOLLOWLIST" ] ; then
		grep -v -x -F -f "$FOLLOWLIST"
	else
		cat
	fi
	return 0
}

save_attrs()
{
	local res=0

	# save special files
	# Note: sv4cpio has no ustar limits (100 chars for a link target, about 255 chars for a path)
	list_special_files | skip_followed | skip_excluded | pax -wd -x sv4cpio -f "$PAXFILE" || res=1
	[ -s "$PAXFILE" ] || rm -f "$PAXFILE" 2>/dev/null

	# save attrs in .metafile
	metastore -s -f "$METAFILE" || res=1
	[ -s "$METAFILE" ] || rm -f "$METAFILE" 2>/dev/null

	# metastore does not follow symlinks (it stores the real paths), so save every followed dir separately
	local link n=0
	if [ -n "$(print_followed)" ] ; then
		mkdir -p "$FOLLOWDIR" || return 1
		while read -r link ; do
			n=$((n+1))
			printf '%s\n' "$link" >"$FOLLOWDIR/$n.path" || res=1
			(cd "$link/" && metastore -s -f "$FOLLOWDIR/$n.metadata") || res=1
		done <"$FOLLOWLIST"
	fi

	return $res
}

remove_attrs()
{
	rm -f "$PAXFILE" 2>/dev/null
	rm -f "$METAFILE" 2>/dev/null
	rm -rf "$FOLLOWDIR" 2>/dev/null
	[ -n "$METADIR" ] && rmdir "$METADIR" 2>/dev/null
	return 0
}

print_metafiles()
{
	[ -s "$PAXFILE" ] && echo "$PAXFILE"
	[ -s "$METAFILE" ] && echo "$METAFILE"
	return 0
}

# print the followed dirs stored in FOLLOWDIR
print_stored_followed()
{
	local pathfile link
	[ -d "$FOLLOWDIR" ] || return 0
	for pathfile in "$FOLLOWDIR"/*.path ; do
		[ -s "$pathfile" ] || continue
		read -r link <"$pathfile"
		echo "$link"
	done
}

# skip metastore lines for paths inside the followed dirs (they are compared separately,
# and they are real dirs in a restored tree)
skip_followed_metastore()
{
	awk -v list="$FOLLOWLIST" '
		BEGIN { while (list != "" && (getline l < list) > 0) links[l] = 1 }
		{ for (l in links) if (index($0, "./" l ":") == 1 || index($0, "./" l "/") == 1) next ; print }'
}

skip_added_removed()
{
	grep -v -E ':	(added|removed) *$'
	return 0
}

# pax listing without dates (symlink dates are not restored), sorted
list_pax()
{
	pax -v 2>/dev/null | grep -v '^pax: ' | sed -E \
		-e 's/^([^ ]+) +[0-9]+ +([^ ]+) +([^ ]+) +(.*) [A-Z][a-z]{2} [ 0-9][0-9] ( [0-9]{4}|[0-9]{2}:[0-9]{2}) (.*)$/\1 \2 \3 \4 \6/' | \
		sort -u
}

# compare the stored metadata with the current dir, print the differences
# return 1 if there are differences
compare_attrs()
{
	local res=0 out link pathfile tmp
	tmp="$(mktemp -d)" || return 1
	FOLLOWLIST="$tmp/follow"
	print_stored_followed >"$FOLLOWLIST"

	# owner, group and mode (metastore does not compare mtime by default)
	# added and removed files are skipped: the files are compared by zpaq, and excluded paths are not restored
	if [ -s "$METAFILE" ] ; then
		out="$(metastore -c -f "$METAFILE" | skip_followed_metastore | skip_added_removed)"
		[ -n "$out" ] && echo "$out" && res=1
	fi
	if [ -d "$FOLLOWDIR" ] ; then
		for pathfile in "$FOLLOWDIR"/*.path ; do
			[ -s "$pathfile" ] || continue
			read -r link <"$pathfile"
			[ -s "${pathfile%.path}.metadata" ] || continue
			out="$(cd "./$link/" && metastore -c -f "${pathfile%.path}.metadata" | skip_added_removed | sed -e "s|^\./|./$link/|")"
			[ -n "$out" ] && echo "$out" && res=1
		done
	fi

	# special files: type, owner, group, device numbers and link targets
	if [ -s "$PAXFILE" ] ; then
		list_pax <"$PAXFILE" >"$tmp/stored"
	else
		: >"$tmp/stored"
	fi
	# followed dirs are real dirs in a restored tree, so their files are listed twice
	list_special_files | skip_followed | skip_excluded | sort -u | pax -wd -x sv4cpio 2>/dev/null | list_pax >"$tmp/current"
	out="$(comm -23 "$tmp/stored" "$tmp/current" | sed -e 's|^|- |' ; comm -13 "$tmp/stored" "$tmp/current" | sed -e 's|^|+ |')"
	if [ -n "$out" ] ; then
		echo "Special files differ (- stored, + current):"
		echo "$out"
		res=1
	fi

	rm -rf "$tmp"
	return $res
}

restore_attrs()
{
	local pathfile link
	# always uses local .metastore for all subdirs
	# pax detects the format itself (sv4cpio or ustar in old backups)
	[ -s "$PAXFILE" ] && pax -r -f "$PAXFILE"
	[ -s "$METAFILE" ] && metastore -a -f "$METAFILE"
	if [ -d "$FOLLOWDIR" ] ; then
		for pathfile in "$FOLLOWDIR"/*.path ; do
			[ -s "$pathfile" ] || continue
			read -r link <"$pathfile"
			[ -s "${pathfile%.path}.metadata" ] || continue
			(cd "./$link/" && metastore -a -f "${pathfile%.path}.metadata")
		done
	fi
	return 0
}

COMMAND=$1
shift

while [ -n "$1" ] ; do
	case "$1" in
		--metadir)
			[ -n "$2" ] || fatal "--metadir requires a dir"
			set_metadir "$2"
			;;
		--follow)
			[ -n "$2" ] || fatal "--follow requires a file"
			FOLLOWLIST="$2"
			;;
		--exclude)
			[ -n "$2" ] || fatal "--exclude requires a file"
			EXCLUDELIST="$2"
			;;
		*)
			fatal "Unknown option $1"
			;;
	esac
	shift 2
done

# epm assure pax
# epm assure metastore

case $COMMAND in
	save)
		save_attrs
		;;
	restore)
		restore_attrs
		;;
	compare)
		compare_attrs
		;;
	metafiles)
		print_metafiles
		;;
	clear)
		remove_attrs
		;;
	-h|--help)
		echo $DESCR
		echo "Run as $0 command [--metadir DIR] [--follow FILE]"

		echo
		echo "Commands:"
		echo "	save       save attrs and spec. files"
		echo "	restore    restore attrs and spec. files"
		echo "	compare    show differences between stored and real metadata (exit 1 if they differ)"
		echo "	metafiles  print list of created files with metainfo"
		echo "	clear      remove files with metainfo"
		echo
		echo "Options:"
		echo "	--metadir DIR  keep metainfo in DIR (absolute path) instead of the current dir"
		echo "	--follow FILE  also save the dirs behind the symlinks listed in FILE (save only)"
	echo "	--exclude FILE skip special files under the paths listed in FILE (save and compare)"
		;;
	*)
		echo "$DESCR" >&2
		echo "Run with -h or --help for help" >&2
		exit 1
	;;
esac

