#!/bin/sh

# Upgrade the rwfs partition if necessary.

# Parameters to save and restore during SOFT_RESET.
backup_parameters="
RemoteService.Enabled
RemoteService.ProxyServer
RemoteService.ProxyPort
RemoteService.ProxyLogin
RemoteService.ProxyPassword
RemoteService.ProxyAuth
RemoteService.ProxyDispatcherOnly
System.BoaPort
"

backup_files=
# reads the conf file for the paths to the three backup-files
# $BCK_PARAMETERS_FILE, $BCK_FILES_FILE $BCK_GROUPS_FILE

bck_conf=/etc/bck/fd_survive.conf
[ ! -r $bck_conf ] || . $bck_conf

# The three read files are created by factorydefaultsurvive.cgi which is a
# script that adds files and parameters to "backup_parameters",
# "backup_files and backup_groups" to be saved during a software reset.

[ ! -r "$BCK_PARAMETERS_FILE" ] ||
	backup_parameters="$backup_parameters $(cat $BCK_PARAMETERS_FILE)"

[ ! -r "$BCK_FILES_FILE" ] ||
	backup_files="$backup_files $(cat $BCK_FILES_FILE)"

backup_groups="
"
[ ! -r "$BCK_GROUPS_FILE" ] ||
	backup_groups="$backup_groups $(cat $BCK_GROUPS_FILE)"

PHC="parhandclient --no-mapped"

BACKUP_DIR=/tmp

UEP=/usr/etc/param
UED=/usr/etc/defaultfiles
ED=/etc/dynamic
EDP=$ED/param
EDT=$BACKUP_DIR/$ED/tmp

EPP=/etc/parhand/portmanager

PHBCK=/etc/parameters_backup.txt

PHTMPBCK=/tmp/parameters_backup.txt
BCK_ARCHIVE=/tmp/bck.tar
BCK_PERMISSIONS_FILE=/tmp/backup_permissions
BCK_BACKUP_FILES_FILE=/tmp/bck_files
PREUPGRADE_SCRIPTS_DIR=/usr/lib/fsupgrade/pre-upgrade.d
SAVEFILES_DIR=/usr/lib/fsupgrade/upgrade-persistent.d
IV_ROLLBACK_DIR=/usr/local/fsupgrade-rollback

BCK_IGNORELIST_FILE=/tmp/bck_ignore.list

# Location for scripts to run after all other handling is done.
POST_UPGRADE_SCRIPTS_LOC=/etc/fsupgrade.d

# Location for scripts to run after all other handling is done
# when a reset has been made.
POST_UPGRADE_RESET_SCRIPTS_LOC=/etc/fsreset.d

SELF=${0##*/}

file_functions=./lib/rcscripts/sh/files.sh
[ -f $file_functions ] && . $file_functions ||
	error "Failed to source helper '$file_functions'!"

info() {
	echo "$SELF: $*">&2
	echo "$SELF: $*">>/run/fsupgrade.log
}

warning() {
	info "$*!"
}

error() {
	warning "$*"
	exit 1
}

make_dir() {
	local f=make_dir

	[ $# -eq 1 ] && [ "$1" ] || error "$f: wrong number of arguments"

	[ -d $1 ] || mkdir -p $1 ||
		error "$f: Failed to create directory '$1'"
}

files_functions=/lib/rcscripts/sh/files.sh
[ -f $files_functions ] && . $files_functions ||
	error "Failed to source helper '$files_functions'!"

start_parhand_dyn() {
	info "Starting parhand..."
	if parhand /dev/null 50011 -d $EDP -daemon -upgrade; then
		info "done"
	else
		warning "start_parhand_dyn: parhand failed"
		return 1
	fi
}

start_parhand() {
	local _epp=

	info "Starting parhand..."
	[ ! -d $EPP ] || _epp="-d $EPP"

	if parhand /dev/null 50011 -d $UEP -d $EDP $_epp -daemon -upgrade; then
		info "done"
	else
		warning "start_parhand: parhand failed"
		return 1
	fi
}

# Stop a daemon. The method used to stop the daemon depends on the arguments:
#
# * Only one argument that contains slashes: stop all processes that are
#   instances of that executable.
# * More than one argument: stop all processes that are started by the
#   commandline specified by all arguments together.
stop_daemon() {
	local _daemonpath _basename _pidfile
	local _ssd=start-stop-daemon

	[ $# -gt 0 ] || error "stop_daemon: wrong number of arguments"

	_daemonpath=${1%% *}
	_basename=${_daemonpath##*/}
	_pidfile=/var/run/$_basename.pid

	if [ -f "$_pidfile" ]; then
		! $_ssd --stop --retry 5 --pidfile "$_pidfile" || return 0

		# That didn't work. Remove the pidfile and try other methods.
		rm -f "$_pidfile"
	fi

	# Get the absolute path to the daemon executable.
	[ "$_daemonpath" != "$_basename" ] ||
		_daemonpath=$(command which "$_basename" || :)

	if [ "$_daemonpath" ]; then
		$_ssd --stop --retry 5 --oknodo --exec "$_daemonpath"
	else
		warning "stop_daemon: daemon path missing"
		return 1
	fi
}

stop_parhand() {
	info "Stopping parhand..."
	if stop_daemon /bin/parhand; then
		info "done"
	else
		warning "stop_parhand: Could not stop parhand, failed!"
		return 1
	fi
}

add_bckf() {
	[ $# -le 1 ] || error "add_bckf: invalid arguments"

	[ ! -e $1 ] || {
		warning "+ saving '$1'"
		printf "%s" "$1"
	}
}

source_bckf() {
	# Get list of files
	local _conff=$UED/fsupgrade.conf

	[ -f $_conff ] && [ -r $_conff ] ||
		error "source_bckf: conf file '$_conff' not found"
	. $_conff || error "source_bckf: failed to source '$_conff'"
}

# Returns UID/GID from given entry
# Input shall be in passwd/group file format
get_uid_gid_from_entry() {
	# $1: variable name used to assign return value
	# $2: line from /etc/passwd or /etc/group

	local _f=get_uid_gid_from_entry

	[ $# -eq 2 ] && [ "$1" ] && [ "$2" ] ||
		error "$_f: invalid argument(s)"

	local _entry_id

	_entry_id=${2#*:*:}
	_entry_id=${_entry_id%%:*}
	eval $1=\$_entry_id
}

# Returns user/group name of given entry
# Input shall be in passwd/group file format
get_user_grp_name_from_entry() {
	# $1: variable name used to assign return value
	# $2: a line from /etc/passwd or /etc/group

	local _f=get_user_grp_name_from_entry

	[ $# -eq 2 ] && [ "$1" ] && [ "$2" ] ||
		error "$_f: invalid argument(s)"

	eval $1=\${2%%:*}
}

# Returns user/group entry with given user/group name
# Input shall be in passwd/group file format
get_user_grp_entry_with_name() {
	# $1: variable name to assign return value on
	# $2: string of /etc/passwd or /etc/group
	# $3: user or group name to look for

	local _f=get_user_grp_entry_with_name

	[ $# -eq 3 ] && [ "$1" ] && [ "$2" ] && [ "$3" ] ||
		error "$_f: invalid argument(s)"

	local _entry _found_entry=
	local IFS='
'
	for _entry in $2; do
		case $_entry in
			$3:*)
				_found_entry=$_entry
				break
				;
		esac
	done

	eval $1=\$_found_entry
}

# Returns user/group entry with given UID/GID.
# Input shall be in passwd/group file format
get_user_grp_entry_with_id() {
	# $1: variable name to assign return value on
	# $2: string of /etc/passwd or /etc/group
	# $3: user or group name

	local _f=get_user_grp_entry_with_id

	[ $# -eq 3 ] && [ "$1" ] && [ "$2" ] && [ "$3" ] ||
		error "$_f: invalid argument(s)"

	local _entry _found_entry=
	local IFS='
'
	for _entry in $2; do
		case $_entry in
			*:*:$3:*)
				_found_entry=$_entry
				break
				;
		esac
	done

	eval $1=\$_found_entry
}

# Returns GID of given user entry.
# Input shall be in passwd format
get_gid_of_user_entry() {
	# $1: variable name used to assign return value
	# $2: line from /etc/passwd or /etc/group

	local _f=get_gid_of_user_entry

	[ $# -eq 2 ] && [ "$1" ] && [ "$2" ] ||
		error "$_f: invalid argument(s)"

	local _entry_gid

	_entry_gid=${2#*:*:*:}
	_entry_gid=${_entry_gid%%:*}
	eval $1=\$_entry_gid
}

# Returns the id that is 1 greater than the biggest id.
# Input shall be in passwd format
get_first_unused_id() {
	# $1: variable name used to assign return value
	# $2: string value of /etc/passwd or /etc/group file
	# $3: smallest id that can be returned

	local _f=get_first_unused_id

	[ $# -eq 3 ] && [ "$1" ] && [ "$2" ] && [ "$3" ] ||
		error "$_f: invalid argument(s)"

	local entry entry_id entry_name first_unused_id=$3
	local IFS='
'

	for entry in $2; do
		get_user_grp_name_from_entry entry_name "$entry"

		[ "$entry_name" != nobody ] && [ "$entry_name" != nogroup ] ||
			continue

		get_uid_gid_from_entry entry_id "$entry"

		[ $first_unused_id -gt $entry_id ] ||
			first_unused_id=$(($entry_id + 1))
	done

	eval $1=\$first_unused_id
}

# Merges two passwd or group files and assigns result to given variable.
pwd_grp_merge() {
	# $1: type of file to merge, "passwd" or "group"
	# $2: path to old passwd/group file
	# $3: path to new passwd/group file
	# $4: which entry to prefer in the event of a name collision, "old" or "new"
	# $5: path to the merged file
	# output: A temporary file with the merged result

	local _f=pwd_grp_merge

	[ $# -eq 5 ] && [ "$1" ] && [ "$2" ] && [ "$3" ] && [ "$4" ] && [ "$5" ] ||
		error "$_f: wrong number of arguments"
	[ $1 = passwd ] || [ $1 = group ] ||
		error "$_f: 2nd parameter shall be \"passwd\" or \"group\""
	[ -f $2 ] && [ -r $2 ] || error "$_f: $2 does not exit or not readable"
	[ -f $3 ] && [ -r $3 ] || error "$_f: $3 does not exit or not readable"
	[ $4 = old ] || [ $4 = new ] ||
		error "$_f: 4th parameter shall be 'old' or 'new'"
	[ -f $5 ] && [ -r $5 ] || error "$_f: $5 does not exit or not readable"

	local mergedentry mergedlines= newentry newentrywitholdid
	local newentrywitholdname newfile new_grp newid newlines newname
	local oldfile oldlines oldentry oldid oldname old_grp tmpmf unused_id
	local prefer ucu_start_id=1000

	local IFS='
'
	filetype=$1
	oldfile=$2
	newfile=$3
	prefer=$4
	tmpmf=$5

	# Check the format of the entries while reading the files,
	# badly formatted lines get discarded.
	oldlines=$(sed -rne '/^[^:]+:[^:]*:[0-9]+:.*/ p' $oldfile)
	newlines=$(sed -rne '/^[^:]+:[^:]*:[0-9]+:.*/ p' $newfile)

	for newentry in $newlines; do
		get_uid_gid_from_entry newid "$newentry"
		get_user_grp_name_from_entry newname "$newentry"

		[ "$filetype" != passwd ] || {
			get_gid_of_user_entry newgrp "$newentry"

			# user in "users" will be merged later
			[ $newgrp -ne 100 ] || continue
		}

		# get the same user from old file if available
		get_user_grp_entry_with_name oldentry "$oldlines" "$newname"

		if [ "$oldentry" ]; then
			get_uid_gid_from_entry oldid "$oldentry"

			if [ "$filetype" != passwd ] &&
				{ [ "$newname" = root ] ||
				  [ "$newname" = admin ] ||
				  [ "$newname" = operator ] ||
				  [ "$newname" = viewer ] ||
				  [ "$newname" = users ] ||
				  [ "$newname" = ptz ] ||
				  [ "$newname" = nvrdevice ]; }; then
				# UCUs are added to this group, always keep the user list from the old entry
				if [ $prefer = new ]; then
					mergedlines="$mergedlines${newentry%:*}:${oldentry##*:}
"
				else
					mergedlines="$mergedlines$oldentry
"
				fi
			else
				if [ $newid -eq $oldid ] || [ $prefer = new ]; then
					# UID/GID for this entry has not changed, or the caller prefers the new one
					mergedlines="$mergedlines$newentry
"
				else
					# check if old UID/GUI is used for another entry in new file, try to keep old UID/GUI
					get_user_grp_entry_with_id newentrywitholdid "$newlines" "$oldid"
					if [ "$newentrywitholdid" ]; then
						# same UID/GUI is used for another entry in new file, use new entry with updated UID/GUI
						mergedlines="$mergedlines$newentry
"
					else
						# no entry with old UID/GUI exist, keep the old one
						mergedlines="$mergedlines$oldentry
"
					fi
				fi
			fi
		else
			# this user did not exist before
			mergedlines="$mergedlines$newentry
"
		fi
	done

	# keep old removed entries, if possible
	for oldentry in $oldlines; do
		get_uid_gid_from_entry oldid "$oldentry"
		get_user_grp_name_from_entry oldname "$oldentry"

		[ "$filetype" != passwd ] || {
			get_gid_of_user_entry oldgrp "$oldentry"

			# user in "users" will be merged later
			[ $oldgrp -ne 100 ] || continue
		}

		get_user_grp_entry_with_name newentrywitholdname "$newlines" "$oldname"

		if [ -z "$newentrywitholdname" ]; then
			# old user is removed, if the UID is not used in new file keep the user
			get_user_grp_entry_with_id newentrywitholdid "$newlines" "$oldid"
			if [ -z "$newentrywitholdid" ]; then
				# UID is availabe, keep user
				mergedlines="$mergedlines$oldentry
"
			else
				info "Following entry in old file could not be merged due to conflict: $oldentry"
			fi
		fi
	done

	# keep all user created users (UCUs)
	# merge all UCUs from old file, they belong to "users (100)" group
	if [ "$filetype" = passwd ]; then
		for oldentry in $oldlines; do
			get_gid_of_user_entry oldgrp "$oldentry"

			# merge only UCUs
			if [ $oldgrp -eq 100 ]; then
				get_uid_gid_from_entry oldid "$oldentry"

				get_first_unused_id unused_id "$mergedlines" "$ucu_start_id"
				oldentry=$(echo "$oldentry" | sed -e "s/$oldid/$unused_id/g")
				mergedlines="$mergedlines$oldentry
"
			fi
		done

		# merge all UCUs from new file
		for newentry in $newlines; do
			get_gid_of_user_entry newgrp "$newentry"

			# merge only UCUs
			if [ $newgrp -eq 100 ]; then
				get_user_grp_name_from_entry newname "$newentry"

				# check ift his user is already added from old file UCUs
				get_user_grp_entry_with_name mergedentry "$mergedlines" "$newname"

				if [ -z "$mergedentry" ]; then
					get_uid_gid_from_entry newid "$newentry"

					get_first_unused_id unused_id "$mergedlines" "$ucu_start_id"
					newentry=$(echo "$newentry" | sed -e "s/$newid/$unused_id/g")
					mergedlines="$mergedlines$newentry
"
				fi
			fi
		done
	fi

	# change shell to /bin/false for non root user
	[ "$filetype" != passwd ] &&
		mergedlines=$(echo "$mergedlines" |
			sed -e '/^[^(root)]/s/\/bin\/sh$/\/bin\/false/')

	mergedlines=$(echo "$mergedlines" | sed -e '/^$/d' | sort -t : -k 3n)

	# Write the result to the temporary merge file. This file will be
	# replacing the original file safely and the parent function.
	echo "$mergedlines" > $tmpmf ||
		error "Failed write the temporary merge file '$tmpmf'"
}

# Prints the entry matching the login name.
# First argument is a login name, second is a shadow file.
get_line_from_shadow() {
	local line

	[ $# -eq 2 ] || error "get_line_from_shadow: missing argument"

	for line in $1; do
		[ ${line%%:*} != $2 ] || {
			echo $line
			return 0
		}
	done

	return 1
}

# Merges two shadow files and prints the result in the same order as in
# the passwd file.
# If there are any conflicts, the entry from the old file stays.
# First argument is the current passwd file, second is the old shadow file,
# third is the new shadow file,
shadow_merge() {
	# $1: Current passwd file
	# $2: Old shadow file
	# $3: New shadow file
	# $4: Merged shadow file
	# output: Merged content to the merged shadow file ($4)

	[ $# -eq 4 ] || error "shadow_merge: missing argument"
	[ -f $1 ] && [ -r $1 ] || error "$f: '$1' missing or not readable"
	[ -f $2 ] && [ -r $2 ] || error "$f: '$2' missing or not readable"
	[ -f $3 ] && [ -r $3 ] || error "$f: '$3' missing or not readable"
	[ -f $4 ] && [ -r $4 ] || error "$f: '$4' missing or not readable"

	local f=shadow_merge line passwd_lines cur_sf old_sf new_sf merged_sf
	local IFS restore_IFS

	cur_sf=$1
	old_sf=$2
	new_sf=$3
	merged_sf=$4

	IFS='
'

	passwd_lines=$(sed -rne '/^[^:]+:[^:]*:[0-9]+:.*/ p' $cur_sf)
	old_shadow_lines=$(sed -rne '/^[^:]+:[^:]+/ p' $old_sf)
	new_shadow_lines=$(sed -rne '/^[^:]+:[^:]+/ p' $new_sf)

	for line in $passwd_lines; do
		login_name=${line%%:*}
		get_line_from_shadow "$old_shadow_lines" $login_name ||
			get_line_from_shadow "$new_shadow_lines" $login_name || {
				# Generate an entry using the same values as
				# pwconv if an entry is missing in both old and
				# new shadow file,
				restore_IFS=$IFS
				IFS=:
				set -- $line
				IFS=$restore_IFS
				[ $# -ge 2 ] || {
					warning "$f: line split failed"
					continue
				}
				echo $1:$2:10231:0:99999:7:::
			}
	done >$merged_sf ||
		error "$f: failed to write the merged shadow file '$merged_sf'"
}

generate_new_preset_file() {
	local f=generate_new_preset_file tmppf line _1st_part _last_part index

	[ $# -eq 1 ] && [ "$1" ] || error "$f: missing argument"
	[ -f $1 ] && [ -r $1 ] || error "$f: '$1' missing or not readable"

	info "Parsing Preset file: $1"
	tmppf=$(mktemp /tmp/ptz_preset.new.XXXXXX) ||
		error "$f: temporary preset file creation failed"

	while read line; do
		_1st_part=${line%[[:blank:]]*}
		_last_part=${line#$_1st_part}
		_last_part=${_last_part##[[:blank:]]}
		case $_last_part in
			[0-9]_position|[0-9][0-9]_position)
				# The preset file has already the new format.
				rm -f $tmppf ||
					error "$f: failed to remove temporary" 						"preset file '$tmppf'"
					return 0
					;
			position)
				# Needs changes.
				index=${_1st_part#*PTZ.Preset.P}
				index=${index%%.*}
				echo $_1st_part ${index}_position >>$tmppf ||
					error "$f: failed to update temporary" 						"file '$tmppf'"
					;
			*)
				rm -f $tmppf ||
					error "$f: failed to remove temporary" 						"file '$tmppf'"
				error "$f: internal error; bad code" 					"'$_last_part' or bad line '$line'"
			;
		esac
	done <$1
	info "Replacing: $1"
	mv -f $tmppf $1 || error "$f: failed to replace '$1' with '$tmppf'"
}

modify_preset_file() {
	local f is_preset_file

	if [ -d $EDT ]; then
		for f in $EDT/*; do
			[ -f $f ] && [ -r $f ] || continue
			is_preset_file=${f#*ptz_preset_p}
			[ "$is_preset_file" = $f ] ||
				generate_new_preset_file $f ||
				error "modify_preset_file: failed to parse '$f'"
		done
	fi
}

add_bckf_from_file() {
	local line filelist= IFS="
"

	if [ -r "$1" ]; then
		while read line; do
			case $line in
				\#*)
					continue
					;
			esac
			filelist="$filelist $(add_bckf $line)"
		done <$1
	else
		warning "Failed to read file '$1'"
	fi

	echo $filelist
}

set_owner_and_permissions() {
	[ $# -eq 4 ] && [ "$1" ] && [ "$2" ] && [ "$3" ] && [ "$4" ] ||
		error "set_owner_and_permissions: missing argument"

	if [ -e $1 ]; then
		chmod $2 $1 ||
			warning "Failed to set permissions to '$2' on '$1'"
		chown $3:$4 $1 ||
			warning "Failed to set owner,group to '$3','$4' on '$1'"
	else
		warning "Skip restoring permissions for '$1', file does not exist"
	fi
}

# Creates an archive of backup files and a seperate file containing the backup
# files original path.
archive_files() {
	# $1: Space separated list of files to backup
	# output: Status code, 0=SUCCESS/1=FAILURE

	local files f self=archive_files

	files=$1

	info "$self: Saving files temporarily..."
	touch $BCK_IGNORELIST_FILE

	if tar cf $BCK_ARCHIVE -X $BCK_IGNORELIST_FILE $files; then
		# Save a list of the backuped files, to be used at restore
		echo $files >$BCK_BACKUP_FILES_FILE ||
			warning "Failed to save list of backup files"
	else
		warning "Failed to create archive of backup files"
	fi

	rm $BCK_IGNORELIST_FILE && info "done" || {
		warning "Failed to remove backup ignore list"
		return 1
	}
}

perform_upgrade_backup() {
	local backup_files bck_list eraseparam extdr f filedevid grp jffsid oldtype
	local prm rootdevid vfsu_d

	[ $# -eq 2 ] && [ "$1" ] && [ "$2" ] || error "perform_upgrade_backup: missing argument"
	jffsid=$1
	oldtype=$2

	if [ "$jffsid" != ONVIF_HARD_RESET ]; then
		# Run pre-upgrade scripts
		if [ -d $PREUPGRADE_SCRIPTS_DIR ]; then
			for f in $PREUPGRADE_SCRIPTS_DIR/*; do
				info "Pre-upgrade script '$f' ..." && JFFSID=$jffsid $f && info "done"  ||
					warning "Failed to execute pre upgrade script '$f'"
			done
		fi
	fi

	# Files for parameters not handled by parhand
	source_bckf
	case $jffsid in
		ONVIF_HARD_RESET)
			bck_list=$ONVIF_HARD_RESET_PERSISTENT_FILES
			;
		SOFT_RESET)
			bck_list=$RESET_PERSISTENT_FILES
			;
		*)
			bck_list=$UPGRADE_PERSISTENT_FILES
			;
	esac
	for f in $bck_list; do
		backup_files="$backup_files $(add_bckf $f)"
	done
	if [ -d $SAVEFILES_DIR ] && [ "$(ls -A $SAVEFILES_DIR)" ]; then
		for f in $SAVEFILES_DIR/*; do
			backup_files="$backup_files $(add_bckf_from_file $f)"
		done
	fi

	make_dir /var/tmp || error "Failed to create /var/tmp"
	make_dir /var/run/parhand || error "Failed to create /var/run/parhand"

	if [ "$jffsid" = ONVIF_HARD_RESET ]; then
		archive_files "$backup_files"

		exit 0
	fi

	if [ "$jffsid" = SOFT_RESET ]; then
		# Soft factory default
		if start_parhand; then
			info 'Saving parameters temporarily...'
			exec 7>$PHTMPBCK || error "Could not open '$PHTMPBCK'"
			for prm in $backup_parameters; do
				$PHC get root.$prm - NAMEVALUE >&7 ||
				warning "'$PHC get root.$prm' failed"
			done
			for grp in $backup_groups; do
				$PHC getgroup root.$grp - NAMEVALUE >&7 ||
				warning "'$PHC getgroup root.$grp' failed"
			done
			exec 7>&-
			info "done"
		else
			warning "Failed to save parameters"
		fi
		stop_parhand || warning "Failed to stop parhand(1)"

		extdr=/sbin/rc.extdevrestore
		if [ -x $extdr ]; then
			info "Resetting external device..."
			$extdr
		fi
	else
		# Upgrade

		# Don't erase third part applications
		eraseparam=--noiv

		# Create directories for merging persistant conf files.
		vfsu_d=/var/tmp/fsupgrade
		make_dir $vfsu_d

		# Save copies of the old files before update.
		# These will be handled by scripts in
		# POST_UPGRADE_SCRIPTS_LOC later.
		for f in $UPGRADE_POST_UPGRADE_FILES; do
			[ ! -r $f ] || cp $f $vfsu_d ||
				warning "Failed to copy '$f' to '$vfsu_d'"
		done

		if [ -f $PHBCK ] && [ -w $PHBCK ]; then
			info "Will use previously stored parameters..."
			mv $PHBCK $PHTMPBCK || error "Failed to move '$PHBCK' to '$PHTMPBCK'"
			info "done"
		else
			if start_parhand; then
				info "Backup parameters..."
				$PHC getgroup root $PHTMPBCK NAMEVALUE || :
				info "done"
			else
				warning "Failed to backup parameters"
			fi
			stop_parhand || warning "Failed to stop parhand(2)"
		fi

		if [ -d $EDP ]; then
			make_dir $EDT
			for f in $EDP/*; do
				[ -f $f ] && [ -r $f ] || continue
				grep DO $f >$EDT/${f#$EDP/} 2>/dev/null
			done

			modify_preset_file
			backup_files="$backup_files $ED"
		fi
	fi

	if [ "$oldtype" = lfp ]; then
		rootdevid=$(stat -c %d /) ||
			warning "Could not determine device id mounted on /"

		# We don't want to back up files which actually came from the
		# rootfs rather than the old rwfs (due to /etc being mounted as
		# an overlay); this is redundant and will interfere with fixing
		# LFP uid/gids when restoring the files.
		if [ "$rootdevid" ]; then
			find $backup_files | xargs stat -c '%n %d' | while read -r f filedevid; do
				[ "$filedevid" != "$rootdevid" ] || echo "${f#/}"
			done >>$BCK_IGNORELIST_FILE ||
				error "Failed to add rootfs files to ignore list"
		fi
	fi

	archive_files "$backup_files"
}

convert_to_temp() {
	local f=$2 tmpf self=convert_to_temp

	[ $# -eq 2 ] && [ "$1" ] && [ "$2" ] || error "$self: missing or empty argument"

	if [ -r $f ]; then
		tmpf=$(mktemp "$f.new.XXXXXX") ||
			error "$self: Failed to create a temporary file for '$f'"
		cp $f $tmpf || error "$self: Failed to copy '$f' to '$tmpf'"
		fsync $tmpf || error "$self: Failed to sync '$tmpf'"
		eval $1=\$tmpf
	else
		warning "$self: File '$f' is missing or insufficient permissions"
	fi
}

safe_merge() {
	# $1: Primary file in the merge
	# $2: Secondary file in the merge
	# $3: Function that will perform the actual merge
	# $@: Arguments for the function specified at $3
	# ouput: Merged result in the file specified at $1

	[ "$1" ] && [ "$2" ] && [ "$3" ] ||
		error "safe_merge: invalid arguments"

	local f=$1 fun=$3 fun_args self=safe_merge tmpf=$2 tmpmf

	shift 3

	if [ -r $f ] && [ -r $tmpf ]; then
		tmpmf=$(mktemp "$f.mrg.XXXXXX") ||
			error "$self: Failed to create a temporary merge file"

		$fun $* $tmpmf ||
			error "$self: Failed to call function '$fun' with arguments '$fun_args'"

		fsynced_write_or_cleanup $tmpmf $f
		#mv $tmpmf $f || error "$self: Failed to move file '$tmpmf' to '$f'"
		#sync || warning "$self: Failed to sync the filesystem"

		chmod 644 $f || error "$self: Failed to set permissions 644 to '$f'"
		rm $tmpf || warning "$self: Failed to remove file '$tmpf'"
	else
		error "$self: Failed to merge '$f' and '$tmpf', missing one of the files"
		mv $tmpf $f || error "$self: Failed to restore '$f'"
	fi
}

do_pwconv() {
	local name pass rest pf=/etc/passwd sf=/etc/shadow tmpsf=/etc/shadow.conv IFS=:

	rm -f $tmpsf
	while read -r name pass rest; do
		[ "$pass" = x ] || echo "$name:$pass:10231:0:99999:7:::"
	done <$pf >$tmpsf

	# Install the new shadow file and clear all passwords from /etc/passwd.
	fsynced_write_or_cleanup $tmpsf $sf
	sed -i -e 's/^\([^:]*\):[^:]*/\1:x/' $pf
	fsync $pf
}

perform_lfp_to_cvp_upgrade_restore() {
	local _f=perform_lfp_to_cvp_upgrade_restore f local_backup_files mergedfile
	local pf=/etc/passwd gf=/etc/group sf=/etc/shadow
	local tmppf tmpgf tmpsf
	local tmpdir etc_filelist common_filelist extra_filelist
	local common_permissions extra_permissions iv_permissions
	local cvp_passwd cvp_group perms usr grp mounted_iv=no

	tmpdir=$(mktemp -d /tmp/tmp.fsupgrade.XXXXXX) ||
		error "Could not create temporary directory"
	etc_filelist=$tmpdir/etc_filelist
	extra_filelist=$tmpdir/extra_filelist
	common_filelist=$tmpdir/common_filelist
	common_permissions=$tmpdir/common_permissions
	extra_permissions=$tmpdir/extra_permissions
	iv_permissions=$tmpdir/iv_permissions

	convert_to_temp tmppf $pf
	convert_to_temp tmpgf $gf
	convert_to_temp tmpsf $sf

	local_backup_files=$(tar tf $BCK_ARCHIVE) ||
		error "Could not read file list from backup archive"

	# tar may emit a trailing slash, whereas find won't.
	local_backup_files=$(echo "$local_backup_files" | sed -e 's|/$||')

	# Find which files are only in the backup and which files are common to
	# both. The files only present in the backup will have old LFP uids and
	# gids which will need to be changed after extraction.
	find /etc | sed -e 's,^/,,' >$etc_filelist
	# Having no files in common means we're already in serious trouble
	# (e.g. /etc/passwd being missing from one of the systems). There being
	# no extra files (files from the old system that don't exist on the
	# new) is suspicious, but shouldn't fatally break anything.
	printf %s "$local_backup_files" | grep -Fxf $etc_filelist >$common_filelist ||
		error "System and backup archive have no files in common"
	printf %s "$local_backup_files" | grep -Fxvf $etc_filelist >$extra_filelist ||
		warning "No extra files from old system in backup archive"

	while read f; do
		# The permissions of a symbolic link itself don't matter and we
		# assume that the permissions of the target are, or will be,
		# correct. This silences errors from chown later, when trying
		# to modify the permissions of files on the rootfs. (Which is
		# read-only, so we wouldn't be able to do anything about it
		# anyway.)
		[ ! -h "$f" ] || continue
		stat -c "%n %a %U %G" $f ||
			warning "Saving permissions of file '$f' failed"
	done <$common_filelist >$common_permissions

	tar xf $BCK_ARCHIVE || error "Failed to unpack backup archive"
	rm $BCK_ARCHIVE || warning "Failed to remove backup archive"

	# Save the permissions for the extra files. Note that we do this
	# _after_ extraction, so stat will pick up the old user and group names
	# from the extracted /etc/group and /etc/passwd.
	while read f; do
		[ ! -h "$f" ] || continue
		stat -c "%n %a %U %G" $f ||
			warning "Saving permissions of file '$f' failed"
	done <$extra_filelist >$extra_permissions

	if ! grep -w /usr/local /proc/mounts; then
		mount /usr/local || error "$_f: failed to mount /usr/local"
		mounted_iv=yes
	fi

	find /usr/local | while read -r f; do
		stat -c "%a %U %G %n" "$f" ||
			warning "Saving permissions of file '$f' failed"
	done >$iv_permissions

	cvp_passwd=$(cat $tmppf)
	cvp_group=$(cat $tmpgf)

	# If there are any hashes in the passwd file, assume it is unconverted
	# and generate a corresponding shadow file.
	! grep -q -v '^[^:]*:x:' $pf || do_pwconv

	safe_merge $pf $tmppf pwd_grp_merge passwd $pf $tmppf new
	safe_merge $gf $tmpgf pwd_grp_merge group $gf $tmpgf new
	safe_merge $sf $tmpsf shadow_merge $pf $sf $tmpsf

	# Use the CVP permissions for files and directories common to both.
	while read f; do
		set_owner_and_permissions $f
	done <$common_permissions

	# The extra files get LFP permissions.
	while read f; do
		set_owner_and_permissions $f
	done <$extra_permissions

	! [ -e $IV_ROLLBACK_DIR ] || rm -rf $IV_ROLLBACK_DIR
	mkdir -m 700 $IV_ROLLBACK_DIR || error "Couldn't create $IV_ROLLBACK_DIR"

	# Write the CVP passwd/group files to /usr/local so that ownership of
	# everything there can be fixed again in the event of a rollback. A
	# factory default wiping these out won't matter; in that case there
	# won't be any files there to fix anyway.
	printf %s "$cvp_passwd" >$IV_ROLLBACK_DIR/passwd.tmp
	printf %s "$cvp_group" >$IV_ROLLBACK_DIR/group.tmp
	chmod 400 $IV_ROLLBACK_DIR/passwd.tmp $IV_ROLLBACK_DIR/group.tmp
	fsynced_write_or_cleanup $IV_ROLLBACK_DIR/passwd.tmp $IV_ROLLBACK_DIR/passwd
	fsynced_write_or_cleanup $IV_ROLLBACK_DIR/group.tmp $IV_ROLLBACK_DIR/group

	# We need to be careful about changing permissions in /usr/local. If we
	# handle all files in a loop as with /etc and we're interrupted (e.g.
	# by a power cut), we'll roll back and be stuck with a /usr/local which
	# is partially correct. To undo this, generate a script that rectifies
	# this if we're interrupted. The script is run by a systemd service
	# patched into the old firmware that runs on boot.
	{
		echo "#!/bin/sh"
		while read perms usr grp f; do
			echo "chmod $perms '$f' || STATUS=1"
			echo "chown $usr:$grp '$f' || STATUS=1"
		done <$iv_permissions
		echo 'exit ${STATUS:-0}'
	} >$IV_ROLLBACK_DIR/recover.sh.tmp
	chmod 700 $IV_ROLLBACK_DIR/recover.sh.tmp
	fsynced_write_or_cleanup $IV_ROLLBACK_DIR/recover.sh.tmp $IV_ROLLBACK_DIR/recover.sh

	# Update /usr/local now.
	$IV_ROLLBACK_DIR/recover.sh ||
		warning "Failed to fix ownership of all files in /usr/local"
	sync

	# We're safe; remove the generated script.
	rm -f $IV_ROLLBACK_DIR/recover.sh

	rm -rf $tmpdir
	if [ $mounted_iv = yes ]; then
		umount /usr/local || error "$_f: failed to unmount /usr/local"
	fi
}

perform_cvp_upgrade_restore() {
	local f local_backup_files
	local pf gf sf tmppf tmpgf tmpsf

	local_backup_files=$(cat $BCK_BACKUP_FILES_FILE)

	source_bckf

	# Save ownerships and permissions from the new fimage before restoring
	# the backed up files otherwise.
	# NOTICE: This function have, previously, been failing to get the
	# permissions of the shadow file since the shadow file is moved to a
	# temporary file (see "convert_to_temp $sf"). Correcting this might
	# cause issues since it will ensure the previous permissions.
	rm -f $BCK_PERMISSIONS_FILE
	exec 7>$BCK_PERMISSIONS_FILE
	for f in $local_backup_files; do
		if [ ! -h $f ] && echo $SKIP_NEW_PERMISSIONS | grep -Fqv $f; then
			stat -c "%n %a %U %G" $f >&7 ||
				warning "Saving permissions of file '$f' failed"
		fi
	done
	exec 7>&-

	# Convert the following files in to temporary files with the format
	# "$1.new.XXXXXX"
	pf=/etc/passwd
	convert_to_temp tmppf $pf

	gf=/etc/group
	convert_to_temp tmpgf $gf

	sf=/etc/shadow
	convert_to_temp tmpsf $sf


	err=$(tar xf $BCK_ARCHIVE 2>&1) || warning "Failed to unpack backup archive: $err"
	rm $BCK_ARCHIVE || warning "Failed to remove backup archive"

	safe_merge $pf $tmppf pwd_grp_merge passwd $pf $tmppf old
	safe_merge $gf $tmpgf pwd_grp_merge group $gf $tmpgf old
	safe_merge $sf $tmpsf shadow_merge $pf $sf $tmpsf

	# Restore the new default permissions of the backuped files
	while read f; do
		set_owner_and_permissions $f
	done <$BCK_PERMISSIONS_FILE
}

perform_upgrade_restore() {
	local f jffsid local_backup_files mergedfile oldtype
	local pf gf sf tmppf tmpgf tmpsf

	[ $# -eq 2 ] && [ "$1" ] && [ "$2" ] || error "perform_upgrade_restore: missing argument"
	jffsid=$1
	oldtype=$2

	if [ "$jffsid" = ONVIF_HARD_RESET ]; then
		tar xf $BCK_ARCHIVE || warning "Restore failed"
		rm $BCK_ARCHIVE || warning "Failed to remove backup archive"
		exit 0
	fi

	if [ "$oldtype" = lfp ]; then
		perform_lfp_to_cvp_upgrade_restore
	else
		perform_cvp_upgrade_restore
	fi

	if [ "$jffsid" != SOFT_RESET ]; then
		rm -f $EDP/*
		for f in $ED/*; do
			[ ! -f "$f" ] || rm -f "$f"
		done

		if start_parhand_dyn; then
			info 'Restoring dynamic parameters...'

			if $PHC --nosync upgradedynparams $EDT; then
				info "done"
			else
				warning "restoring dynamic parameters failed!"
			fi
			if $PHC sync; then
				info "Synced restored dynamic parameters to file"
			else
				warning "Failed to sync restored dynamic parameters to file"
			fi
		else
			warning "Failed to restore dynamic parameters"
		fi
		stop_parhand || warning "Failed to stop parhand(3)"

		rm -rf $EDT
	fi

	if start_parhand; then
		if [ -e $PHTMPBCK ]; then
			info "Restoring temporarily saved parameters... "

			if $PHC --nosync --restore setparams $PHTMPBCK; then
				info "done"
			else
				warning "'$PHC --nosync --restore setparams $PHTMPBCK' failed!"
			fi
			if $PHC sync; then
				info "Synced restored parameters to file"
			else
				warning "Failed to sync restored parameters to file"
			fi
			rm -f $PHTMPBCK || warning "removing '$PHTMPBCK' failed"
		else
			warning "Saved parameters are lost"
		fi
	else
		warning "Failed to restore saved parameters"
	fi

	if [ -d $POST_UPGRADE_SCRIPTS_LOC ]; then
		for file in $POST_UPGRADE_SCRIPTS_LOC/*; do
			if [ -f "$file" ] && [ -x "$file" ]; then
				info "Post upgrade script $file ..."
				JFFSID=$jffsid $file && info "done" ||
					warning "Failed to execute post upgrade script '$file'"
			fi
		done
	fi

	if [ "$jffsid" = SOFT_RESET ]; then
		if [ -d $POST_UPGRADE_RESET_SCRIPTS_LOC ]; then
			for file in $POST_UPGRADE_RESET_SCRIPTS_LOC/*; do
				[ ! -f "$file" ] || [ ! -x "$file" ] ||	$file
			done
		fi
	fi

	stop_parhand || warning "Failed to stop parhand(4)"

	df
}

perform_embsdk_restore() {
	local rc_iv=/sbin/rc.ivrestore

	case $1 in
		ONVIF_HARD_RESET|SOFT_RESET)
			;
		'')
			error "perform_embsdk_restore: jffsid is not set"
			;
		*)
			# Make sure that any uploaded applications are restored
			[ ! -x $rc_iv ] || $rc_iv || warning "$rc_iv failed!"
			;
	esac
}

[ $# -ge 2 ] || error "fsupgrade: missing argument"

jffsid=$1
rwfsold=$3
res=0

case $2 in
	backup)
		info "fsupgrade: BACKUP START '$jffsid'"
		perform_upgrade_backup $jffsid cvp
		res=$?
		info "fsupgrade: BACKUP STOP '$jffsid'"
		;
	backuplfp)
		info "fsupgrade: BACKUP START '$jffsid'"
		perform_upgrade_backup $jffsid lfp
		res=$?
		info "fsupgrade: BACKUP STOP '$jffsid'"
		;
	restore)
		info "fsupgrade: RESTORE START '$jffsid'"
		perform_upgrade_restore $jffsid cvp
		ret=$?
		perform_embsdk_restore $jffsid
		res=$(($ret + $?))
		info "fsupgrade: RESTORE STOP '$jffsid'"
		;
	restorelfp)
		info "fsupgrade: RESTORE START '$jffsid'"
		perform_upgrade_restore $jffsid lfp
		ret=$?
		perform_embsdk_restore $jffsid
		res=$(($ret + $?))
		info "fsupgrade: RESTORE STOP '$jffsid'"
		;
	'')
		error "Missing command"
		;
esac

exit $res