"Fossies" - the Fresh Open Source Software archive 
#!/bin/bash
#==============================================================================
# R.A.L.F. (Recover A Lost File) (c) 2008 by Itzchak Rehberg & IzzySoft
# Syntax: $0 <filename>
#------------------------------------------------------------------------------
# Ralf is just a shell script that automates the manual steps required to
# restore a (accidentally) deleted file from a modern ext2/ext3 file system.
# For other *nix file systems there may be easier solutions - though Rolf
# should also be able to handle them, as long as they work with iNodes.
#------------------------------------------------------------------------------
# Requires: photorec OR foremost
# Requires: sleuthkit (at least fsstat, dls and fls from that package)
#==============================================================================
# $Id: ralf 41 2008-07-20 17:35:30Z izzy $
#==========================================================[ Configuration ]===
# for explanation of the options, see ext3undelrc
#-------------------------------------------------[ File Systems and Paths ]---
TMPDIR=/tmp
MNTFILE=${TMPDIR}/undel_FIFO
TMP=$TMPDIR/undel_FIFO2$$
RESTDIR=recover
#--------------------------------------------------------[ runtime options ]---
SILENT=0
DEBUG=0
#-------------------------------------------------------[ foremost options ]---
FILESPEC="everything"
USETS=0
USEQUICK=1
USEQUIET=1
READONLY=0
VERBOSE=1
ALLHEAD=1
#-------------------------------------------------------[ photorec options ]---
WHOLESPACE=0
RECOPROG=photorec
#-----------------------------------------------[ Read configuration files ]---
[ -e /etc/ext3undel/ext3undelrc ] && . /etc/ext3undel/ext3undelrc
[ -e $HOME/.ext3undel/ext3undelrc ] && . $HOME/.ext3undel/ext3undelrc
#===================================================[ Helper / SubRoutines ]===
shopt -s extglob
trap "cleanUp ; exit 2" 2 3 15
#-----------------------------------[ Check whether we are running as root ]---
function checkRoot() {
local uid=`id -u`
[ $uid -gt 0 ] && {
echo "This script must be run as root (or at least with sudo)."
exit 1
}
}
#-------------------------------[ Make sure needed binaries exist in $PATH ]---
function checkBins() {
local fm=1
[ "$RECOPROG" = "photorec" -a -z "`which photorec`" ] && RECOPROG=foremost
[ "$RECOPROG" = "foremost" -a -z "`which foremost`" ] && {
echo
echo "Neither the 'photorec' nor the 'foremost' executable could be found"
echo "in your \$PATH - but we need one of them. They usually ship in a"
echo "package with the same name (i.e. 'photorec' or 'foremost')."
echo
fm=0
}
[ -z "`which fls`" ] && {
echo
echo "Could not find the 'fls' executable in your \$PATH. This is part of the"
echo "sleuthkit package."
echo
fm=0
}
[ -z "`which fsstat`" ] && {
echo
echo "Could not find the 'fsstat' executable in your \$PATH. This is part of the"
echo "sleuthkit package."
echo
fm=0
}
[ -z "`which dls`" ] && {
echo
echo "Could not find the 'dls' executable in your \$PATH. This is part of the"
echo "sleuthkit package."
echo
fm=0
}
[ $fm -ne 1 ] && {
echo
echo "One or more of the essential tools required to recover your file cannot"
echo "be found. Please make sure you have them installed, and they can be found"
echo "in your \$PATH."
echo
exit 2
}
}
#--------------------------------------------------[ Display help and exit ]---
function syntax() {
echo
echo "Syntax:"
echo " ralf <filename> [options]"
echo "where <filename> specifies the name of the file to restore - either"
echo "relative to the current working directory, or with full (absolute) path."
echo
echo "Available options:"
echo "+d / --debug Enable Debug Output"
echo "-d / --nodebug Disable Debug Output"
echo "+s / --silent Make output less verbose"
echo "-s / --nosilent Display progress messages"
echo "--freespace Recover from free space only"
echo "--wholespace Recover from whole space"
echo
exit 0
}
#-------------------------------[ Remove leading and trailing white spaces ]---
function trim() {
echo "$1"|sed 's/^\s*//;s/\s*$//'
}
#---------------------------------------------[ Read a number (user input) ]---
# $1: Prompt message
# opt $2: Min
# opt $3: Max
function readnum() {
read -p "$1" rd
local tn=`echo $rd|sed 's/[0-9]//g'`
local min=$2
local max=$3
[ -n "$tn" ] && {
echo "Please enter only digits!"
readnum "$1" "$2" "$3"
}
[ -z "$min" ] && min=0
[ -z "$max" ] && max=99999999
[ $rd -lt $min -o $rd -gt $max ] && {
echo "Please enter a number between $2 and $3!"
readnum "$1" "$2" "$3"
}
}
#---------------------------------------------------------[ Read Y/N input ]---
function readyn() {
read -n 1 rd
echo
rd=`echo $rd|tr [:upper:] [:lower:]`
if [ "$rd" = "y" -o "$rd" = "j" ]; then
rd=1
elif [ "$rd" = "n" ]; then
rd=0
else
echo -n "Please enter 'y' for 'yes', 'n' for 'no' - or press Ctrl-C to abort: "
readyn
fi
}
#-------------------------------------------[ Select Destination directory ]---
function selDest() {
local rd
echo
echo "Please select the file system to store the recovered files to."
echo "(This must be on a different device than you restore from)"
echo
printf "%2s| %-7s| %-15s| %-32s| %-16s\n" "ID" "Type" "Device" "MountPoint" "Size"
echo "--+--------+----------------+---------------------------------+----------------"
typeset -i i=0
local uLINE=""
local SKIP=1
mkfifo $MNTFILE
df -h -l -T >$MNTFILE &
while read line; do
[ $SKIP -eq 1 ] && { # skip the header
SKIP=0
continue
}
if [ -n "$(trim "$uLINE")" ]; then # wrapped line continued
line="${uLINE} $line"
else
[ -z "$(trim "`echo $line|awk '{print $7}'`" )" ] && { # line wrapped?
uLINE="$line"
continue
}
fi
uLINE=""
if [[ "$line" == /dev* ]]; then # skip pseudo filesystems
i+=1
devs[$i]=`echo $line|awk '{print $1}'`
[ "${devs[$i]}" != "$DEV" ] && echo "$i $line"|awk '{printf "%2d| %-7s| %-15s| %-32s| %-16s\n", $1, $3, $2, $8, $4 }'
stores[$i]=`echo $line|awk '{print $7}'`
fi
done<$MNTFILE
rm -f $MNTFILE
readnum "Destination ID: " 1 $i
if [ "${devs[$rd]}" = "$DEV" ]; then
echo
echo "Invalid selection: Source and Target file systems must not be identical!"
echo "This way you may destroy your lost data permanently. Are you sure to"
echo -n "proceed (y/n)? "
readyn
echo
if [ $rd -eq 1 ]; then
recoTo=${stores[$rd]}/$RESTDIR
else
li1 "Aborting on user request."
cleanUp
exit 0
fi
else
STORETO=${stores[$rd]}
fi
echo
}
#--------------------------------------------------------[ Output progress ]---
function li1() {
[ $SILENT -eq 0 ] && echo "* $1"
}
#-----------------------------------------------[ Output debug information ]---
function debugMsg() {
[ $DEBUG -gt 0 ] && echo "# debug: $1"
}
#--------------------------------------------------------[ Verify filename ]---
function fileName() {
if [[ "$1" == /* ]]; then
uFILE=$1
else
uFILE="`pwd`/$1"
fi
li1 "FileName set to '$uFILE'"
}
#-----------------------------------------------[ Select Source MountPoint ]---
function selSrc() {
local mid
typeset -i i=0
mounts[0]=""
local uLINE=""
local SKIP=1
mkfifo $TMP
mkfifo $MNTFILE
df -h -l -T >$MNTFILE &
while read line; do
[ $SKIP -eq 1 ] && { # skip the header
SKIP=0
continue
}
if [ -n "$(trim "$uLINE")" ]; then # wrapped line continued
line="${uLINE} $line"
else
[ -z "$(trim "`echo $line|awk '{print $7}'`" )" ] && { # line wrapped?
uLINE="$line"
continue
}
fi
uLINE=""
if [[ "$line" == /dev* ]]; then # skip pseudo filesystems
echo $line|awk '{print $7}'>>$TMP &
uDEV[$i]=`echo $line|awk '{print $1}'`
uMOUNT[$i]=`echo $line|awk '{print $7}'`
uFSTYPE[$i]=`echo $line|awk '{print $2}'`
i+=1
fi
done<$MNTFILE
sort -n -r <$TMP>$MNTFILE &
while read line; do
if [[ "$uFILE" == $line* ]]; then
i=0
while (( i < ${#uMOUNT[*]} )); do
if [[ "$line" = "${uMOUNT[$i]}" ]]; then
li1 "Selected file should be on device ${uDEV[$i]}, mounted to ${uMOUNT[$i]} (${uFSTYPE[$i]})."
DEV=${uDEV[$i]}
MNT=${uMOUNT[$i]}
FSTYPE=${uFSTYPE[$i]}
break 2
fi
i+=1
done
fi
done <$MNTFILE
rm -f $TMP
rm -f $MNTFILE
li1 "Evaluated '$MNT' as corresponding mount point, using '$FSTYPE' file system."
[ "$FSTYPE" != "ext2" -a "$FSTYPE" != "ext3" ] && echo "! WARNING: This is not an ext2/ext3 file system, so our algorithm may fail!"
}
#--------------------------------------------[ Splitup filename for search ]---
function splitFileName() {
if [ "$MNT" = "/" ]; then
SEARCH=${uFILE:1}
else
SEARCH=${uFILE#$MNT/*}
fi
li1 "Setting SearchString relative to mountpoint ('$SEARCH')"
SEARCH=`echo "$SEARCH"|awk {'gsub("*",".*");gsub("?",".{1}");gsub("/","\\\/");print $0}'`
li1 "Translating wildcards to RegExp ('$SEARCH')"
}
#-----------------------------------------------------[ Check for Symlinks ]---
function symlinkCheck() {
local left="$uFILE"
local check=""
while [ -n "$left" ]; do
if [ -n "$right" ]; then
right="${left##*/}/${right}"
else
right="${left##*/}"
fi
left="${left%/*}"
[ -n "$left" ] && check=`readlink $left`
[ -n "$check" ] && {
uFILE="${check}/${right}"
left="$uFILE"
right=""
}
done
li1 "Real filename: '$uFILE'"
}
#---------------------------------------------------[ Get PhotoRec Version ]---
function photoRecVer() {
local TMP="${TMPDIR}/undel_xFIFO.$$"
mkfifo $TMP
photorec --help>$TMP &
while read line; do
local ver=`echo $line|awk '{print $2}'`
break
done<$TMP
pr_maj=${ver%.*}
pr_min=${ver#*.}
pr_min=${pr_min%,*}
[ "$pr_min" != "${pr_min%-*}" ] && let pr_min=${pr_min%-*}-1
rm -f $TMP
}
#--------------------------------------------------[ Restore with PhotoRec ]---
function recoPhotoRec() {
photoRecVer
li1 "Extracting file with PhotoRec v${pr_maj}.${pr_min}"
cmd="photorec /d ${STORETO}/${RESTDIR} /cmd $DUMPFILE partition_i386"
[ "$FSTYPE" = "ext3" -o "$FSTYPE" = "ext2" ] && cmd="${cmd},options,mode_ext2"
if [ $pr_maj -gt 6 -o $pr_maj -eq 6 -a $pr_min -ge 9 ]; then
if [ "$FILESPEC" = "everything" ]; then
cmd="${cmd},fileopt,everything,enable,search"
else
cmd="${cmd},fileopt,everything,disable,$FILESPEC,enable,search"
fi
if [ $WHOLESPACE -eq 0 ]; then
cmd="${cmd},freespace"
else
cmd="${cmd},wholespace"
fi
else
if [ "$FILESPEC" = "everything" ]; then
cmd="${cmd},search"
else
cmd="${cmd},fileopt,$FILESPEC,enable,search"
fi
fi
debugMsg "Executing '$cmd'"
eval $cmd
rc=$?
}
#--------------------------------------------------[ Restore with foremost ]---
function recoForemost() {
li1 "Extracting file with foremost..."
cmd="foremost "
[ $USEQUICK -eq 1 ] && cmd="$cmd -q"
[ $USETS -eq 1 ] && cmd="$cmd -T"
[ $USEQUIET -eq 1 ] && cmd="$cmd -Q"
[ $READONLY -eq 1 ] && cmd="$cmd -w"
[ $VERBOSE -eq 1 ] && cmd="$cmd -v"
[ $ALLHEAD -eq 1 ] && cmd="$cmd -a"
local FILEXT
if [ -z "$FILESPEC" -o "$FILESPEC" = "everything" ]; then
FILEXT="all";
else
FILEXT="$FILESPEC"
fi
cmd="$cmd -t $FILEXT -o ${STORETO}/$RESTDIR ${DUMPFILE} >/dev/null"
debugMsg "Executing '$cmd'"
eval $cmd
rc=$?
}
#----------------------------------------------------[ Obtain the FileType ]---
function getFileType() {
local FEXT=`echo ${1##*.}|tr [:upper:] [:lower:]`
local EXT
local DESC
local FTLIST="filetypes.${RECOPROG}"
if [ -f /etc/ext3undel/$FTLIST ]; then
FTLIST="/etc/ext3undel/$FTLIST"
elif [ -f $HOME/.ext3undel/$FTLIST ]; then
FTLIST="$HOME/.ext3undel/$FTLIST"
elif [ -f ${0%/*}/$FTLIST ]; then
FTLIST="${0%/*}/$FTLIST"
fi
while read line; do
EXT=${line%%;*}
[ "$FEXT" != "$EXT" ] && continue
DESC=${line##*;}
break
done<$FTLIST
if [ -z "$DESC" ]; then
li1 "FileType '$FEXT' is unknown to $RECOPROG - so we let it check all it knows."
FILESPEC="everything"
else
echo "File has the extension '$EXT'. According to the list of known file types, it"
echo "probably is a '$DESC' file."
echo "Shall we handle it as such (y), or better check all other file types"
echo -n "as well (y/n)? "
readyn
echo
if [ $rd -eq 1 ]; then
FILESPEC="$EXT"
li1 "FileType set to '$EXT' ('$DESC')"
else
FILESPEC="everything"
li1 "FileType set to 'everything' (all that's supported)"
fi
fi
}
#-------------------------------------------------[ Find iNode information ]---
# $1: what to search (-d for deleted, -u for undeleted, "all" for all)
# $2: "all" for no "r/r" restriction
function getINode() {
case "$1" in
"all") FLS="fls -r -p $DEV|grep -v '(realloc)'|egrep '$SEARCH'";;
"-d"|"-u") FLS="fls -r $1 -p $DEV|grep -v '(realloc)'|grep 'r/r'|egrep '$SEARCH'";;
esac
mkfifo $TMP
debugMsg "$FLS"
eval $FLS>$TMP &
typeset -i i=0
while read line; do
if [ "$(echo $line|awk '{print $2}')" = "*" ]; then
iFILE[$i]=`echo "'$line '"|awk '{print $4}'`
iNODE[$i]=`echo "'$line'"|awk '{print $3}'`
else
iFILE[$i]=`echo "'$line '"|awk '{print $3}'`
iNODE[$i]=`echo "'$line'"|awk '{print $2}'`
fi
iNODE[$i]=${iNODE%*:}
i+=1
done <$TMP
debugMsg "Found ${#iNODE[*]} entries"
rm -f $TMP
}
#------------------------------------------------[ Restore a file (or not) ]---
# $1: filename (relative to mountpoint)
# $2: iNode#
# $3: directory iNode substituted
function fileRestore() {
if [ -n "$3" ]; then
local SUBST=$3
else
local SUBST=0
fi
if [ $SUBST -eq 0 ]; then
local NAM="file"
else
local NAM="substituted directory"
fi
echo
echo -n "Found $NAM '$MNT/$1' on iNode '$2'. Restore (y/n)? "
readyn
if [ $rd -eq 1 ]; then
echo
if [ $SUBST -eq 0 ]; then
getFileType "$1"
else
getFileType "$uFILE"
fi
mkfifo $TMP
fsstat $DEV > $TMP &
typeset -i min
typeset -i max
typeset -i nod=$2
local range=0
local start=0
while read line; do
if [ $start -eq 0 ]; then
[[ "$line" == Group:* ]] && start=1
else
if [[ "$line" == Inode?Range:* ]]; then
min=`echo $line|awk '{print $3}'`
max=`echo $line|awk '{print $5}'`
if [ $nod -gt $min -a $nod -lt $max ]; then
li1 "iNode $2 found in range '$min - $max'"
range=1
continue
fi
elif [ $range -eq 1 ]; then
if [[ "$line" == Block?Range:* ]]; then
min=`echo $line|awk '{print $3}'`
max=`echo $line|awk '{print $5}'`
li1 "Creating image of matching block range ($min - $max)"
dls $DEV $min-$max >${DUMPFILE}
case "$RECOPROG" in
"photorec") recoPhotoRec;;
"foremost") recoForemost;;
*) echo "Ooops - no recovery tool specified???"
echo "You normally should not see this message - there seems to be some bug in the script..."
;;
esac
if [ $rc -eq 0 ]; then
echo
echo "All recoverable files from the data block range where the requested"
echo "file had been stored in have been reconstructed and stored into"
echo " ${STORETO}/$RESTDIR"
echo "(if you used PhotoRec, you have to add '.#' to this path, where '#'"
echo "is a number). You still have to check these files manually, and"
echo "to rename/copy those you want to where you want them."
else
echo "Ooops! It looks like $RECOPROG failed to recover your file. So we will"
echo "exit as well now - with the exit code $RECPROG gave us..."
cleanUp
exit $rc
fi
break
fi
fi
fi
done<$TMP
[ $range -eq 0 ] && echo "Could not determine data block range - so no restore, sorry..."
fi
}
#------------------------------------------------[ cleanUp temporary stuff ]---
function cleanUp() {
li1 "Cleaning up..."
echo
rm -f $TMP $MNTFILE $DUMPFILE
echo
}
#============================================================[ Do the job! ]===
#---------------------------------------------------[ check pre-requisites ]---
DIRECTHIT=1 # Hopefully we find the files iNode
checkRoot
checkBins
#-----------------------------------------------------[ parse command line ]---
[ -z "$1" ] && syntax
case "$1" in
"-h"|"--help"|"-?") syntax;;
*)
set -f
echo
fileName "$1"
set +f
;;
esac
shift
while [ -n "$1" ]; do
case "$1" in
"+d"|"--debug") DEBUG=1;;
"-d"|"--nodebug") DEBUG=0;;
"+s"|"--silent") SILENT=1;;
"-s"|"--nosilent") SILENT=0;;
"--freespace") WHOLESPACE=0;;
"--wholespace") WHOLESPACE=1;;
esac
shift
done
debugMsg "SILENT=${SILENT}"
debugMsg "DEBUG=${DEBUG}"
#----------------------------------------------------------[ check sources ]---
symlinkCheck
selSrc
[ -z "$DEV" ] && {
echo
echo "Sorry - something went wrong, could not determine the source device."
echo
cleanUp
exit 19
}
selDest
if [ -n "$STORETO" ]; then
DUMPFILE=${STORETO}/dump.$$
else
echo
echo "Sorry - something went wrong, we do not have a destination to store to."
echo
cleanUp
exit 2
fi
splitFileName
li1 "Searching for iNode on '$DEV'..."
getINode "-d"
if [ ${#iNODE[*]} -eq 0 ]; then
DIRECTHIT=0
echo "No matching iNode found. Shall we try to substitute with the iNode of the"
echo -n "parent directory (y/n)? "
readyn
echo
if [ $rd -eq 1 ]; then
SEARCH=${SEARCH%\\*}
getINode "all"
fi
fi
if [ ${#iNODE[*]} -eq 0 ]; then
echo "No matching iNode found. Looks like G.A.B.I. is your last chance."
else
typeset -i i=0
if [ $DIRECTHIT -eq 1 ]; then
while (( i < ${#iNODE[*]} )); do
fileRestore "${iFILE[$i]}" ${iNODE[$i]} 0
i+=1
done
else # we substitute the directories iNode - so first hit only
fileRestore "${iFILE[0]}" ${iNODE[0]} 1
fi
fi
cleanUp
exit 0