#!/bin/sh

#
# Script to format a storage device (e.g. partition on USB stick) for
# writable content storage. The argument is the sysfs block device
# (e.g., /sys/block/sda/sda1), if preceded by "-f" then the device
# is formatted even if contains some contents.
#

# Exit codes:

# Success
EXIT_OK=0
# General error, did not format
EXIT_ERROR=1
# Read only (write protected) device, did not format
EXIT_RODEV=2
# Contains content that may be in use, did not format, use -f to force
EXIT_BUSY=3
# The lock could not be obtained (normally a timeout)
EXIT_NOLOCK=4
# Format started but did not complete, filesystem corrupted
EXIT_FORMAT_FAILED=8
# In addition could not properly unmount the filesystem, filesystem corrupted
EXIT_FORMAT_FAILED2=9

# The label to use for the created filesystem
LABEL=hmp-content

# The maximum number of tries for /etc/fstab locking
MAX_LOCK_TRIES=5

# The maximum wait for umounting a filesystem
MAX_UMOUNT_WAIT=5

# The content directory and file to check for empty filesystem if mounted
CONTENTDIR=/srv/raperca/content
CONTENTINDEX="$CONTENTDIR"/index.svg

# Force formatting
FORCE=no

# Logs a message to syslog and stdout, the arguments are the message
log() {
    # get input from /dev/null to prevent logger from reading from stdin
    # and blocking if there are no arguments
    logger -t "content-format[$$]" -- "$@" < /dev/null
    echo "$@"
}

#
# Use this function instead of exit, so that the
# exit code may be logged
#
vexit() {
    local c="$?"
    [ -n "$1" ] && c="$1"
    log "EXIT: $c" >&2
    exit "$c"
}

#
# Parse arguments
#

if [ "$1" = "-f" ]; then
    FORCE=yes
    shift
fi
DEVPATH="$1"

if [ -z "$DEVPATH" ]; then
    log "ERROR: no device path given in command line" >&2
    vexit $EXIT_ERROR
fi

if [ ! -d "$DEVPATH" ]; then
    log "ERROR: '$DEVPATH' is not a device path" >&2
    vexit $EXIT_ERROR
fi

#
# Grab lock
#
# NOTE: we use the same lock as media-mount and other fstab manipulating
# utilities so that there are no concurrent mounts / umounts and no
# concurrent invocations of this script.

fstab_lock() {
    for (( i=1; i <= $MAX_LOCK_TRIES; i++ )) ; do
        dotlockfile -p -r 0 -l /var/run/fstab.lock && return 0
        sleep 1
    done
    log "ERROR: timed out acquiring fstab lock, system busy, try again later" >&2
    return 1
}

fstab_unlock() {
    dotlockfile -p -u /var/run/fstab.lock
}

fstab_lock || vexit $EXIT_NOLOCK

trap fstab_unlock EXIT

#
# Find device name
#

UDEVPATH="${DEVPATH#/sys}"
DEV="$(udevadm info --query=name --path="$UDEVPATH")"
if [ $? -ne 0 -o -z "$DEV" ]; then
    log "ERROR: no device file for '$DEVPATH'" >&2
    vexit $EXIT_ERROR
fi
DEV=/dev/"$DEV"
if [ "$(blockdev --getro "$DEV")" != 0 ]; then
    log "ERROR: device is write-protected" >&2
    vexit $EXIT_RODEV
fi

#
# Umount device
#
umount_dev() {
    # Find all the device names and/or symlinks
    local DEVS="$(udevadm info --query=all --path="$UDEVPATH" | awk '/^[NS]:/ {print $2}')"

    # Find all the mountpoint of the device, under any name, we gather
    # them in reverse order into $MPTS
    local MPTS=
    for d in $DEVS; do
        m="$(grep "^/dev/$d[[:space:]]" /proc/mounts | cut -d' ' -f 2 | tac | tr '\n' ' ')"
        if [ -n "$m" ]; then
            MPTS="$m $MPTS"
        fi
    done

    if [ -z "$MPTS" ]; then
        return $EXIT_OK
    fi

    # Check for content
    for m in $MPTS; do
        if [ "$FORCE" != yes -a "$m" = "$CONTENTDIR" -a -f "$CONTENTINDEX" ]; then
            log "ERROR: device has some contents that may be being played, aborting" >&2
            return $EXIT_BUSY
        fi
    done

    # Umount all mount points
    local UMPTS=
    for m in $MPTS; do
        umount "$m" 2>/dev/null
        [ $? -eq 0 ] && UMPTS="$m $UMPTS" && continue

        log "INFO: mount point '$m' in use, waiting up to $MAX_UMOUNT_WAIT seconds" >&2
        # Mhh... filesystem in use, retry a few times
        r=1
        for (( i=0; i<="$MAX_UMOUNT_WAIT"; i++ )); do
            umount "$m" 2>/dev/null
            if [ $? -eq 0 ]; then
                log "INFO: device stopped" >&2
                r=0
            fi
            sleep 1
        done
        [ $r -eq 0 ] && UMPTS="$m $UMPTS" && continue
        log "ERROR: timed out trying to unmount '$DEVPATH' at '$m', filesystem busy, try again later" >&2
        [ -z "$UMPTS" ] || log "ERROR: already unmounted '$DEVPATH' from some locations (${UMPTS% }), cannot recover previous state" >&2
        return $EXIT_BUSY
    done

    return $EXIT_OK
}

umount_dev
RET=$?
if [ $RET -ne 0 ]; then
    log "ERROR: device is in use and could not stop it, exiting" >&2
    vexit $RET
fi

#
# Make the filesystem
#

mkfs.ext4 -F -q -m 1 -L "$LABEL" "$DEV"
if [ $? -ne 0 ]; then
    log "ERROR: failed creating ext4 filesystem on '$DEV'" >&2
    vexit $EXIT_FORMAT_FAILED
fi

#
# Create directories and adjust permissions in filesystem
#

mpt="$(mktemp -d /tmp/content-format.XXXXXX)"
if [ $? -ne 0 -o -z "$mpt" ]; then
    log "ERROR: failed creating temporary mountpoint" >&2
    vexit $EXIT_FORMAT_FAILED
fi

mount -t ext4 "$DEV" "$mpt"
if [ $? -ne 0 ]; then
    log "ERROR: failed temporary mount of new filesystem on '$DEV'" >&2
    rmdir "$mpt"
    vexit $EXIT_FORMAT_FAILED
fi

RET=$EXIT_OK

# Create the new style mount config file
touch "$mpt"/.spx-mount
if [ $? -ne 0 ]; then
    log "ERROR: failed creating the mount configuration file on new filesystem" >&2
    RET=$EXIT_FORMAT_FAILED
fi

mkdir "$mpt"/content
if [ $? -ne 0 ]; then
    log "ERROR: failed creating content directory on new filesystem" >&2
    RET=$EXIT_FORMAT_FAILED
fi

chgrp www-data "$mpt"/content
if [ $? -ne 0 ]; then
    log "ERROR: failed changing group on content directory of new filesystem" >&2
    RET=$EXIT_FORMAT_FAILED
fi

chmod g+w "$mpt"/content
if [ $? -ne 0 ]; then
    log "ERROR: failed setting group write permission on content directory of new filesystem" >&2
    RET=$EXIT_FORMAT_FAILED
fi

mkdir "$mpt"/capture
if [ $? -ne 0 ]; then
    log "ERROR: failed creating capture directory on new filesystem" >&2
    RET=$EXIT_FORMAT_FAILED
fi

chgrp raperca "$mpt"/capture
if [ $? -ne 0 ]; then
    log "ERROR: failed changing group on capture directory of new filesystem" >&2
    RET=$EXIT_FORMAT_FAILED
fi

chmod g+w "$mpt"/capture
if [ $? -ne 0 ]; then
    log "ERROR: failed setting group write permission on capture directory of new filesystem" >&2
    RET=$EXIT_FORMAT_FAILED
fi

umount "$mpt"
if [ $? -ne 0 ]; then
    umount -r "$mpt" 2>/dev/null; umount -l "$mpt" 2>/dev/null
    log "ERROR: failed unmounting new filesystem on '$DEV'" >&2
    RET=$EXIT_FORMAT_FAILED2
fi

rmdir "$mpt"
if [ $? -ne 0 ]; then
    log "WARNING: failed removing temporary mount directory" >&2
fi

vexit $RET
