#!/bin/bash
#
# Make shadowed content of mount points (if there is any) visible.
#
# IMPORTANT: Read the documentation:
# https://github.com/shundhammer/qdirstat/blob/master/doc/Shadowed-by-Mount.md
#
# Author:  Stefan Hundhammer <Stefan.Hundhammer@gmx.de>
# License: GPL V2
#

SCRIPT_NAME=$(basename $0)
DRY_RUN=0
CLEANUP=0
SHADOWED_FILES_COUNT=0
MNT_ROOT="/mnt/root"
MNT_SHADOWED="/mnt/shadowed"


# Show usage information and exit.
function usage()
{
    echo "$SCRIPT_NAME [-h][-n]"
    echo ""
    echo "Make shadowed content of mount points of the root filesystem visible"
    echo "with some bind mounts."
    echo ""
    echo "This needs root privileges unless started with the -n (dry run) option."
    echo ""
    echo "Read the documentation at"
    echo "https://github.com/shundhammer/qdirstat/blob/master/doc/Shadowed-by-Mount.md"
    echo ""
    echo "Options:"
    echo ""
    echo "-h  This usage message"
    echo "-n  Dry run: Don't actually execute the commands, just show them."
    echo "-c  Clean up: Unmount the bind mounts and remove the mount directories)."
    echo ""
    exit 2
}


# Write a message to stderr and exit with an error code.
function die()
{
    echo "$SCRIPT_NAME: FATAL: $*" >&2
    exit 1
}


# Process the command line and set some variables based on command line options.
function process_command_line()
{
    while getopts "nhc" opt; do
	case "${opt}" in
	    n)
		DRY_RUN=1
                echo ""
		echo "*** Dry run - not executing any dangerous commands. ***"
                echo ""
		;;
            c)
                CLEANUP=1
                echo "Cleaning up"
                echo ""
                ;;
	    *)
		usage
		;;
	esac
    done
}


# Check if we are running as root (including sudo) and exit if not.
function enforce_root_privileges()
{
    test $(id -u) = 0 || die "This needs root privileges."
}


# Echo and execute a command that requires root privileges.
# In dry run mode, only echo, don't execute it.
function root_cmd()
{
    local cmd="$*"

    echo "$cmd"
    test $DRY_RUN -eq 1 || $cmd
}


# Try to bind-mount $1 to $2. Skip if the same mount already exists.
# In dry run mode, only echo, don't execute it.
function try_mount()
{
    local SRC=$1
    local DEST=$2

    test -n "$SRC"  || die "try_mount() argument error: no SRC"
    test -n "$DEST" || die "try_mount() argument error: no DEST"

    if $(grep -q " $DEST " /proc/mounts); then
        echo "try_mount(): $DEST is already mounted - skipping."
    else
        if [ -d $SRC -o $DRY_RUN -eq 1 ]; then
            test -d "$DEST" || root_cmd "mkdir -p $DEST"
            root_cmd "mount -o bind $SRC $DEST"
        else
            # The root directory doesn't have that source directory;
            # so it must be a mount on top of a mount, e.g. a separate
            # /var/log on top of a separate /var.
            #
            # It would be nice to show that exactly in the dry run, but we
            # don't have the "naked" root filesystem yet at that point
            # (i.e. without any filesystem mounted on it), so the dry run
            # reports all those mounts on top of mounts as well.
            # Nobody is perfect.
            echo "$SRC does not exist - skipping."
        fi
    fi
}


# Show a summary of mounts below /mnt
function mount_summary()
{
    echo ""
    echo "Mounts below /mnt:"
    echo ""
    grep ' /mnt' /proc/mounts | cut -d ' ' -f1,2,3
}


function check_shadowed_files()
{
    if [ -d $MNT_SHADOWED ]; then
        SHADOWED_FILES_COUNT=$(find $MNT_SHADOWED -type f | head -n 1000 | wc -l)
    else
        SHADOWED_FILES_COUNT=0
    fi
}


# Create bind mounts:
# - bind-mount the root filesystem to /mnt/root
# - bind-mount each mount directly on the root filesystem to /mnt/shadowed/mp_name
function create_bind_mounts()
{
    # Check if anything is mounted to /mnt
    grep -q " /mnt " /proc/mounts && die "A filesystem is mounted to /mnt"

    try_mount / $MNT_ROOT
    test -d "$MNT_SHADOWED" || root_cmd "mkdir -p $MNT_SHADOWED"

    # Exclude all mount points from the root device:
    # They are either Btrfs subvolumes or bind mounts.
    # Btrfs subvolumes are expected to contain files;
    # bind mounts are duplicates of existing directories.
    # Both would only confuse the user. Stick to "real" mounts.

    ROOT_DEVICE=$(grep ' / ' /proc/mounts | cut -d ' ' -f 1)

    for MOUNT_POINT in $(grep -v "^$ROOT_DEVICE" /proc/mounts | egrep -v " /(sys|proc|dev|run|mnt)" | cut -d ' ' -f 2 | grep -v '^/$')
    do
        # Examples for $MOUNT_POINT:
        #   /home
        #   /work/tmp

        TARGET=${MOUNT_POINT#/}     # Remove leading "/"
        TARGET=${TARGET//\//_}      # Replace all "/" with "_"
        TARGET="${MNT_SHADOWED}/${TARGET}"
        SRC="${MNT_ROOT}$MOUNT_POINT"
        # echo "MOUNT_POINT: $MOUNT_POINT  SRC: $SRC  TARGET: $TARGET"

        try_mount $SRC $TARGET
    done
}


# Undo what a previous create_bind_mounts has done:
# - unmount all bind mounts from /mnt/shadowed
# - unmount the bind mount in /mnt/root
# - remove all mount directories we created
function tear_down_bind_mounts()
{
    for MOUNT in $(grep " $MNT_SHADOWED/" /proc/mounts | cut -d ' ' -f 2)
    do
        root_cmd "umount $MOUNT"
    done

    test -d $MNT_SHADOWED && root_cmd "rmdir $MNT_SHADOWED/*"
    test -d $MNT_SHADOWED && root_cmd "rmdir $MNT_SHADOWED"
    root_cmd "umount $MNT_ROOT"
    test -d $MNT_ROOT &&  root_cmd "rmdir $MNT_ROOT"
}



#----------------------------------------------------------------------
# main()
#----------------------------------------------------------------------

process_command_line $*
test $DRY_RUN -eq 1 || enforce_root_privileges

if [ $CLEANUP -eq 1 ]; then
    tear_down_bind_mounts
    mount_summary
    exit 0
fi

create_bind_mounts

if [ -d $MNT_SHADOWED ]; then
    check_shadowed_files
    echo ""

    if [ $SHADOWED_FILES_COUNT -eq 0 ]; then
        echo "=== Good news: No shadowed files. ==="
    else
        if [ $SHADOWED_FILES_COUNT -ge 1000 ]; then
            echo "=== Found $SHADOWED_FILES_COUNT or more shadowed files. ==="
        else
            echo "=== Found $SHADOWED_FILES_COUNT shadowed files.==="
        fi

        echo ""
        echo "=== Disk space in shadowed directories:"
        echo ""
        du -hs $MNT_SHADOWED/*

        echo ""
        echo "Now run    qdirstat $MNT_SHADOWED."
        echo ""
    fi

    echo ""
    echo "Remember to later clean everything up with"
    echo ""
    echo "    $SCRIPT_NAME -c"
    echo ""
fi
