:
#
# movehome	-- Move (rename) a user's home directory
#
#	@(#) movehome.sh 22.1 90/04/18 
#
# Copyright (c) 1989, 1990, The Santa Cruz Operation, Inc.,
# and SecureWare, Inc.  All rights reserved.
#
# This Module contains Proprietary Information of the Santa Cruz
# Operation, Inc., and SecureWare, Inc., and should be treated
# as Confidential. 
#
# Usage: movehome -r user homedir oldhome nbfree nffree
#        movehome -c user homedir oldhome nbfree nffree
#
# where -r means simple rename, and -c means rename-by-copy
# (if on different filesystems, otherwise rename-by-linking).
#

PATH=/bin:/usr/bin
umask 077

# Tunable parameters:
#	Bslop	- Number of additional free blocks over and above amount used
#	Fslop	- Number of additional free inodes over and above amount used
# These are used only in the rename-by-copy case (-c, different filesystems).
Bslop=1000
Fslop=100

readonly Bslop Fslop
set -u

#
# usage		-- Print the "usage" message
#
usage() {
	[ $# -ge 1 ] && echo "$@" >&2
	echo "Usage: $0 -rc user homedir oldhome nbfree nffree" >&2
	exit 2
}
readonly usage

#
# fatal		-- Print an error message and exit with a non-0 (failure) code
#
fatal() {
	echo "$@" >&2
	status=1
	exit 1
}
readonly fatal

#
# isnumber	-- Check that a string is an (unsigned) decimal integer
#
# Padding blanks (spaces or tabs) are Ok; if the argument isn't Ok,
# then a fatal error results.
#
isnumber() {
	expr "X$1" : 'X[	 ]*[0-9][0-9]*[	 ]*$' >/dev/null ||
		fatal "Bad number of $2: $1"
}
readonly isnumber

#
# exists	-- Does the argument exist (any file type, any mode)?
#
exists() {
	ls -d "$1" >/dev/null 2>/dev/null
}
readonly exists

#
# prompt	-- Ask a question and wait for a "Yes" or "No" answer
#
prompt() {
	while	
		echo "$*: (y/n) \c"
		read _answer
	do	
		case $_answer in
		[yY]*)	return 0
			;;
		[nN]*)	return 1
			;;
		[qQ]*)	exit 2
			;;
		*)	echo "Please enter either 'y' or 'n'."
			;;
		esac
	done
	return 1
}
readonly prompt

#
# Check the number of arguments, and save them.
#
case $# in
6)	cmd=$1
	user=$2
	newhome=$3
	oldhome=$4
	nbfree=$5
	nffree=$6
	;;
*)	usage
	;;
esac

#
# Check the first argument (the command option):  Must be -c or -r.
#
case $cmd in
-[rc])	: Ok
	;;
*)	usage "Unknown command: $cmd"
	;;
esac

#
# Check the second argument (the user's name)
#
# N.b.	We use awk(C) rather than the more obvious grep(C) so that
#	absurd (contains ":") or problematic (contains ".") $user's
#	don't foul us up.
#
awk -F: '{ if (NF >= 7 && ($1 "") == (UserName "")) { found = 1; exit } }
     END { exit !found }' UserName=$user /etc/passwd ||
	fatal "Non-existent user: $user"

#
# Check the third argument (the new home directory):  It must be an
# absolute path that must not exist, but its parent must exist and
# be a directory we can write in.
#
case $newhome in
/*)	newdotdot=`dirname "$newhome"`
	exists "$newhome" &&
		fatal "New home directory already exists: $newhome"
	[ -d "$newdotdot" -a -w "$newdotdot" ] ||
		fatal "Cannot write in parent of new home directory: $newdotdot"
	set -- `/etc/devnm "$newdotdot"` ||
		fatal "Cannot determine filesystem containing new home: $newhome"
	newdev=$1
	;;
*)	fatal "New home directory path not absolute: $newhome"
	;;
esac

#
# Check the fourth argument (the old home directory):  This must be an
# absolute path that exists and is a directory we can both read and search,
# and it's parent (which must be a directory) must be writable.
#
case $oldhome in
/*)	olddotdot=`dirname "$oldhome"`
	[ -d "$oldhome" -a -r "$oldhome" -a -x "$oldhome" ] ||
		fatal "Old home does not exist or is not a directory: $oldhome"
	[ -d "$olddotdot" -a -w "$olddotdot" ] ||
		fatal "Cannot write in parent of old home directory: $olddotdot"
	set -- `/etc/devnm "$oldhome"` ||
		fatal "Cannot determine filesystem containing old home: $oldhome"
	olddev=$1
	;;
*)	fatal "Old home directory path not absolute: $oldhome"
	;;
esac

#
# Check the fifth (number of available free blocks) and sixth (free inodes)
# arguments:  These must be non-negative decimal integers.
#
isnumber "$nbfree" "free blocks"
isnumber "$nffree" "free inodes"

#
#  All the arguments seem to be Ok.  Now actually do the work.
#
#  First, step up a trap to delete our temporary file on exit
#  (especially if we are killed).  The rm in this trap fouls up
#  the exit code, so be sure to set $status to the real exit code
#  before exiting.
#
tmpfile=/tmp/mvhome$$
status=99

trap 'rm -f $tmpfile; exit $status' 0
trap 'rm -f $tmpfile; status=3; exit 3' 1 2 3 15

#
#  Second, check to see if a process belonging to this user
#  is on the system.  If so, don't move the home directory.
#  We assume that we have the "mem" subsystem authorisation.
#  Checking this (in a shell script) is a Real Pain (tm).
#
ps -u "$user" > $tmpfile ||
	fatal "Cannot determine if any of the files may be active"
set -- `wc -l $tmpfile`
isnumber "$1" "number of $user processes"
[ $1 -ne 1 ] && {
	cat $tmpfile
	echo
	fatal "Should not move home directory as it may currently be in use"
}

#
# Third, if this is a -r (simple rename), just use the /etc/rename command.
#
case $cmd in
-r)	/etc/rename "$oldhome" "$newhome" ||
		fatal "Cannot rename home directory for user: $user"
	sync
	echo "The home directory for $user has been moved to $newhome"
	status=0
	exit 0
	;;
-c)	: Ok
	;;
*)	fatal "Internal error $0: cmd = $cmd"
	;;
esac

#
#  If we got this far, this must be the -c case: Either copy or link
#  the files into a brand new directory.  We copy rather than link if,
#  and only if, the new and old home directories are on different
#  (mounted) filsystems.
#
#  So figure out which case this is, and explain to the administrator
#  just what is about to happen.  Then ask whether or not to proceed.
#
#  In the copy case (different filesystems), check to see if there will
#  be enough room (that is, enough free blocks and inodes).  If not,
#  then don't even bother asking, since the copy apriori won't work!
#
#  We ought to check the inodes in the case of the link case (against
#  the number of directories that will be made -- directories are not
#  linked), but this is a rarer failure and a pain besides, so we don't.
#

if [ "X$olddev" = "X$newdev" ]; then
	same_fs=YES

	echo "The new and old home directories for user $user
are on the same mounted filesystem.  The files in the old home
directory will be moved to the new home by linking them into the
about-to-be-created new home directory (rather than just changing
the name of the home directory itself).

This linking may take a long time, depending on how many files
the user has (and how many directories must be created).
"
	prompt "Proceed (link the old home to the new home)?" || {
		status=1
		exit 1
	}

	echo "
Generating list of files to link ... \c"

else
	same_fs=NO

	echo "The new and old home directories for user $user
are on different mounted filesystems.  This means that all
of the files in the old home must be copied to the new home.
The permissions, owner, and modification date of all the files
will be preserved, intact.

This copying may take a long time, depending on how many files
the user has (and how big they are).

Calculating the number of files that must be copied ...\c"

	du -ar "$oldhome" > $tmpfile ||
		fatal "Cannot determine the current disc usage (blocks) for user: $user"
	set -- `wc -l $tmpfile`
	nfiles=$1
	isnumber "$nfiles" "files in old home"

	set -- `tail -1 $tmpfile` ||
		fatal "Cannot determine the current disc usage (inodes) for user: $user"
	nblock=$1
	isnumber "$nblock" "blocks used in old home"

	echo "... done.
\007
There are $nfiles files, totaling $nblock blocks, to copy.
(The filesystem that is to contain the new home directory
can hold $nffree additional files, totaling $nbfree blocks.)
"
	isnumber "$Bslop" "extra free blocks (Bslop)"
	[ `expr $nblock + $Bslop` -ge $nbfree ] &&
		fatal "Not enough free space to move home directory for user: $user"

	isnumber "$Bslop" "extra free inodes (Fslop)"
	[ `expr $nfiles + $Fslop` -ge $nffree ] &&
		fatal "Not enough free inodes to move home directory for user: $user"

	echo This copying make take a long time.

	prompt "Proceed (copy the old home to the new home)?" || {
		status=1
		exit 1
	}
	echo "
Generating list of files to copy ... \c"

fi

#
#  Now actually do the copy/link, using the classic `find | cpio'
#  pipe.  (Actually, we use a temporary file, so that we can detect
#  find errors more reliably.)  This classic pipe is not, however,
#  completely reliable.  E.g., filenames containing newlines will
#  cause it to misbehave, and the misbehaviour may not be obvious.
#
#  We can't, unfortunately, just use the du-generated list of names
#  because only one instance of each linked file is listed (and
#  besides, we only generate that list if the filesystems aren't
#  the same).
#
#  The check to make sure we generated a complete list of files
#  to move is not 100% reliable.
#
#  For all of the above reasons, plus general paranoia, we ask the
#  administrator (and confirm a "Yes" answer) whether or not they
#  really want to remove the old home directory.  Hopefully the
#  administrator is smart enough to say "No" if an error message
#  appeared!  Ideally, the files would be backed up to tape first,
#  but that is near-impossible to arrange from this script.
#

cd "$oldhome" ||
	fatal "Cannot chdir to old home directory: $oldhome"

find . -depth -print > $tmpfile ||
	fatal "Cannot generate list of files to move for user: $user"

[ "X`tail -1 $tmpfile`" != X. ] &&
	fatal "Insufficient disc space to generate list of files to move"

mkdir -m 700 "$newhome" ||
	fatal "Cannot make new home directory: $newhome"

umask 0
case $same_fs in
NO)	echo copying files
	cpio -pdVm  "$newhome" < $tmpfile || {
		rm -rf "$newhome"
		fatal "Could not copy all of the files, so new home directory removed"
	}
	sync
	echo "\007
The home directory for $user has been copied from $oldhome
to $newhome (which is on a different mounted filesystem).\007
"
	;;
YES)	echo linking files
	cpio -pdlVm "$newhome" < $tmpfile || {
		rm -rf "$newhome"
		fatal "Could not link all of the files, so new home directory removed"
	}
	sync
	echo "\007
The files in the old home directory for $user, $oldhome,
have been linked to the new home directory $newhome.\007
"
	;;
*)	fatal "Internal error $0: same_fs = $same_fs"
	;;
esac

cd /

prompt "Do you wish to remove the old home, $oldhome ?" && {
	prompt "Are you sure you wish to remove $oldhome ?" && {
		rm -rf "$oldhome"
		echo "\007"
	}
}
if [ -d "$oldhome" ]; then
	echo "WARNING - The old home directory was not completely removed: $oldhome"
else
	echo The old home directory has been completely removed.
fi
status=0
exit 0
