#!/bin/bash #Copyright 2004 William Stearns #Released under the GPL #V0.4.2 #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 -P "$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