#! /bin/sh

# Copyright (c) 2012 Slawomir Wojciech Wojtczak (vermaden)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that following conditions are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS 'AS IS' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

PATH=${PATH}:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

__usage() {
    cat << EOF
AUTOMOUNT is a devd(8) based automounter for FreeBSD.

It supports following file systems:
UFS/FAT/exFAT/NTFS/EXT2/EXT3/EXT4

It needs these ports to mount NTFS/exFAT/EXT4 respectively:
 o sysutils/fusefs-ntfs
 o sysutils/fusefs-exfat
 o sysutils/fusefs-ext4fuse

By default it mounts/unmounts all removable media but
it is possible to set some additional options at the
/usr/local/etc/automount.conf config file.

Below is a list of possible options with description.

MNTPREFIX (set to /media by default)
  With this options You can alter the default root
  for mounting the removable media, for example to
  the /mnt directory.

  example: MNTPREFIX="/media"

ENCODING (set to en_US.ISO8859-1 by default)
  Only used with FAT32 mounts, specifies which
  encoding to use at the mount.

  example: ENCODING="pl_PL.ISO8859-2"

CODEPAGE (set to cp437 by default)
  Only used with FAT32 mounts, specifies which
  code page to use at the mount.

  example: CODEPAGE="cp852"

USER (unset by default)
  If set to some username, the mount command will
  chown(1) the mount directory with the user and
  its primary user group. If used with FM option
  allows to launch the specified file manager after
  a successful mount.

  example: USER="vermaden"

FM (unset by default)
  If set to file manager command, the mount will
  launch the specified command after successful
  mount. Works only if USER parameter is also set.

  example: FM="nautilus --browser --no-desktop"

USERUMOUNT (set to NO by default)
  When set to YES it will 'chmod +s /sbin/umount'
  which would allow an USER to unmount the file
  system with their selected file manager.

  example: USERUMOUNT="YES"

ATIME (set to YES by default)
  When set to NO it will mount filesystems with
  noatime options when possible.

  example: ATIME="NO"

REMOVEDIRS (set to NO by default)
  When set to YES it will remove empty directories
  under the used ${MNTPREFIX} after device detach.

  example: REMOVEDIRS="YES"

EOF
  exit 0
}

if [ "${1}" = "-h" -o "${1}" = "--help" -o ${#} -eq 0 -o ${#} -eq 1 ]
then
  __usage
fi

if [ -f /usr/local/etc/automount.conf ]
then
  . /usr/local/etc/automount.conf
fi

: ${MNTPREFIX="/media"}               # mount prefix
: ${LOG="/var/log/automount.log"}     # log file
: ${STATE="/var/run/automount.state"} # current state file
: ${ENCODING="en_US.ISO8859-1"}       # US/Canada
: ${CODEPAGE="cp437"}                 # US/Canada
: ${DATEFMT="%Y-%m-%d %H:%M:%S"}      # 2012-02-20 07:49:09
: ${USERUMOUNT="NO"}                  # when YES add suid bit to umount(8)
: ${ATIME="YES"}                      # when NO mount with noatime
: ${REMOVEDIRS="NO"}                  # remove empty dirs under ${MNTPREFIX}
: ${USER="0"}                         # which user to use for popup
: ${FM="0"}                           # which file manager to use

if [ "${USERUMOUNT}" = YES ]
then
  chmod u+s /sbin/umount 1> /dev/null 2> /dev/null # WHEEL group member
  chmod u+s /sbin/mount* 1> /dev/null 2> /dev/null # WHEEL group member
  sysctl vfs.usermount=1 1> /dev/null 2> /dev/null # allow USER to mount
fi

__create_mount_point() { # 1=DEV
  mkdir -p ${MNT}
  if [ "${USER}" != 0 ]
  then
    chown ${USER}:$( id -g -n ${USER} ) ${MNT}
  fi
}

__state_add() { # 1=DEV 2=PROVIDER 3=MNT
  if grep -E "${3}$" ${STATE} 1> /dev/null 2> /dev/null
  then
    __log "${1}: duplicated '${STATE}'"
    exit 0
  fi
  echo "${1} ${2} ${3}" >> ${STATE}
}

__state_remove() { # 1=MNT
  BSMNT=$( echo ${1} | sed 's/\//\\\//g' ) # backslash the slashes ;)
  sed -i '' "/${BSMNT}\$/d" ${STATE}
}

__remove_dir() { # 1=TARGET
  if [ "${REMOVEDIRS}" = YES ]
  then
    find "${1}" -type d -empty -maxdepth 1 -exec rm -r {} '+' 2> /dev/null
  fi
}

__log() { # @=MESSAGE
  echo $( date +"${DATEFMT}" ) "${@}" >> ${LOG}
}

__check_already_mounted() { # 1=(-d|-m) 2=(DEV|MNT)
  local MOUNT="$( mount )"
  case ${1} in
    (-d)
      if echo "${MOUNT}" | grep -q "^${2} on "
      then
        local MOUNT="$( echo "${MOUNT}" | grep "^${2} on " | cut -d ' ' -f 3-255 | cut -d '(' -f 1 | sed s/.$// )"
        __log "${DEV}: -d already mounted on '${MOUNT}' mount point"
        exit 0
      fi
      ;;
    (-m)
      if echo "${MOUNT}" | grep -q " on ${2} "
      then
        local DEVICE="$( echo "${MOUNT}" | grep " on ${2} " | awk '{print $1}' )"
        __log "${DEVICE}: -m already mounted on '${2}' mount point"
        exit 0
      fi
      ;;
  esac
}

DEV=/dev/${1}

case ${2} in
  (attach)
    ADD=0
    MNT="${MNTPREFIX}/${1}"
    __check_already_mounted -d ${DEV}
    __check_already_mounted -m ${MNT}
    if [ "${ATIME}" = NO ]
    then
      OPTS="noatime"
    fi
    while ! head -c 1 ${DEV} 1> /dev/null 2> /dev/null
    do
      sleep 0.2
    done
    case $( file -b -L -s ${DEV} | sed -E 's/label:\ \".*\"//g' ) in
      (*FAT*) # must be before NTFS section: newfs_msdos -O NTFS -L NTFS
        __create_mount_point ${DEV}
        fsck_msdosfs -y ${DEV} \
          | while read LINE
            do
              __log "${DEV}: fsck_msdosfs ${LINE}"
            done
        if mount_msdosfs -o large -D ${CODEPAGE} -L ${ENCODING} \
                         -m 644 -M 755 ${DEV} ${MNT}
        then
          ADD=1
        else
          __log "${DEV}: mount failed (fat) 'mount_msdosfs -o large -D ${CODEPAGE} -L ${ENCODING} -m 644 -M 755 ${DEV} ${MNT}'"
          exit 1
        fi
        __log "${DEV}: mount (fat)"
        ;;
      (*NTFS*) # must be after FAT section: newfs_msdos -O NTFS -L NTFS
        __create_mount_point ${DEV}
        if which ntfs-3g 1> /dev/null 2> /dev/null # sysutils/fusefs-ntfs
        then
          if ntfs-3g -o ${OPTS} ${DEV} ${MNT}
          then
            ADD=1
          else
            __log "${DEV}: mount failed (ntfs) 'ntfs-3g -o ${OPTS} ${DEV} ${MNT}'"
            exit 1
          fi
        else
          if ! [ "${USER}" = 0 ]
          then
            OPTS="-u ${USER} -g $( id -g -n ${USER} )"
          fi
          if mount_ntfs ${OPTS} -o noatime ${DEV} ${MNT}
          then
            ADD=1
          else
            __log "${DEV}: mount failed (ntfs) 'ntfs-3g -o ${OPTS} ${DEV} ${MNT}'"
            exit 1
          fi
        fi
        __log "${DEV}: mount (ntfs)"
        ;;
      (*ext2*)
        __create_mount_point ${DEV}
        fsck.ext2 -y ${DEV} \
          | while read LINE
            do
              __log "${DEV}: fsck.ext2 ${LINE}"
            done
        if mount -t ext2fs -o ${OPTS} ${DEV} ${MNT}
        then
          ADD=1
        else
          __log "${DEV}: mount failed (ext2) 'mount -t ext2fs -o ${OPTS} ${DEV} ${MNT}'"
          exit 1
        fi
        __log "${DEV}: mount (ext2)"
        ;;
      (*ext3*)
        __create_mount_point ${DEV}
        fsck.ext3 -y ${DEV} \
          | while read LINE
            do
              __log "${DEV}: fsck.ext3 ${LINE}"
            done
        if mount -t ext2fs -o ${OPTS} ${DEV} ${MNT}
        then
          ADD=1
        else
          __log "${DEV}: mount failed (ext3) 'mount -t ext2fs -o ${OPTS} ${DEV} ${MNT}'"
          exit 1
        fi
        __log "${DEV}: mount (ext3)"
        ;;
      (*ext4*)
        __create_mount_point ${DEV}
        fsck.ext4 -y ${DEV} \
          | while read LINE
            do
              __log "${DEV}: fsck.ext4 ${LINE}"
            done
        if ext4fuse ${DEV} ${MNT} # sysutils/fusefs-ext4fuse
        then
          ADD=1
        else
          __log "${DEV}: mount failed (ext4) 'ext4fuse ${DEV} ${MNT}'"
          exit 1
        fi
        __log "${DEV}: mount (ext4)"
        ;;
      (*Unix\ Fast\ File*)
        __create_mount_point ${DEV}
        fsck_ufs -C -y ${DEV} \
          | while read LINE
            do
              __log "${DEV}: fsck_ufs ${LINE}"
            done
        if mount -t ufs -o ${OPTS} ${DEV} ${MNT}
        then
          ADD=1
        else
          __log "${DEV}: mount failed (ufs) 'mount -t ufs -o ${OPTS} ${DEV} ${MNT}'"
          exit 1
        fi
        __log "${DEV}: mount (ufs)"
        ;;
      (*)
        case $( dd < ${DEV} count=1 2> /dev/null | strings | head -1 ) in
          (*EXFAT*)
            __create_mount_point ${DEV}
            if mount.exfat -o ${OPTS} ${DEV} ${MNT} # sysutils/fusefs-exfat
            then
              ADD=1
            else
              __log "${DEV}: mount failed (exfat) 'mount.exfat -o ${OPTS} ${DEV} ${MNT}'"
              exit 1
            fi
            __log "${DEV}: mount (exfat)"
            ;;
        esac
        ;;
    esac
    if [ ${ADD} -eq 1 ]
    then
      ADD=0
      PROVIDER=$( mount | grep -m 1 " ${MNT} " | awk '{printf $1}' )
      __state_add ${DEV} ${PROVIDER} ${MNT}
      if [ "${USER}" != 0 -a "${FM}" != 0 ]
      then
        # su - ${USER} -c "env DISPLAY=:0 ${FM} ${MNT} &"
        su - ${USER} -c "env DISPLAY=:0 ${FM} &"
      fi
    fi
    ;;

  (detach)
    grep -E "${MNTPREFIX}/${1}$" ${STATE} \
      | while read DEV PROVIDER MNT
        do
          TARGET=$( mount | grep -E "^${PROVIDER} " | awk '{print $3}' )
          __state_remove ${MNT}
          if [ -z ${TARGET} ]
          then
            continue
          fi
          ( # put entire umount/find/rm block into background
            umount -f ${TARGET}
            __remove_dir "${TARGET}"
            __log "${DEV}: removed '${TARGET}'"
          ) &
          unset TARGET
          __log "${DEV}: umount"
        done
    __log "${DEV}: detach"
    __remove_dir "${MNTPREFIX}/${1}"
    __log "${DEV}: mount point '${MNTPREFIX}/${1}' removed"
    ;;

esac
