#!/bin/bash
#Copyright 2004 William Stearns <wstearns@pobox.com>
#Released under the GPL
#V0.4.1

#Despite this app's best efforts, it's still a poster child for race
#conditions.  It really should only be run on a system in single user
#mode, or at least on files you _know_ won't be touched.

requireutil () {
	while [ -n "$1" ]; do
		if ! type -path "$1" >/dev/null 2>/dev/null ; then
			echo Missing utility "$1". Please install it. >&2
			return 1	#False, app is not available.
		fi
		shift
	done
	return 0	#True, app is there.
} #End of requireutil

TFile () {
	$SUDO mktemp -q "$BaseFile.XXXXXX"
	if [ $? -ne 0 ]; then
		echo Unable to make temporary file, exiting. >&2
		echo
		exit 1
	fi
}

fail () {
	while [ -n "$1" ]; do
		echo "$1" >&2
		shift
	done
	echo "Exiting." >&2
	echo
	exit 1
}

Step () {
	if [ "$SingleStep" = "true" ]; then
		echo -n "$*, Press Enter to proceed:  "
		read JUNK
	fi
}

CleanupAndExit () {
	echo
	if [ -n "$1" ]; then
		while [ -n "$1" ]; do
			echo "$1" >&2
			shift
		done
	else
		echo "Exiting because ctrl-c pressed."
	fi
	echo "Cleaning up; removing immutable on original file."
	Step 'About to perform cleanup on exit'
	$SUDO chattr -i "$BaseFile"

	if [ -n "$Pass" ]; then
		echo -n 'Deleting temp files: '
		for DeletePass in `seq 1 $Pass` ; do
			if [ -e "${Try[$DeletePass]}" ]; then
				echo -n "$DeletePass "
				$SUDO rm -f "${Try[$DeletePass]}"
			fi
		done
		echo
	fi
	echo Exiting.
	exit 1
}

MaxTries=6

if [ $EUID -ne 0 ]; then
	requireutil sudo || exit 1
	SUDO=`which sudo`
fi

requireutil awk chattr chmod chown dd df diff filefrag grep lsattr lsof ls md5sum mktemp mv rm sed seq touch || exit 1

if [ "z$1" = "z-s" -o "z$1" = "z--singlestep" ]; then
	SingleStep='true'
	shift
fi

BaseFile="$1"

if [ -z "$BaseFile" -o -n "$2" ]; then
	fail 'Usage:' "$0 [-s|--singlestep] File_to_defragment" 'You can only specify a single file, and it must not be in use.' '-s or --singlestep pauses at each step to allow you to inspect progress'
fi

echo "Considering $BaseFile"

Step 'Checking to see if BaseFile exists and is a file'
if [ ! -f "$BaseFile" -o -L "$BaseFile" ]; then
	fail "BaseFile is not a file."
fi

Step 'About to check for open file'
if LsofOut="`$SUDO lsof \"$BaseFile\" 2>&1`" ; then
	fail "$BaseFile is being held open:" "$LsofOut" 'Unable to continue on this file.'
fi

Step 'File closed, good, about to check for immutable flag'
if [ -n "`$SUDO lsattr \"$BaseFile\" | awk '{print $1}' | grep 'i'`" ]; then
	fail "$BaseFile is already immutable - was there a previous aborted attempt?" 'Unable to continue on this file.'
fi

Step 'File is not immutable, good, about to set immutable'
#Here's the first place where we need to clean anything up
trap CleanupAndExit SIGINT		#Ctrl-C generates this
$SUDO chattr +i "$BaseFile"		#FIXME - check return code to see if we succeeded or failed instead of running lsattr
if [ -z "`$SUDO lsattr \"$BaseFile\" | awk '{print $1}' | grep 'i'`" ]; then
	fail "Unable to make $BaseFile immutable." 'Unable to continue on this file.'
fi

Step 'File is immutable, good, checking again for open file'
if LsofOut="`$SUDO lsof \"$BaseFile\" 2>&1`" ; then
	CleanupAndExit "$BaseFile is open after being made immutable:" "$LsofOut" 'Unable to continue on this file.'
fi

Step 'Counting extents'
ExtentInfo=`$SUDO filefrag "$BaseFile" | sed -e 's/^.*: \([0-9]*\) extents* found, perfection would be \([0-9]*\) extents*$/\1 \2/' -e 's/^.*: \([0-9]*\) extents* found$/\1 1/'`
CurrentExtents=${ExtentInfo%% *}
PerfectExtents=${ExtentInfo##* }

echo -n "Current $CurrentExtents Perfect $PerfectExtents: "
if [ $CurrentExtents -eq $PerfectExtents ]; then
	$SUDO chattr -i "$BaseFile"
	echo 'cannot do better, exiting.'
	echo
	exit 0
elif [ $CurrentExtents -lt $PerfectExtents ]; then
	CleanupAndExit "Hmmm, $Basefile claimed to have fewer extents than perfection, something is wrong."
else
	echo "fragmented, trying to defrag."

	Step 'Checking for free space'
	FileMegs=`$SUDO ls -s --block-size=1048576 "$BaseFile" | awk '{print $1}'`
	FreeSpace=`$SUDO df -m "$BaseFile" | grep -v '^Filesystem' | awk '{print $4}'`

	if [ $[ $FileMegs * $MaxTries + 20 ] -gt $FreeSpace ]; then
		CleanupAndExit "$BaseFile is ${FileMegs}M large, $FreeSpace free on that filesystem" "Making $MaxTries copies of $BaseFile would not leave 20M free."
	fi

	Step 'Attempting copies'
	BestPass=0
	BestExtents=$CurrentExtents
	declare -a Try TryExtents
	echo -n "Num extents in copies: "
	for Pass in `seq 1 $MaxTries` ; do
		Try[$Pass]="`TFile`"
		if $SUDO dd if="$BaseFile" of="${Try[$Pass]}" 2>/dev/null  ; then				# | grep -v 'records [io][nu]'   doesn't work
			TryExtents[$Pass]=`$SUDO filefrag "${Try[$Pass]}" | sed -e 's/^.*: //' -e 's/ extent.*//'`
			echo -n "${TryExtents[$Pass]} "
			if [ ${TryExtents[$Pass]} -eq $PerfectExtents ]; then
				echo "(Reached perfection on pass $Pass, no more copies)"
				BestPass=$Pass
				BestExtents=${TryExtents[$Pass]}
				break
			elif [ ${TryExtents[$Pass]} -lt $BestExtents ]; then
				BestPass=$Pass
				BestExtents=${TryExtents[$Pass]}
			fi
		else
			echo ; Step 'Copy failed, about to delete copies and remove immutable flag from orig'
			CleanupAndExit 'Exiting because copy failed.'
		fi
		if [ $Pass -eq $MaxTries ]; then
			echo
		fi
	done

	Step 'Copies succeeded, about to remove all but the best'
	for Pass in `seq 1 $Pass` ; do
		if [ $Pass -ne $BestPass ]; then
			$SUDO rm -f "${Try[$Pass]}"
		fi
	done

	if [ $BestPass = 0 ]; then
		echo "Didn't do better than the original file, exiting."
		if $SUDO chattr -i "$BaseFile" ; then
			echo
			exit 0
		else
			fail "Could not remove immutable flag from $BaseFile on the way out, please fix"
		fi
	fi

	echo "Best pass was pass number $BestPass with ${TryExtents[$BestPass]} extents."

	Step 'About to compare orig and best pass'
	$SUDO md5sum "$BaseFile" "${Try[$BestPass]}"
	if ! $SUDO diff -q "$BaseFile" "${Try[$BestPass]}" >/dev/null ; then
		Step 'File compare failed, about to delete best pass'
		$SUDO rm -f "${Try[$BestPass]}"
		fail "Warning - $BaseFile and ${Try[$BestPass]} differ"
	fi

	Step 'About to set timestamp, mode, and ownership of best pass'
	$SUDO chmod --reference="$BaseFile" "${Try[$BestPass]}" || CleanupAndExit "chmod ${Try[$BestPass]} failed"
	$SUDO chown --reference="$BaseFile" "${Try[$BestPass]}" || CleanupAndExit "chown ${Try[$BestPass]} failed"
	$SUDO touch --reference="$BaseFile" "${Try[$BestPass]}" || CleanupAndExit "touch ${Try[$BestPass]} failed"


	Step 'Checking for open best pass file'
	if LsofOut="`lsof \"${Try[$BestPass]}\" 2>&1`" ; then
		Step 'Best pass was opened, aborting, about to delete it'
		CleanupAndExit "Someone opened ${Try[$BestPass]}:" "$LsofOut" "Unable to continue on $BaseFile."
	fi
	Step 'Checking for open Basefile'
	if LsofOut="`lsof \"$BaseFile\" 2>&1`" ; then
		Step 'Basefile was opened, aborting, about to delete bast pass'
		CleanupAndExit "Someone opened $BaseFile:" "$LsofOut" 'Unable to continue on this file.'
	fi

#To completely replace the original with no backup
	echo "Succeeded, renaming ${Try[$BestPass]} to $BaseFile"
	Step 'About to replace Basefile with Best pass'
	trap 'echo Replacing original file, bad time to interrupt.' SIGINT		#Ctrl-C generates this
	if $SUDO chattr -i "$BaseFile" ; then
		if $SUDO rm "$BaseFile" ; then
			if $SUDO mv "${Try[$BestPass]}" "$BaseFile" ; then
				:
			else
				fail "Failed to move ${Try[$BestPass]} to $BaseFile, please move by hand."
			fi
		else
			fail "Failed to delete $Basefile, please fix and clean up ${Try[$BestPass]}"
		fi
	else
		fail "Failed to remove immutable flag from $BaseFile, please fix and clean up ${Try[$BestPass]}"
	fi

#For manual replacement
	#echo "Succeeded, please rename ${Try[$BestPass]} to $BaseFile."

#To keep the original as a backup (untested)
	#echo "Succeeded, renaming ${Try[$BestPass]} to $BaseFile and keeping a backup of the original."
	#mv "$BaseFile" "`TFile`"
	#mv "${Try[$BestPass]}" "$BaseFile"
fi

echo