#!/bin/sh # Copyright (c) 2012-2019 Slawomir Wojciech Wojtczak (vermaden) # Copyright (c) 2019 Rozhuk Ivan # 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/MTP/HFS Add these to mount NTFS/exFAT/EXT4/HFS/XFS/MTP respectively: o sysutils/fusefs-ntfs o sysutils/fusefs-exfat o sysutils/fusefs-ext4fuse o sysutils/fusefs-hfsfuse o sysutils/fusefs-lkl o sysutils/fusefs-jmtpfs o x11/zenity 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. MNT_PREFIX (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: MNT_PREFIX='/media' MNT_GROUP (operator by default) If set to some group name, the mount command will chown(1) the mount directory with the group. example: group='wheel' MNT_MODE (set to 775 by default) Value for chmod on mount point. FAT_ENCODING (set to ru_RU.UTF-8 by default) Only used with FAT32 mounts, specifies which encoding to use at the mount. example: FAT_ENCODING='en_US.ISO8859-1' FAT_CODEPAGE (set to CP866 by default) Only used with FAT32 mounts, specifies which code page to use at the mount. example: FAT_CODEPAGE='cp437' ISO_9660_CODEPAGE (set to UTF-8 by default) Only used with cd9660 mounts, specifies which code page to use at the mount. ATIME (set to NO by default) When set to NO it will mount filesystems with noatime options when possible. example: ATIME='YES' RETRY_COUNT (set to 16 by default) How many times try to get file system type or try to mount. example: RETRY_COUNT='8' RETRY_DELAY (set to 1 second by default) Delay beetwin retry attempt. example: RETRY_DELAY='2.5' 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' NOTIFY (set to NO by default) Use 'notify-send' and 'libnotify' to show notifications of mounting and unmounting devices on the desktop. example: NOTIFY='YES' WALL (set to NO by default) Use wall(1) to show notifications of mounting and unmounting devices on terminals of logged in users. example: WALL='YES' FILE_MANAGER ('exo-open --launch FileManager' 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: FILE_MANAGER='nautilus --browser --no-desktop' BLACKLIST (unset by default) The automount will ignore devices defined here. example: BLACKLIST='da0 da3s1a' EOF exit 0 } if [ "${1}" = '--version' -o "${1}" = '-version' -o "${1}" = 'version' ]; then echo 'automount 1.6.3 2019/08/14' exit 0 fi 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 : ${MNT_PREFIX='/media'} # Mount prefix. : ${MNT_GROUP='operator'} # which user to use for popup : ${MNT_MODE='775'} # Mount point mode. : ${FAT_ENCODING='en_US.ISO8859-1'} # US/Canada : ${FAT_CODEPAGE='cp437'} # US/Canada : ${ISO_9660_CODEPAGE='UTF-8'} # : ${ATIME='NO'} # When NO mount with noatime. : ${RETRY_COUNT='8'} # Retry count. : ${RETRY_DELAY='1'} # Retry delay time. : ${USERUMOUNT='NO'} # When YES add suid bit to umount(8). : ${NOTIFY='NO'} # use 'notify-send' and 'libnotify' : ${WALL='NO'} # use 'wall(1)' : ${FILE_MANAGER='exo-open --launch FileManager'} # which file manager to use : ${LOG_FILE='/tmp/automount.log'} # log file : ${LOG_DATEFMT='%Y-%m-%d %H:%M:%S'} # 2012-02-20 07:49:09 __log() { # @=MESSAGE echo `date +"${LOG_DATEFMT}"` "${@}" >> "${LOG_FILE}" } __show_message() { # 1=MESSAGE case ${WALL} in [Yy][Ee][Ss]) echo "automount: ${1}" | wall ;; *) ;; esac case ${NOTIFY} in [Yy][Ee][Ss]) local _DISPLAY_IDS=`ps aew | sed -n 's|.*DISPLAY=\([-_a-zA-Z0-9:.]*\).*|\1|p' | sort -u | tr '\n' ' '` for _DISPLAY_ID in ${_DISPLAY_IDS}; do local _USER=`ps aewj | grep "DISPLAY=${_DISPLAY_ID}" | awk '{print $1;}' | sort -u | tr -cd '[:print:]'` [ -z "${_USER}" ] && continue su -l "${_USER}" -c "env DISPLAY=${_DISPLAY_ID} notify-send automount '${1}' &" >/dev/null 2>&1 done ;; *) ;; esac } __check_is_already_mounted() { # 1=DEV 2=MNT local _MOUNT=`mount` if echo "${_MOUNT}" | grep -q "^${1} on " ; then local _MOUNT_POINT=`echo "${_MOUNT}" | grep "^${1} on " | cut -d ' ' -f 3-255 | cut -d '(' -f 1 | sed s/.$//` __log "${DEV}: already mounted on '${_MOUNT_POINT}' mount point." return 1 fi if echo "${_MOUNT}" | grep -q " on ${2} " ; then local _DEVICE=`echo "${_MOUNT}" | grep " on ${2} " | awk '{print $1}'` __log "${_DEVICE}: already mounted on '${2}' mount point." return 1 fi return 0 } readonly FS_TYPE_UNKNOWN=0 readonly FS_TYPE_ISO9660=1 readonly FS_TYPE_UFS=8 readonly FS_TYPE_EXT2=9 readonly FS_TYPE_EXT3=10 readonly FS_TYPE_EXT4=11 readonly FS_TYPE_XFS=12 readonly FS_TYPE_HFS=13 readonly FS_TYPE_FAT=32 readonly FS_TYPE_EXFAT=33 readonly FS_TYPE_NTFS=34 readonly FS_TYPE_MTP=128 __guess_fstype() { # 1=DEV, 2=ONLY_DEV_NAME # By device name. case ${2} in (iso9660*) return ${FS_TYPE_ISO9660} ;; (ugen*) return ${FS_TYPE_MTP} ;; esac # Try with GPT first. local _GPT_PART_UUID=`gpart show -rp | grep ${2} | awk '{print $4}' 2> /dev/null` case ${_GPT_PART_UUID} in (516e7cb6-6ecf-11d6-8ff8-00022d09712b) # freebsd-ufs return ${FS_TYPE_UFS} ;; (48465300-0000-11aa-aa11-00306543ecac) # HFS+ return ${FS_TYPE_HFS} ;; (c12a7328-f81f-11d2-ba4b-00a0c93ec93b) # EFI = fat32 return ${FS_TYPE_FAT} ;; #(ebd0a0a2-b9e5-4433-87c0-68b6b72699c7|de94bba4-06d1-4d40-a16a-bfd50179d6ac) # ms-basic-data | ms-recovery # return ${FS_TYPE_NTFS} # Sometimes FAT, let fstyp handle it. # ;; *) ;; esac # Try with fstyp. local _FS_TYPE=`fstyp ${2} 2> /dev/null` case ${_FS_TYPE} in (cd9660) return ${FS_TYPE_ISO9660} ;; (ufs) return ${FS_TYPE_UFS} ;; (ext2fs) return ${FS_TYPE_EXT2} ;; (msdosfs) return ${FS_TYPE_FAT} ;; (exfat) return ${FS_TYPE_EXFAT} ;; (ntfs) return ${FS_TYPE_NTFS} ;; *) ;; esac # Magic detection code. local _TMP=`dd if="${1}" conv=sync count=1 bs=1k 2> /dev/null | strings | head -1` case ${_TMP} in *EXFAT*) return ${FS_TYPE_EXFAT} ;; *) ;; esac # First time guess try with file util. local _TMP=`file -r -b -L -s ${1} 2> /dev/null | sed -E 's/label:\ \".*\"//g'` case ${_TMP} in *ISO\ 9660*) return ${FS_TYPE_ISO9660} ;; *Unix\ Fast\ File*) return ${FS_TYPE_UFS} ;; *ext2*) return ${FS_TYPE_EXT2} ;; *ext3*) return ${FS_TYPE_EXT3} ;; *ext4*) return ${FS_TYPE_EXT4} ;; *SGI\ XFS*) return ${FS_TYPE_XFS} ;; *Macintosh\ HFS*) return ${FS_TYPE_HFS} ;; *) ;; esac # Second time guess try with file util. # Re try with -k: Don't stop at the first match, keep going. local _TMP=`file -r -k -b -L -s ${1} 2> /dev/null | sed -E 's/label:\ \".*\"//g'` case ${_TMP} in *Unix\ Fast\ File*) return ${FS_TYPE_UFS} ;; *NTFS*) return ${FS_TYPE_NTFS} ;; *FAT* | *MSDOS*) return ${FS_TYPE_FAT} ;; *) ;; esac return ${FS_TYPE_UNKNOWN} } __wait_for_device() { # 1=DEV case ${1} in *ugen* | iso9660*) # Do not wait for some devices. return ;; *) ;; esac # Try to read from device to ensure that it alive. local COUNT=0 while ! dd if="${1}" of=/dev/null conv=sync count=1 bs=256k >/dev/null 2>&1 ; do if [ ! -e "${1}" ]; then __log "Device gone: ${1}." exit 1 fi COUNT=$(( COUNT + 1 )) if [ ${COUNT} -ge ${RETRY_COUNT} ]; then return fi sleep "${RETRY_DELAY}" __log "${1}: wait for device retry ${COUNT}." done } # Init variables. DEV="/dev/${1}" MNT="${MNT_PREFIX}/${1}" UID='0' # root GID=`id -g ${MNT_GROUP}` if [ ${?} -ne 0 ]; then __log "Invalid group: ${MNT_GROUP}." exit 1 fi # Process USERUMOUNT option. case ${USERUMOUNT} in [Yy][Ee][Ss]) chmod u+s /sbin/umount >/dev/null 2>&1 # WHEEL group member chmod u+s /sbin/mount* >/dev/null 2>&1 # WHEEL group member sysctl -q vfs.usermount=1 >/dev/null 2>&1 # allow user to mount ;; *) ;; esac case ${2} in (attach) if [ ! -e "${DEV}" ]; then __log "Invalid device: ${DEV}." exit 1 fi __log "${DEV}: attach." # Blacklist check. if [ -n "${BLACKLIST}" ]; then for _I in ${BLACKLIST}; do if [ "${1}" = "${_I}" ]; then __log "${DEV}: device blocked by BLACKLIST option." exit 0 fi done fi # Is already mounted? __check_is_already_mounted "${DEV}" "${MNT}" [ ${?} -ne 0 ] && exit 0 # Create mount point. [ -d "${MNT}" ] && exit 0 mkdir -m "${MNT_MODE}" -p "${MNT}" chown "root:${MNT_GROUP}" "${MNT}" # Make sure that data can be readed from device. __wait_for_device "${DEV}" # Detect filesysytem type. __guess_fstype "${DEV}" "${1}" FS_TYPE=${?} case ${ATIME} in [Nn][Oo]) OPTS='-o noatime' ;; *) ;; esac # Per fs abstract layer. case ${FS_TYPE} in (${FS_TYPE_ISO9660}) FS_CHK_CMD='' FS_CHK_ARGS='' FS_MOUNT_CMD='mount' FS_MOUNT_ARGS="-t cd9660 -o -e -o -C=${ISO_9660_CODEPAGE} ${DEV} ${MNT}" ;; (${FS_TYPE_UFS}) FS_CHK_CMD='fsck_ufs' FS_CHK_ARGS='-C -y' FS_MOUNT_CMD='mount' FS_MOUNT_ARGS="-t ufs ${OPTS} ${DEV} ${MNT}" ;; (${FS_TYPE_EXT2}) FS_CHK_CMD='fsck.ext2' FS_CHK_ARGS='-y' FS_MOUNT_CMD='mount' FS_MOUNT_ARGS="-t ext2fs ${OPTS} ${DEV} ${MNT}" ;; (${FS_TYPE_EXT3}) FS_CHK_CMD='fsck.ext3' FS_CHK_ARGS='-y' FS_MOUNT_CMD='mount' FS_MOUNT_ARGS="-t ext2fs ${OPTS} ${DEV} ${MNT}" ;; (${FS_TYPE_EXT4}) # sysutils/fusefs-ext4fuse FS_CHK_CMD='fsck.ext4' FS_CHK_ARGS='-y' FS_MOUNT_CMD='ext4fuse' FS_MOUNT_ARGS="${DEV} ${MNT}" ;; (${FS_TYPE_XFS}) # sysutils/fusefs-lkl FS_CHK_CMD='xfs_repair' FS_CHK_ARGS='-d' FS_MOUNT_CMD='lklfuse' FS_MOUNT_ARGS="-o type=xfs -o allow_other -o uid=${UID} -o gid=${GID} ${DEV} ${MNT}" ;; (${FS_TYPE_HFS}) # sysutils/fusefs-hfsfuse FS_CHK_CMD='' FS_CHK_ARGS='' FS_MOUNT_CMD='hfsfuse' FS_MOUNT_ARGS="--force ${OPTS} ${DEV} ${MNT}" ;; (${FS_TYPE_FAT}) FS_CHK_CMD='fsck_msdosfs' FS_CHK_ARGS='-C -y' FS_MOUNT_CMD='mount_msdosfs' FS_MOUNT_ARGS="-o longnames -m 644 -M ${MNT_MODE} -D ${FAT_CODEPAGE} -L ${FAT_ENCODING} -u ${UID} -g ${GID} ${OPTS} ${DEV} ${MNT}" ;; (${FS_TYPE_EXFAT}) # sysutils/fusefs-exfat FS_CHK_CMD='fsck.exfat' FS_CHK_ARGS='-y' FS_MOUNT_CMD='mount.exfat' FS_MOUNT_ARGS="-o uid=${UID} -o gid=${GID} -o dmask=002 -o fmask=113 ${OPTS} ${DEV} ${MNT}" ;; (${FS_TYPE_NTFS}) # sysutils/fusefs-ntfs FS_CHK_CMD='' FS_CHK_ARGS='' if /usr/bin/which -s ntfs-3g ; then FS_MOUNT_CMD='ntfs-3g' FS_MOUNT_ARGS="-o recover ${OPTS} ${DEV} ${MNT}" else FS_MOUNT_CMD='mount_ntfs' FS_MOUNT_ARGS="-u root -g ${MNT_GROUP} ${OPTS} ${DEV} ${MNT}" fi ;; (${FS_TYPE_MTP}) # sysutils/fusefs-jmtpfs FS_CHK_CMD='' FS_CHK_ARGS='' FS_MOUNT_CMD='jmtpfs' _PHONEDEV=`echo "${DEV}" | sed -n 's|.*ugen\([0-9]*\)\.\([0-9]*\).*|\1,\2|p' | tr -cd '[:print:]'` FS_MOUNT_ARGS="-device=${_PHONEDEV} ${MNT} -o use_ino -o readdir_ino -o allow_other -o intr -o big_writes -o uid=${UID} -o gid=${GID}" ;; (*) __log "${DEV}: filesystem not supported or no filesystem." exit 0 ;; esac # Check file system before mount. if [ -n "${FS_CHK_CMD}" ]; then if ! /usr/bin/which -s "${FS_CHK_CMD}" ; then __log "${FS_CHK_CMD} - not found." exit 1 fi ${FS_CHK_CMD} ${FS_CHK_ARGS} ${DEV} | while read _LINE ; do __log "${DEV}: ${FS_CHK_CMD} ${_LINE}" done fi # Try to mount. if ! /usr/bin/which -s "${FS_MOUNT_CMD}" ; then __log "${FS_MOUNT_CMD} - not found." exit 1 fi __wait_for_device "${DEV}" COUNT=0 while ! ${FS_MOUNT_CMD} ${FS_MOUNT_ARGS} >> "${LOG_FILE}" ; do if [ ! -e "${DEV}" ]; then __log "Device gone: ${DEV}." exit 1 fi COUNT=$(( COUNT + 1 )) if [ ${COUNT} -gt ${RETRY_COUNT} ]; then __log "${DEV}: mount FAILED: '${FS_MOUNT_CMD} ${FS_MOUNT_ARGS}'." exit 1 fi sleep "${RETRY_DELAY}" __log "${DEV}: fs mount retry ${COUNT}." done # Workaround for MTP: it can mount before user agree and this # mountpoint will be dead, so remove it. case ${FS_TYPE} in (${FS_TYPE_MTP}) COUNT=0 while ! ls "${MNT}" ; do if [ ! -e "${DEV}" ]; then __log "Device gone: ${DEV}." exit 1 fi COUNT=$(( COUNT + 1 )) if [ ${COUNT} -gt ${RETRY_COUNT} ]; then umount -f "${MNT}" >/dev/null 2>&1 rm -r "${MNT}" __log "${DEV}: mount FAIL: MTP requires user permission on device." exit 1 fi sleep "${RETRY_DELAY}" __log "${DEV}: MTP read fs retry ${COUNT}." done ;; esac __log "${DEV}: mount OK: '${FS_MOUNT_CMD} ${FS_MOUNT_ARGS}'." # Post processing. __show_message "Device '${DEV}' mounted on '${MNT}' directory." if [ -n "${FILE_MANAGER}" ]; then _GROUP_USERS=`pw group show ${MNT_GROUP} | sed -e 's|.*:||' -e 's|,| |g'` for _USER in ${_GROUP_USERS}; do # TODO: add wayland support here. _DISPLAY_ID=`ps aew -U "${_USER}" | grep -v Xorg | sed -n 's|.*DISPLAY=\([-_a-zA-Z0-9:.]*\).*|\1|p' | sed '/^$/d' | sort -u | head -n 1 | tr -cd '[:print:]'` [ -z "${_DISPLAY_ID}" ] && continue su -l "${_USER}" -c "env DISPLAY=${_DISPLAY_ID} ${FILE_MANAGER} ${MNT} &" >/dev/null 2>&1 done # Force to re-read dir. wait sleep "${RETRY_DELAY}" touch "${MNT}" fi ;; (detach) __log "${DEV}: detach." # Force unmount removed devices. COUNT=0 while umount -f "${MNT}" >/dev/null 2>&1 ; do COUNT=$(( COUNT + 1 )) __log "${DEV}: unmount retry ${COUNT}." done [ ! -e "${MNT}" ] && exit 0 rm -rf "${MNT}" find "${MNT}/*" -type d -empty -delete > /dev/null 2>&1 __log "${DEV}: mount point '${MNT}' removed." __show_message "Device '${DEV}' unmounted from '${MNT}' directory." ;; esac