#!/usr/local/bin/cbsd
#v10.0.3
MYARG="target"
MYOPTARG="ver basename name dstdir stable basepath"
MYDESC="Upgrade base and/or kernel from other prepared hier"
ADDHELP="target= kernel, world, node(world+kernel)\n\
basepath - specify base or kernel source directory (arch, stable and ver will be ignored)\n"

. ${subrdir}/nc.subr
. ${cbsdinit}

. ${subrdir}/build.subr
. ${subrdir}/mailtools.subr
readconf buildworld.conf
. ${subrdir}/universe.subr

# $1 - source dir
# if $dstdir set - install to $dstdir (default is /boot/kernel)
upgrade_kernel()
{
	local _basedir DST_DIR _err
	[ -z "$1" ] && err 1 "${N1_COLOR}Give me source dir${N0_COLOR}"

	_basedir="${1}"

	if [ -n "${dstdir}" ]; then
		DST_DIR="${dstdir}"
	else
		DST_DIR="/boot/kernel"
	fi

	[ ! -d "${KERNEL_DIR}/boot/kernel" ] && err 1 "${N1_COLOR}No such kernel here: ${N2_COLOR}${KERNEL_DIR}/boot/kernel${N0_COLOR}"

	[ -d "${DST_DIR}.old" ] && rm -rf "${DST_DIR}.old"

	if [ -d "${DST_DIR}" ]; then
		mv "${DST_DIR}" "${DST_DIR}.old"
		$ECHO "${N1_COLOR}Old kernel rotated to: ${N2_COLOR}${DST_DIR}.old${N0_COLOR}"
	fi

	${ECHO} "${N1_COLOR}Upgrading kernel in ${DST_DIR}. don't interrupt the process${N0_COLOR}"
	_err=0
	cp -rP ${KERNEL_DIR}/boot/kernel ${DST_DIR} || _err=$(( _err + 1 ))
	kldxref ${DST_DIR} || _err=$(( _err + 1 ))

	[ ! -f "${DST_DIR}/kernel" ] && _err=$(( _err + 1 ))

	if [ $_err -ne 0 ]; then
		${ECHO} "${N1_COLOR}Error while upgrading kernel. Restoring old version${N0_COLOR}"
		rm -rf ${DST_DIR}
		mv "${DST_DIR}.old" "${DST_DIR}"
	fi

	sync
}

set_files_tofile()
{
	local _file _rootfile
	for _file in $( eval ${find_files} ); do
		echo "${_file#$rootdir}" >> ${tmpdir}/${name}.hier.$$
	done
}

set_dirs_tofile()
{
	local _dir _rootdir

	for _dir in $( eval ${find_dirs} ); do
		if [ "${_dir}" = "$rootdir" ]; then
			continue
		fi
		echo "${_dir#$rootdir}" >> ${tmpdir}/${name}.hier.$$
	done
}

# create separated removed,added,updated files from two hier list
# $1 - dst dir. $2 - src dir
create_hierdiff()
{
	sort ${1} > ${1}.sort.$$
	sort ${2} > ${2}.sort.$$

	comm -23 ${1}.sort.$$ ${2}.sort.$$ > ${tmpdir}/files.removed.$$
	comm -13 ${1}.sort.$$ ${2}.sort.$$ > ${tmpdir}/files.added.$$
	comm -12 ${1}.sort.$$ ${2}.sort.$$ > ${tmpdir}/files.updated.$$

	rm -f ${1}.sort.$$ ${2}.sort.$$
}

# For all paths appearing in $1, inspect the system
# and generate $2 describing what is currently installed
# in ${BASEDIR}
fetch_inspect_system()
{
	# No errors yet...
	rm -f ${tmpdir}/.err.$$
	# Tell the user why his disk is suddenly making lots of noise
	printf "  ${N2_COLOR}*${N1_COLOR} Inspecting system [${N2_COLOR}${BASEDIR}${N1_COLOR}]... ${N0_COLOR}"

	truncate -s0 ${tmpdir}/filelist.$$

	# Generate list of files to inspect
	cat $1 | cut -f 1 -d '|' | sort -u > ${tmpdir}/filelist.tmp.$$
	sort -u ${tmpdir}/filelist.tmp.$$ > ${tmpdir}/filelist.$$
	rm -f ${tmpdir}/filelist.tmp.$$

	# Examine each file and output lines of the form
	# /path/to/file|type|device-inum|user|group|perm|flags|value
	# sorted by device and inode number.
	while read F; do
		# If the symlink/file/directory does not exist, record this.
		if [ ! -e ${BASEDIR}/${F} ]; then
			echo "${F}|-||||||"
			continue
		fi

		if [ ! -r ${BASEDIR}/${F} ]; then
			echo "Cannot read file: ${BASEDIR}/${F}" >/dev/stderr
			touch ${tmpdir}/.err.$$
			return 1
		fi

		# Otherwise, output an index line.
		if [ -L ${BASEDIR}/${F} ]; then
			echo -n "${F}|L|"
			stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
			readlink ${BASEDIR}/${F};
		elif [ -f ${BASEDIR}/${F} ]; then
			echo -n "${F}|f|"
			stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
			sha256 -q ${BASEDIR}/${F};
		elif [ -d ${BASEDIR}/${F} ]; then
			echo -n "${F}|d|"
			stat -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
		else
			echo "Unknown file type: ${BASEDIR}/${F}" >/dev/stderr
			touch ${tmpdir}/.err.$$
			return 1
		fi
	done < ${tmpdir}/filelist.$$ | sort -k 3,3 -t '|' > $2.tmp.$$
	rm -f ${tmpdir}/filelist.$$

	# Check if an error occurred during system inspection
	[ -f ${tmpdir}/.err.$$ ] && return 1

	# Convert to the form
	# /path/to/file|type|user|group|perm|flags|value|hlink
	# by resolving identical device and inode numbers into hard links.
	cut -f 1,3 -d '|' $2.tmp.$$ |
	sort -k 1,1 -t '|' |
	sort -s -u -k 2,2 -t '|' |
	join -1 2 -2 3 -t '|' - $2.tmp.$$ |
	${AWK_CMD} -F \| -v OFS=\|         \
		'{
		if (($2 == $3) || ($4 == "-"))
			print $3,$4,$5,$6,$7,$8,$9,""
		else
			print $3,$4,$5,$6,$7,$8,$9,$2
		}' |
	sort > $2
	rm $2.tmp.$$

	# We're finished looking around
	${ECHO} "${N1_COLOR}done.${N0_COLOR}"
}

# Remove files which we want to delete
install_delete()
{
	# Generate list of new files
	cut -f 1 -d '|' < $2 | sort > newfiles

	# Generate subindex of old files we want to nuke
	sort -k 1,1 -t '|' $1 |
		join -t '|' -v 1 - newfiles |
		sort -r -k 1,1 -t '|' |
		cut -f 1,2 -d '|' |
		tr '|' ' ' > killfiles

	# Remove the offending bits
	while read FPATH TYPE; do
		case ${TYPE} in
		d)
			rmdir ${BASEDIR}/${FPATH}
			;;
		f)
			rm ${BASEDIR}/${FPATH}
			;;
		L)
			rm ${BASEDIR}/${FPATH}
			;;
	esac
	done < killfiles

	# Clean up
	rm newfiles killfiles
}


#Install new files, delete old files, and update linker.hints
prepare_joblist()
{
	# Get all the lines which mismatch in something other than file
	# flags.  We ignore file flags because sysinstall doesn't seem to
	# set them when it installs FreeBSD; warning about these adds a
	# very large amount of noise.

	sort -k 1,1 -t '|' $1 > $1.sorted

	cat $2 |
	comm -13 $1.sorted - |
	fgrep -v '|-|||||' |
	sort -k 1,1 -t '|' |
	join -t '|' $1.sorted - > ${tmpdir}/INDEX-NOTMATCHING

	# Ignore files which match IDSIGNOREPATHS.
	for X in ${IDSIGNOREPATHS}; do
		grep -E "^${X}" ${tmpdir}/INDEX-NOTMATCHING
	done |
	sort -u |
	comm -13 - ${tmpdir}/INDEX-NOTMATCHING > ${tmpdir}/INDEX-NOTMATCHING.tmp
	mv ${tmpdir}/INDEX-NOTMATCHING.tmp ${tmpdir}/INDEX-NOTMATCHING

	# Go through the lines and print warnings.
	while read LINE; do
		sqllist ${LINE} FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK P_TYPE P_OWNER P_GROUP P_PERM P_FLAGS P_HASH P_LINK

		# Warn about different object types.
		if [ "${TYPE}" != "${P_TYPE}" ]; then
			echo -n "${FPATH} is a "
			case "${P_TYPE}" in
				f)
					echo -n "regular file, "
					;;
				d)
					echo -n "directory, "
					;;
				L)
					echo -n "symlink, "
					;;
			esac

			echo -n "but should be a "
			case "${TYPE}" in
				f)
					echo -n "regular file."
					;;
				d)
					echo -n "directory."
					;;
				L)
					echo -n "symlink."
				;;
			esac

			echo
			echo "${FPATH}|${TYPE}|${OWNER}|${GROUP}|${PERM}|${FLAGS}|${HASH}|${LINK}" >> ${WORK_IDS}

			# Skip other tests, since they don't make sense if
			# we're comparing different object types.
			continue
		fi

		# Warn about different owners.
		if [ "${OWNER}" != "${P_OWNER}" ]; then
			echo -n "${FPATH} is owned by user id ${P_OWNER}, "
			echo "but should be owned by user id ${OWNER}."
			echo "${FPATH}|${TYPE}|${OWNER}|${GROUP}|${PERM}|${FLAGS}|${HASH}|${LINK}" >> ${WORK_IDS}
			continue
		fi

		# Warn about different groups.
		if [ "${GROUP}" != "${P_GROUP}" ]; then
			echo -n "${FPATH} is owned by group id ${P_GROUP}, "
			echo "but should be owned by group id ${GROUP}."
			echo "${FPATH}|${TYPE}|${OWNER}|${GROUP}|${PERM}|${FLAGS}|${HASH}|${LINK}" >> ${WORK_IDS}
			continue
		fi

		# Warn about different permissions.  We do not warn about
		# different permissions on symlinks, since some archivers
		# don't extract symlink permissions correctly and they are
		# ignored anyway.
		if [ "${PERM}" != "${P_PERM}" -a "${TYPE}" != "L" ]; then
			echo -n "${FPATH} has ${P_PERM} permissions, "
			echo "but should have ${PERM} permissions."
			echo "${FPATH}|${TYPE}|${OWNER}|${GROUP}|${PERM}|${FLAGS}|${HASH}|${LINK}" >> ${WORK_IDS}
			continue
		fi

		if [ "${FLAGS}" != "${P_FLAGS}" -a "${TYPE}" != "L" ]; then
			echo -n "${FPATH} has ${P_FLAGS} flags, "
			echo "but should have ${FLAGS} flags."
			echo "${FPATH}|${TYPE}|${OWNER}|${GROUP}|${PERM}|${FLAGS}|${HASH}|${LINK}" >> ${WORK_IDS}
			continue
		fi

		# Warn about different file hashes / symlink destinations.
		if [ "${HASH}" != "${P_HASH}" ]; then
			if [ "${TYPE}" = "L" ]; then
				echo -n "${FPATH} is a symlink to ${P_HASH}, "
				echo "but should be a symlink to ${HASH}."
				echo "${FPATH}|${TYPE}|${OWNER}|${GROUP}|${PERM}|${FLAGS}|${HASH}|${LINK}" >> ${WORK_IDS}
				continue
			fi
			if [ "${TYPE}" = "f" ]; then
				echo -n "${FPATH} has SHA256 hash ${P_HASH}, "
				echo "but should have SHA256 hash ${HASH}."
				echo "${FPATH}|${TYPE}|${OWNER}|${GROUP}|${PERM}|${FLAGS}|${HASH}|${LINK}" >> ${WORK_IDS}
				continue
			fi
		fi

		# We don't warn about different hard links, since some
		# some archivers break hard links, and as long as the
		# underlying data is correct they really don't matter.
	done < ${tmpdir}/INDEX-NOTMATCHING

	# Clean up
	rm $1.sorted ${tmpdir}/INDEX-NOTMATCHING
}

# Install new files
install_from_index()
{
	# First pass: Do everything apart from setting file flags.  We
	# can't set flags yet, because schg inhibits hard linking.
	sort -k 1,1 -t '|' $1 |
	tr '|' ' ' |
	while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK; do

		OLDFLAGS=$( stat -f "%Of" ${DST_DIR}${FPATH} 2>/dev/null )
		NEWFLAGS=$( stat -f "%Of" ${BASE_DIR}${FPATH} 2>/dev/null )

		if [ "$OLDFLAGS" != "0" ]; then
			echo "Clean flags for ${DST_DIR}${FPATH}"
			chflags 0 ${DST_DIR}${FPATH}
		fi

		case ${TYPE} in
			d)
				# Create a directory
				install -d -o ${OWNER} -g ${GROUP} -m ${PERM} ${DST_DIR}${FPATH}
				;;
			f)
				if [ -z "${LINK}" ]; then
					# Create a file, without setting flags.
					install -S -o ${OWNER} -g ${GROUP} -m ${PERM} ${BASE_DIR}${FPATH} ${DST_DIR}${FPATH}
				else
					install -S -o ${OWNER} -g ${GROUP} -m ${PERM} ${BASE_DIR}${FPATH} ${DST_DIR}${FPATH}
					# Create a hard link.
					# ln -f ${DST_DIR}${LINK} ${DST_DIR}${FPATH}
					ln -f ${DST_DIR}${FPATH} ${DST_DIR}${LINK}
					[ "${OLDFLAGS}" != "0" ] && chflags ${OLDFLAGS} ${DST_DIR}${FPATH}
				fi
				;;
			L)
				# Create a symlink
					#ln -sfh ${HASH} ${DST_DIR}${FPATH}
					ln -sfn ${HASH} ${DST_DIR}${FPATH}
				;;
		esac
		[ "$NEWFLAGS" != "0" ] && chflags $NEWFLAGS ${DST_DIR}${FPATH}
	done

	# Perform a second pass, adding file flags.
	tr '|' ' ' < $1 | while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK; do
		if [ ${TYPE} = "f" ] && ! [ ${FLAGS} = "0" ]; then
			chflags ${FLAGS} ${DST_DIR}/${FPATH}
		fi
	done
}


### Cumulative. func
stage1()
{
	${ECHO} "${N2_COLOR}* ${N1_COLOR}Create hier${N0_COLOR}"

	# create hier for source dir
	exd="^${BASE_DIR}(${exclude_dirs})"
	exf="^${BASE_DIR}(${exclude_files})"

	name=${SRC_NAME}
	rootdir=${BASE_DIR}
	find_files="find -E ${BASE_DIR} \( -type f -or -type l \) -and -not -regex \"$exf\""
	find_dirs="find -E ${BASE_DIR} -type d -and -not -regex \"$exd\""

	truncate -s0 ${tmpdir}/${SRC_NAME}.hier.$$
	set_files_tofile
	set_dirs_tofile

	if [ -f "${dbdir}/nodehier.${DST_NAME}.gz" ]; then
	    mv ${dbdir}/nodehier.${DST_NAME}.gz ${tmpdir}
	    gzip -d ${tmpdir}/nodehier.${DST_NAME}.gz
	    mv ${tmpdir}/nodehier.${DST_NAME} ${tmpdir}/${DST_NAME}.hier.$$
	else
	    ${ECHO} "${N1_COLOR}   Destination hier not found - source hier will be used instead${N0_COLOR}"
	    cp ${tmpdir}/${SRC_NAME}.hier.$$  ${tmpdir}/${DST_NAME}.hier.$$
	fi

	trap "rm -f ${tmpdir}/${SRC_NAME}.hier.$$ ${tmpdir}/${DST_NAME}.hier.$$ ${tmpdir}/files.removed.$$ ${tmpdir}/files.added.$$ ${tmpdir}/files.updated.$$ ${tmpdir}/cmp.ids.$$ ${tmpdir}/job.ids.$$" HUP INT ABRT BUS TERM EXIT
}

stage2()
{
	${ECHO} "${N2_COLOR}* ${N1_COLOR}Compare hier${N0_COLOR}"
	create_hierdiff ${tmpdir}/${DST_NAME}.hier.$$ ${tmpdir}/${SRC_NAME}.hier.$$
}

stage3()
{
	${ECHO} "${N2_COLOR}* ${N1_COLOR}Create IDS${N0_COLOR}"
	for BASEDIR in ${BASE_DIR} ${DST_DIR}; do
		name=$( /sbin/md5 -qs ${BASEDIR} )
		# For all paths appearing in INDEX-OLD or INDEX-NEW, inspect the
		# system and generate an INDEX-PRESENT file.
		fetch_inspect_system ${tmpdir}/${name}.hier.$$ ${tmpdir}/${name}.ids.$$ || return 1
	done
}

stage4()
{
	${ECHO} "${N2_COLOR}* ${N1_COLOR}Create file list from IDS${N0_COLOR}"
	truncate -s0 ${WORK_IDS}
	prepare_joblist ${tmpdir}/${SRC_NAME}.ids.$$ ${tmpdir}/${DST_NAME}.ids.$$
}

stage5()
{
	while read F; do
		fgrep "${F}|" ${tmpdir}/${SRC_NAME}.hier.$$ >> ${WORK_IDS}
	done < ${tmpdir}/files.added.$$
}

stage7()
{
	${ECHO} "${N2_COLOR}* ${N1_COLOR}Prune old files and directory from IDS${N0_COLOR}"
	install_delete ${tmpdir}/${SRC_NAME}.ids.$$ ${tmpdir}/${DST_NAME}.ids.$$
}

stage6()
{
	${ECHO} "${N2_COLOR}* ${N1_COLOR}Installing${N0_COLOR}"
	install_from_index ${WORK_IDS}
}

stage8()
{
	${ECHO} "${N2_COLOR}* ${N1_COLOR}Cleanup${N0_COLOR}"
	#store src hier as dst hier in gzip
	mv ${tmpdir}/${SRC_NAME}.hier.$$  ${tmpdir}/${DST_NAME}.hier
	gzip -9c ${tmpdir}/${DST_NAME}.hier > ${dbdir}/nodehier.${DST_NAME}.gz
	rm -f ${tmpdir}/${SRC_NAME}.ids.$$ ${tmpdir}/${DST_NAME}.ids.$$ ${tmpdir}/${SRC_NAME}.hier.$$ ${tmpdir}/${DST_NAME}.hier.$$ ${tmpdir}/${DST_NAME}.hier ${tmpdir}/files.removed.$$ ${tmpdir}/files.added.$$ ${tmpdir}/files.updated.$$ ${tmpdir}/cmp.ids.$$ ${tmpdir}/job.ids.$$
}

init_world()
{
	if [ -n "${dstdir}" ]; then
		DST_DIR="${dstdir}"
	else
		DST_DIR="/"
	fi

	[ ! -d "${BASE_DIR}" ] && err 1 "${N1_COLOR}You have no ${BASE_DIR}. Use ${N2_COLOR}cbsd world${N1_COLOR} or ${N2_COLOR}repo action=get sources=base${N1_COLOR} before upgrade${N0_COLOR}"

	SRC_NAME=$( /sbin/md5 -qs ${BASE_DIR} )
	DST_NAME=$( /sbin/md5 -qs ${DST_DIR} )
	CMP_IDS="${tmpdir}/cmp.ids.$$"
	WORK_IDS="${tmpdir}/job.ids.$$"
}

upgrade_world()
{
	${ECHO} "${N1_COLOR}Upgrading world in ${DST_DIR}. don't interrupt the process${N0_COLOR}"
	stage1
	stage2
	stage3
	stage4
	stage5
	stage6
#	stage7
	stage8
}

###MAIN
readconf upgrade.conf
init_target_arch
init_srcdir
init_supported_arch
init_basedir
init_kerneldir

#preferred utils from rescue
export PATH=/rescue:${PATH}

case "${target}" in
	"world")
		init_world
		upgrade_world
		;;
	"kernel")
		upgrade_kernel "${KERNEL_DIR}"
		;;
	"node")
		init_world
		upgrade_world
		upgrade_kernel "${KERNEL_DIR}"
		;;
	*)
		${ECHO} "${N1_COLOR}Unsupported target: ${N2_COLOR}${target}${N0_COLOR}"
		;;
esac
