#!/bin/bash
# version 2017-03-11.2
usage() {
        cat 1>&2 <<- USAGE
        move files in ldiskfs lost+found to orig (or .lustre/lost+found)

        usage: $(basename $0) [--fix] [--home=/home] <mdt-mtpt> <e2fsck.log> [...]
        mdt-mtpt:       ldiskfs mounted MDT filesystem
        e2fsck.log:     e2fsck output from repairing <device>
        --fix:          actually fix filesystem, otherwise just print actions
USAGE

        exit 1
}

UHOME="/home"
FIX=false
while [ "${1:0:2}" = "--"; do
	[ "$1" = "--fix" ] && FIX=true && shift && continue
	[ "$1" = "--home=/" ] && UHOME=${1/--home=\//} && shift && continue
	usage
done

MDT="$1"
shift
LOGS="$*"
[ -z "$MDT" -o -z "$LOGS" ] && usage
[ ! -d "$MDT" ] && echo "$MDT: not a directory" 1>&2 && usage
DOT_LUSTRE="ROOT/.lustre"
[ ! -d "$MDT/$DOT_LUSTRE" ] && echo "$MDT/$DOT_LUSTRE: not found" 1>&2 &&
usage
DEV=$(df -P $MDT | awk '/'${MDT//\//.}'/ { print $1 }')
[ -z "$DEV" ] && echo "$DEV: not mounted" 1>&2 && usage
for L in $LOGS; do
        [ ! -f "$L" ] && echo "$L: not a regular file" 1>&2 && usage
        PASS1=$(head -100 $L | grep "^Pass 1: ")
        [ -z "$PASS1" ] && echo "$L: not an e2fsck log" 1>&2 && usage
done

fix() {
        local src="$1"
        local tgt="$2"
        [ -e "$tgt" ] && echo "$tgt exists, use $tgt.recov$INO" &&
                tgt="$tgt.recov$INO"
        $FIX && mv -v "$src" "$tgt" && return
        echo "#$INO: could recover '$(basename $src)' to '$tgt'"
}

find_fid() {
        local ino=$1
        local stat="$(debugfs -c -R "stat <$ino>" $DEV 2>/dev/null)"
        local fid="$(tr = ' '<<<"$stat" | awk '/lma: fid/ { print $3 }')"
        local lov="$(grep 'lov =' <<<$stat)"

        # always generate a FID for a directory
        grep -q directory <<<$stat && lov="fake"
        [ -z "$lov" -a -z "$fid" ] && echo "#$ino: not a lustre file 1>&2" &&
                return
        if [ -z "$fid" ]; then  # missing fid, use IGIF
                local gen=$(awk '/Generation: / { print $2 }' <<<$stat)
                fid=$(printf "[%#x:%#x:0x0]" $ino $gen)
        fi
        echo $fid
}

fix_by_fid() {
        local fid=$(find_fid $INO)
        [ -z "$fid" ] && echo "#$INO: no FID" 1>&2 && return
        local user=$(stat -c%U "$MDT_LPF/#$INO")
        [ "$user" = "root" ] && echo "#$INO: skip root file"
        local tgt="$MDT/ROOT/$UHOME/$user"
        [ -d "$tgt" ] && tgt="$tgt/lost+found" || tgt="$LUSTRE_LPF/$user"
        [ -d "$MDT_LPF/#$INO" ] && tgt="$tgt/subdir"
        $FIX && mkdir -p "$tgt" && chown $user "$tgt"
        fix "$MDT_LPF/#$INO" "$tgt/$fid"
}

fix_dir() {
        local pino="$1"
        local pname=$(basename "$2")
        [ "$pname" = "???" ] && pname="$(basename $(dirname "$2"))"
        [ -z "$pname" -o "$pname" = "." -o "$pname" = "..." ] &&
pname="subdir"
        local pfid=$(find_fid $pino)
        local tgt="$pname/$pfid"

        echo "$tgt"
}

fix_ino() {
        # First entry 'group_00002' (inode=234051200) in directory inode
        #       233853819 (...) should be '.'
        # Entry 'group_000283' in ... (233853819) has deleted/unused inode
        #       234150462.  Clear? yes
        NAME=$(grep "$INO" $LOGS |
               awk '/First|Second/ { print $3 };
                    /deleted.unused inode/ { print $2 }' | grep -v "'\.\.'" |
                    sed -e "s/^[^']*'//" -e "s/'.*$//" | uniq)
        if [ -z "$NAME" ]; then
                echo "can't find name for #$INO"
                fix_by_fid
                continue
        fi
        if [ $(wc -l<<<"$NAME") -ne 1 ]; then
                echo "#$INO: multiple names found: '$NAME'"
                [ -d "$MDT_LPF/#$INO" ] && fix_by_fid
                continue
        fi
        # Second entry 'group_00003' (inode=234051201) in directory inode
        #       233853819 (...) should be '..'
        # Entry 'a' in /ROOT/d39o.sanity/f39o.sanity (27825) has
        #       deleted/unused inode 27826.  Clear? yes
        PINO=$(grep "$INO" $LOGS |
               awk '/First|Second/ { print $8 };
                    /deleted.unused inode/ { print $5 }' | tr -d "()" | uniq)
        if [ -z "$PINO" ]; then
                echo "#$INO:$NAME: no parent inode"
                fix "$MDT_LPF/#$INO" "$LUSTRE_LPF/$NAME.recov$INO"
                continue
        fi
        #PDIR=$(grep "$PINO" $LOGS | awk '/is duplicate/ { print $4 }' | uniq)
        # handle directory names with spaces in them
        # Entry 'a' in /ROOT/d39o.sanity/f39o.sanity (27825) has
        #        deleted/unused inode 27826.  Clear? yes
        # Entry '.' in /ROOT/home/???/??? (230417515) is duplicate '.' entry.
       PDIR=$(grep "$PINO" $LOGS | grep -v "'\.\.'" |
               egrep 'is duplicate|has deleted.unused' |
               sed -e "s/.*Entry .* in //" -e "s/ ([0-9]*) is duplicate.*//" \
                   -e "s/ ([0-9]*) has deleted.*//" |
              uniq)
        if [ -z "$PDIR" -o ! -d "$MDT$PDIR" ]; then
                echo "#$INO:$NAME: parent #$PINO:$PDIR missing"
                subdir=$(fix_dir "$PINO" "$PDIR")
                user=$(stat -c%U "$MDT_LPF/#$INO")
                [ "$user" = "root" ] && echo "#$INO: skip root file"
                udir="$MDT/ROOT/$UHOME/$user"
                [ -d "$udir" ] && udir="$udir/lost+found/$subdir" ||
                        udir="$LUSTRE_LPF/$user/$subdir"
                $FIX && mkdir -p "$udir" && chown $user "$udir"
                fix "$MDT_LPF/#$INO" "$udir/$NAME"
                continue
        fi

        fix "$MDT_LPF/#$INO" "$MDT$PDIR/$NAME"
}

LUSTRE_LPF="$MDT/$DOT_LUSTRE/lost+found"
MDT_LPF="$MDT/lost+found"
$FIX && mkdir -p "$LUSTRE_LPF"
\ls $MDT_LPF | tr -d '#' | while read INO; do
        [ -d "$MDT_LPF/#$INO" ] && fix_ino
done
\ls $MDT_LPF | tr -d '#' | while read INO; do
        [ -f "$MDT_LPF/#$INO" ] && fix_ino
done
