#!/bin/bash

vers="2010.02.02.A"
cwd=$(PWD)
cd "${0%/*}"

#  does not fix:
#    ERROR: skipping unknown error "rsync: delete_file: rmdir(Flash/.HFS+ Private Directory Data#015) failed: Operation not permitted (1)"



#########################################################################################
#########################################################################################
###  fix symbolic link errors from rsync vers 260 error messages
#########################################################################################
#########################################################################################

if [[ (-z "$1") || (-z "$2") || (-z "$3") || (-z "$4") ]] ; then
  echo
  echo "FixLinks vers $vers"
  echo
  echo "syntax: fixlinks {mode} {sourcefolder} {backupfolder} {errorfile}"
  echo
  echo "available modes:"
  echo "  0 - dry run (debug)"
  echo "  1 - dry run (noisy)"
  echo "  2 - dry run (noisy), then confirmation, then live (noisy)"
  echo "  3 - live (noisy)"
  echo "  4 - live (quiet)"
  echo
  echo "Notes:"
  echo "- remote source not supported, must run fixlinks on source machine"
  echo "- syntax for {sourcefolder} or {backupfolder} is \"/Volumes/Drive1/\" etc"
  echo "- optional syntax for {backupfolder} is \"user:port@ipaddress:/path\""
  echo "    port is optional, root user is assumed (defaults to root:22)"
  echo
  echo "Exit codes:"
  echo "  0 - success"
  echo "  1 - parameter error"
  echo "  2 - temp file r/w error"
  echo "  3 - not run as root"
  echo "  4 - file/folder not found"
  echo "  5 - use error"
  echo "  6 - some errors not fixed"
  echo "  7 - error during push to remote server"
  echo
  exit 1
fi


#########################################################################################
###  subroutine to run one fix pass (so it can do dry run followed by live)
#########################################################################################


fixlinks1 () {

debug=$1 ; if [ $debug == 0 ] ; then debug= ; fi
noisy=$2 ; if [ $noisy == 0 ] ; then noisy= ; fi
live=$3 ; if [ $live == 0 ] ; then live= ; fi

rcs=0  # return code if all goes well

if [ $debug ] ; then
  echo "DEBUG: params: debug=$debug  noisy=$noisy  live=$live"
  echo "DEBUG: sourcefolder=\"$sourcefolder\""
  echo "DEBUG: backupfolder=\"$backupfolder\""
  echo "DEBUG: sourceremote=\"$sourceremote\""
  echo "DEBUG: backupremote=\"$backupremote\""
  echo "DEBUG:  sourcelogin=\"$sourcelogin\""
  echo "DEBUG:  backuplogin=\"$backuplogin\""
  echo "DEBUG:   backupport=\"$backupport\""
  echo "DEBUG:    errorfile=\"$errorfile\""
fi

# cannot fix links from remote source
if [ $sourceremote ] ; then
  echo "ERROR: cannot fix links from remote source"
  exit 1
fi

# prepare for remote backup
if [ $backupremote ] ; then
  remotefile="$(date "+%s")_fixlinks.command"
  if [ -f "/tmp/$remotefile" ] ; then
    rm "/tmp/$remotefile"
  fi
  if [ -e "/tmp/$remotefile" ] ; then
    echo "ERROR: Unable to remove old \"/tmp/$remotefile\""
    exit 2
  fi
  echo "#!/bin/bash" > "/tmp/$remotefile"
  if [ $noisy ] ; then
    echo "FIXLINKS: COMMAND: #!/bin/bash"
  fi
  chmod +x "/tmp/$remotefile"
  if ! [ -f "/tmp/$remotefile" ] ; then
    echo "ERROR: Unable to create new \"/tmp/$remotefile\""
    exit 2
  fi
fi

# read the error file into an array for processing
x=1;while read p ; do if ! [ -z "$p" ] ; then lines[x]=$p ; ((x=x+1)) ; fi ; done < "$errorfile"
if [ $debug ] ; then echo "DEBUG: loaded ${#lines[*]} error lines" ; fi

# look for errors caused by symbolic links
linenum=0
proposed=0
while [ $linenum -lt ${#lines[*]} ] ; do
  ((linenum=linenum+1))
  line=${lines[linenum]}
  if [ $debug ] ; then echo "DEBUG: got line \"$line\"" ; fi
  dest="$(echo $line | cut -d '"' -f 2)"

  action=

  # error trying to rm a symbolic link
  # delete_one: unlink "/Volumes/Service Data 1/Denver/Software/Harcourt SW/Phonics Express/Level B/Harcourt Phonics Express/media/LevelB/111/11162.cxt" failed: Operation not permitted
  if [ -z "$action" ] ; then
    header="delete_one: unlink "
    trailer="Operation not permitted"
    if [ -n "$(echo "$line" | grep "^${header}.*${trailer}$")" ]  ; then
      action="rm"
      if [ $debug ] ; then echo "DEBUG: MATCH \"$header\" / \"$trailer\"" ; fi
    else
      if [ $debug ] ; then echo "DEBUG: did not match \"$header\" / \"$trailer\"" ; fi
    fi
  fi

  # error trying to rm a symbolic link
  # delete_one: unlink "/Volumes/Service Data 1/Denver/Software/Harcourt SW/Phonics Express/Level B/Harcourt Phonics Express/media/LevelB/111/11162.cxt" failed: Directory not empty
  if [ -z "$action" ] ; then
    header="delete_one: unlink "
    trailer="Directory not empty"
    if [ -n "$(echo "$line" | grep "^${header}.*${trailer}$")" ]  ; then
      action="rm"
      if [ $debug ] ; then echo "DEBUG: MATCH \"$header\" / \"$trailer\"" ; fi
    else
      if [ $debug ] ; then echo "DEBUG: did not match \"$header\" / \"$trailer\"" ; fi
    fi
  fi

  # error trying to chown a finder-locked file
  # chown "/Volumes/Backup/MBP/System/Library/CoreServices/boot.efi" failed: Operation not permitted
  if [ -z "$action" ] ; then
    header="chown "
    trailer=" failed: Operation not permitted"
    if [ -n "$(echo "$line" | grep "^${header}.*${trailer}$")" ]  ; then
      action="chownL"
      if [ $debug ] ; then echo "DEBUG: MATCH \"$header\" / \"$trailer\"" ; fi
    else
      if [ $debug ] ; then echo "DEBUG: did not match \"$header\" / \"$trailer\"" ; fi
    fi
  fi

  # error trying to chown a symbolic link
  # chown "/Volumes/Service Tiger 1/Library/Receipts/iTunesX.pkg/Contents/Resources/iTunesX.sizes" failed: No such file or directory
  if [ -z "$action" ] ; then
    header="chown "
    trailer=" failed: No such file or directory"
    if [ -n "$(echo "$line" | grep "^${header}.*${trailer}$")" ]  ; then
      action="chown"
      if [ $debug ] ; then echo "DEBUG: MATCH \"$header\" / \"$trailer\"" ; fi
    else
      if [ $debug ] ; then echo "DEBUG: did not match \"$header\" / \"$trailer\"" ; fi
    fi
  fi

  # error trying to replace a folder with a symlink  (this is the only error with TWO path parameters)
  # symlink "/Volumes/Service Tiger 1/ULB1" -> "/Volumes/Service Tiger 1/ULB2" failed: File exists
  if [ -z "$action" ] ; then
    header="symlink "
    trailer=" failed: File exists"
    if [ -n "$(echo "$line" | grep "^${header}.*${trailer}$")" ]  ; then
      action="symlink"
      if [ $debug ] ; then echo "DEBUG: MATCH \"$header\" / \"$trailer\"" ; fi
    else
      if [ $debug ] ; then echo "DEBUG: did not match \"$header\" / \"$trailer\"" ; fi
    fi
  fi

  # error trying to rmdir a locked folder
  # delete_file: rmdir(Flash/.HFS+ Private Directory Data#015) failed: Operation not permitted (1)
  # there is no good way to deal with this.

  if [ -z "$action" ] ; then
    echo "ERROR: skipping unknown error \"$line\""
    rcs=6
    continue
  fi

  if [ $debug ] ; then
    echo "DEBUG: header = \"$header\""
    echo "DEBUG: trailer = \"$trailer\""
  fi

  # extract path of link to fix (backup path)
  bpath=${line:$((${#header}+1)):$((${#line}-${#trailer}-${#header}-2))}

  # extract path of original (source path, relies on bpath being set properly)
  spath=${sourcefolder}${bpath:$((${#backupfolder})):999}
  
  k1path=${bpath%\" -> \"*}  # the link
  k2path=${bpath#*\" -> \"}  # the link target
  if [ $debug ] ; then
    echo "DEBUG: spath = \"$spath\""
    echo "DEBUG: bpath = \"$bpath\""
    echo "DEBUG: k1path = \"$k1path\""
    echo "DEBUG: k2path = \"$k2path\""
  fi

  if [ "$action" == "rm" ] ; then
    echo "1a"
    if ! [ -e "$bpath" ] ; then
      echo "ERROR: cannot find backup to remove: \"$bpath\""
    else
      ((proposed=proposed+1))
      if [ $backupremote ] ; then 
        echo "rm -Rf \"$bpath\"" >> "/tmp/$remotefile"
        echo "((r=r+\$?))" >> "/tmp/$remotefile"
        if [ $noisy ] ; then
          echo "FIXLINKS: COMMAND: rm -Rf \"$bpath\""
          #echo "FIXLINKS: COMMAND: ((r=r+\$?))"
        fi
      elif [ $live ] ; then
        if [ $noisy ] ; then
          echo "FIXLINKS: RUN: rm -Rf \"$bpath\""
        fi
        rm -Rf "$bpath"
      else 
        if [ $noisy ] ; then
          echo "FIXLINKS: PROPOSED: rm -Rf \"$bpath\""
        fi
      fi
    fi
  elif [ "$action" == "chown" ] ; then
    if ! [[ (-e "$spath") || (-L "$spath") ]] ; then  # -e works for everything but symlinks
      echo "ERROR: cannot find source for chown: \"$spath\""
      rcs=6
    elif ! [[ (-e "$bpath") || (-L "$bpath") || ($backupremote) ]] ; then
      echo "ERROR: cannot find backup for chown: \"$bpath\""
      rcs=6
    else
      ((proposed=proposed+1))
      suser="$(export LS_COLWIDTHS="::15:15" ; ls -blan "$spath" | tr -s ' ' | cut -d ' ' -f 3)"
      sgroup="$(export LS_COLWIDTHS="::15:15" ; ls -blan "$spath" | tr -s ' ' | cut -d ' ' -f 4)"
      if [ $backupremote ] ; then 
        echo "/usr/sbin/chown -h $suser:$sgroup \"$bpath\"" >> "/tmp/$remotefile"
        echo "((r=r+\$?))" >> "/tmp/$remotefile"
        if [ $noisy ] ; then
          echo "FIXLINKS: COMMAND: /usr/sbin/chown -h $suser:$sgroup \"$bpath\""
          #echo "FIXLINKS: COMMAND: ((r=r+\$?))"
        fi
      elif [ $live ] ; then
        if [ $noisy ] ; then
          echo "FIXLINKS: RUN: /usr/sbin/chown -h $suser:$sgroup \"$bpath\""
        fi
        /usr/sbin/chown -h $suser:$sgroup "$bpath"
      else 
        if [ $noisy ] ; then
          echo "FIXLINKS: PROPOSED: /usr/sbin/chown -h $suser:$sgroup \"$bpath\""
        fi
      fi
    fi
  elif [ "$action" == "chownL" ] ; then
    if ! [[ (-e "$spath") || (-L "$spath") ]] ; then  # -e works for everything but symlinks
      echo "ERROR: cannot find source for chown: \"$spath\""
    elif ! [[ (-e "$bpath") || (-L "$bpath") || ($backupremote) ]] ; then
      echo "ERROR: cannot find backup for chown: \"$bpath\""
    else
      ((proposed=proposed+1))
      suser="$(export LS_COLWIDTHS="::15:15" ; ls -blan "$spath" | tr -s ' ' | cut -d ' ' -f 3)"
      sgroup="$(export LS_COLWIDTHS="::15:15" ; ls -blan "$spath" | tr -s ' ' | cut -d ' ' -f 4)"
      if [ $backupremote ] ; then 
        echo "/usr/local/bin/setfile -a l \"$bpath\"" >> "/tmp/$remotefile"
        echo "((r=r+\$?))" >> "/tmp/$remotefile"
        if [ $noisy ] ; then
          echo "FIXLINKS: COMMAND: /usr/local/bin/setfile -a l \"$bpath\""
        fi
        echo "/usr/sbin/chown -h $suser:$sgroup \"$bpath\"" >> "/tmp/$remotefile"
        echo "((r=r+\$?))" >> "/tmp/$remotefile"
        if [ $noisy ] ; then
          echo "FIXLINKS: COMMAND: /usr/sbin/chown -h $suser:$sgroup \"$bpath\""
        fi
      elif [ $live ] ; then
        if [ $noisy ] ; then
          echo "FIXLINKS: RUN: /usr/local/bin/setfile -a l \"$bpath\""
        fi
        /usr/local/bin/setfile -a l "$bpath"
        if [ $noisy ] ; then
          echo "FIXLINKS: RUN: /usr/sbin/chown -h $suser:$sgroup \"$bpath\""
        fi
        /usr/sbin/chown -h $suser:$sgroup "$bpath"
      else 
        if [ $noisy ] ; then
          echo "FIXLINKS: PROPOSED: /usr/local/bin/setfile -a l \"$bpath\""
          echo "FIXLINKS: PROPOSED: /usr/sbin/chown -h $suser:$sgroup \"$bpath\""
        fi
      fi
    fi
  elif [ "$action" == "symlink" ] ; then
    if ! [[ (-d "$k1path") || (-d "$k2path") ]] ; then  # should both be folders
      echo "ERROR: source or target not folder for symlink"
    else
      ((proposed=proposed+1))
      if [ $backupremote ] ; then 
        echo "rm -Rf \"$k1path\" ; ln -s \"$k1path\" \"$k2path\"" >> "/tmp/$remotefile"
        echo "((r=r+\$?))" >> "/tmp/$remotefile"
        if [ $noisy ] ; then
          echo "FIXLINKS: COMMAND: rm -Rf \"$k1path\" ; ln -s \"$k1path\" \"$k2path\""
          #echo "FIXLINKS: COMMAND: ((r=r+\$?))"
        fi
      elif [ $live ] ; then
        if [ $noisy ] ; then
          echo "FIXLINKS: RUN: rm -Rf \"$k1path\" ; ln -s \"$k1path\" \"$k2path\""
        fi
        rm -Rf "$k1path"
        ln -s "$k1path" "$k2path"
      else 
        if [ $noisy ] ; then
          echo "FIXLINKS: RUN: rm -Rf \"$k1path\" ; ln -s \"$k1path\" \"$k2path\""
        fi
      fi
    fi
  else
    echo "ERROR: UNEXPECTED ACTION: \"$action\""
    rcs=6
  fi
done
if [ $proposed == 0 ] ; then
  if [ $noisy ] ; then
    echo "FIXLINKS: found no fixable errors"
  fi
fi
if [ $debug ] ; then
  echo "DEBUG: remotefile=\"$remotefile\""
  echo "DEBUG: proposed=\"$proposed\""
fi

# push commands if remote backup
if [[ ($backupremote) && ($proposed != 0) ]] ; then
  echo "exit \$r" >> "/tmp/$remotefile"
  if [ $noisy ] ; then
    echo "FIXLINKS: COMMAND: exit \$r"
  fi
  if [ $noisy ] ; then echo -n "SCPing remote file with:  scp -P ${backupport} \"/tmp/$remotefile\" \"${backuplogin}@${backupip}:/tmp/\" ... " ; fi
  scp -P ${backupport} "/tmp/$remotefile" "${backuplogin}@${backupip}:/tmp/" &> /dev/null
  rc=$?
  if [ $noisy ] ; then echo "done with exit code \"$rc\"" ; fi
  if [ $rc != 0 ] ; then
    echo "ERROR: Bad return code \"$rc\" from backup server (copy) \"$backuplogin\""
    rcs=7
  elif [ $live ] ; then
    if [ $noisy ] ; then echo -n "Executing fix with:  ssh -p ${backupport} \"${backuplogin}@${backupip}\" \"/tmp/$remotefile ; rc=\$? ; if [ \$rc == 0 ] ; then rm /tmp/$remotefile ; fi ; rm /tmp/$remotefile ; echo \$rc\" ... " ; fi
    rc2=$(ssh -p ${backupport} "${backuplogin}@${backupip}" "/tmp/$remotefile ; rc=\$? ; if [ \$rc == 0 ] ; then rm /tmp/$remotefile ; fi ; echo \$rc ; exit 0")
    rc=$?
    if [ $noisy ] ; then echo "done with exit code \"$rc\" and return code \"$rc2\"" ; fi
    if [ $rc != 0 ] ; then
      echo "ERROR: Bad return code \"$rc\" from backup server (execute)"
      rcs=7
    else
      if [ $rc2 == 0 ] ; then
        if [ $noisy ] ; then
          echo "FIXLINKS: Good return code \"$rc2\" from backup server (return)"
        fi
      else
        echo "ERROR: Bad return code \"$rc2\" from backup server (return)"
        rcs=7
      fi
    fi
  else
    if [ $noisy ] ; then
      echo "FIXLINKS: /tmp/$remotefile is ready on backup server"
      echo "FIXLINKS: execute with command: ssh $backuplogin \"/tmp/$remotefile ; rc=$? ; rm /tmp/$remotefile ; echo $rc\""
    fi
  fi
  # rm "/tmp/$remotefile"
fi
exit $rcs

}


#########################################################################################
#########################################################################################
###  PROGRAM START
#########################################################################################
#########################################################################################

# must be run as root (not merely trying to use sudo) because of export/ls
if [ $UID != 0 ] ; then
  echo "FIXLINKS must run as root"
  exit 3
fi

# load parameters
mode=$1
if [ "$(echo $2 | grep "/$")" ] ; then
  sourcefolder=$2
else
  sourcefolder=$2"/"
fi
if [ "$(echo $3 | grep "/$")" ] ; then
  backupfolder=$3
else
  backupfolder=$3"/"
fi
errorfile=$4

# split up source and backup parameters
if [ -n "$(echo "$sourcefolder" | grep ":")" ] ; then
  # technically this is not supported
  sourceremote=1
  sourcelogin=${sourcefolder%:*}
  sourcefolder=${sourcefolder#*:}
fi  



backupleft=${backupfolder%@*}
backupright=${backupfolder#*@}
if [ "$backupleft" == "$backupright" ] ; then
  backupremote= 
else
  backupremote=1
  backuplogin=${backupleft%:*} 
  backupport=${backupleft#*:}
  if [ "$backuplogin" == "$backupport" ] ; then
    backupport="22"
  fi
  backupip=${backupright%:*}
  backupfolder=${backupright#*:}
fi



if ! [ -d "$sourcefolder" ] ; then
  echo "source folder (parameter 2) \"$sourcefolder\" not found"
  exit 4
elif ! [[ (-d "$backupfolder") || ($backupremote) ]] ; then
  echo "target folder (parameter 3) \"$backupfolder\" not found"
  exit 4
elif ! [ -f "$errorfile" ] ; then
  if [ -f "${cwd}/$errorfile" ] ; then  # be merciful, will settle for partial path on errorfile
    errorfile="${cwd}/$errorfile"
  else
    echo "error file (parameter 4) \"$errorfile\" not found"
    exit 4
  fi
fi

# fixlinks1
#   vars:
#     sourcefolder
#     backupfolder
#     sourceremote
#     backupremote
#     sourcelogin
#     backuplogin
#     errorfile
#     backupport
#     backupremote
#   parameters:
#     1 = debug
#     2 = noisy
#     3 = live

# select by mode
rcl=0
case "$mode" in
  "0")  # dry run (debug)
    fixlinks1 1 1 0
    rcl=$?
  ;;
  "1")  # dry run (noisy)
    fixlinks1 0 1 0
    rcl=$?
  ;;
  "2")  # dry run (noisy), then confirmation, then live (noisy)
    # cannot fix links from remote source
    if [ -n "$(echo "$sourcefolder" | grep "@")" ] ; then
      echo "ERROR: mode 2 not supported for remote source"
      exit 5
    fi
    fixlinks1 0 1 0
    if [ $proposed -gt 0 ] ; then
      yn=
      while [ -z "$yn" ] ; do
        read -p "Fix links?  (Y/N) : " yn
        yn=$(echo "$yn" | tr "a-z" "A-Z")
        if ! [[ ( "$yn" == "Y" ) || ( "$yn" == "N" ) ]] ; then
          yn=""
          echo -n $'\a'
        fi
      done
      if [ "$yn" == "Y" ] ; then
        fixlinks1 0 1 1
        rcl=$?
      else
        echo "(not fixed)"
      fi
    fi
  ;;
  "3")  # live (noisy)
    fixlinks1 0 1 1
    rcl=$?
  ;;
  "4")  # live (quiet)
#    fixlinks1 0 0 1
    fixlinks1 0 0 1
    rcl=$?
  ;;
  *)
    echo "Invalid mode \"$mode\""
    exit 1
  ;;
esac

exit $rcl
