#!/bin/bash -e
# Copyright (c) TurnKey GNU/Linux - http://www.turnkeylinux.org
#
# This file is part of Deck
#
# Deck is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.

if [[ -n "$DECK_DEBUG" ]] || [[ -n "$DEBUG" ]]; then
    set -x
fi

VERSION=2.1.0

fatal() { echo "fatal: $*" 1>&2; exit 1; }
warning() { echo "warning: $*" 1>&2; }

usage() {
cat<<EOF
Deck a filesystem using overlayfs
Syntax: $(basename "$0") path/to/dir/or/deck path/to/new/deck
Syntax: $(basename "$0") [ -D | -u ] path/to/existing/deck [path/to/other/deck ...]
Syntax: $(basename "$0") [ -option ] path/to/existing/deck

Options:
    -m|--mount      - mounts a deck (the default)
    -u|--umount     - unmount a deck
    -D|--delete     - delete a deck
    -f|--force      - force kill processes running within deck (only relevant
                      with -D|--delete or -u|--umount; otherwise ignored)
    -v|--version    - echo package version and exit

    --isdeck        - test if path is a deck
    --isdirty       - test if deck is dirty
    --ismounted     - test if deck is mounted
    --list-layers   - list layers of a deck

EOF
exit 1
}

real_path() {
    if [[ -d "$1" ]]; then
        realpath "$1"
    elif [[ -f "$1" ]]; then
        fatal "Path '$1' is a file."
    else
        if [[ "$1" == "/"* ]]; then
            echo "$1"
        else
            echo "$PWD/$1"
        fi
    fi
}

[[ -z "$DEBUG" ]] || set -x

deck_mount() {
    local parent=
    local mountpath=
    local deckdir=
    local parent_deckdir=
    local lowerdir=

    parent=$(real_path "$1")
    mountpath=$(real_path "$2")
    [[ -d "$parent" ]] || fatal 'parent does not exist'
    
    if [[ -d "$mountpath" ]]; then
        [[ $(find "$mountpath" -maxdepth 0 -empty) == "$mountpath" ]] || \
            fatal "mountpath (${mountpath}) exists but not empty"
    fi

    deckdir="$(dirname "$mountpath")/.deck/$(basename "$mountpath")"
    local workdir="$deckdir/work"
    local upperdir="$deckdir/upper"
    mkdir -p "$deckdir" "$workdir" "$upperdir"

    if deck_isdeck "$parent"; then
        echo "$parent" > "$deckdir/parent"
        parent_deckdir="$(dirname "$parent")/.deck/$(basename "$parent")"
        cp "$parent_deckdir/layers" "$deckdir/layers"
        sed -i "1i$parent_deckdir/upper" "$deckdir/layers"
        deck_ismounted "$parent" && deck_umount "$parent"
    else
        echo "$parent" > "$deckdir/parent"
        echo "$parent" > "$deckdir/layers"
    fi

    mkdir -p "$mountpath"
    lowerdir="$(tr '\n' ':' <"$deckdir/layers" | sed 's/:$//')"
    local options="lowerdir=$lowerdir,upperdir=$upperdir,workdir=$workdir"
    "${MOUNT[@]}" -o "$options" "$mountpath"
}

deck_remount() {
    local mountpath=
    local deckdir=
    local parent=

    mountpath=$(real_path "$1")
    [[ -d "$mountpath" ]] || fatal 'mountpath does not exist'
    deck_ismounted "$mountpath" && fatal 'already mounted'
    deck_isdeck "$mountpath" || fatal 'not a deck'
    deckdir="$(dirname "$mountpath")/.deck/$(basename "$mountpath")"
    parent=$(cat "$deckdir/parent")
    deck_mount "$parent" "$mountpath"
}

real_umount() {
    local mountpath=
    mountpath=$(real_path "$1")
    local force=$2
    [[ -d "$mountpath" ]] || fatal 'mountpath does not exist'
    if deck_ismounted "$mountpath"; then
        if [[ "$force" == "yes" ]]; then
            fuser --kill "$mountpath" \
                || warning "fuser --kill returned non-zero exit code."
            "${UMOUNT_F[@]}" "$mountpath" \
                || fatal "Cannot unmount $mountpath"
        else
            processes=$(fuser --mount "$mountpath" 2>/dev/null) || true
            [[ -z "$processes" ]] || \
                fatal "Running processes locking $mountpath," \
                      " try again with -f|--force to kill"
            if ! "${UMOUNT[@]}" "$mountpath" >/dev/null 2>&1; then
                submounts=$(mount | \
                    sed -n "s|.*\($mountpath/[a-zA-Z0-9 /]*\) type.*|\1|p" \
                )
                [[ -n "$submounts" ]] \
                    || fatal "Unknown reason for failing '${UMOUNT[*]} $mountpath'."
                echo -e "info: Attempting to unmount busy paths:\n$submounts"
                echo "$submounts" | while read -r submount; do
                    "${UMOUNT[@]}" "$submount" \
                        || fatal "unmounting busy path $submount failed"
                done
                "${UMOUNT[@]}" "$mountpath" || fatal "Cannot unmount $mountpath"
            fi
        fi
    fi
}

deck_umount() {
    local mountpath=

    mountpath=$(real_path "$1")
    local force=$2
    deck_ismounted "$mountpath" || fatal "not mounted: $mountpath"
    real_umount "$mountpath" "$force"
}

deck_delete() {
    local mountpath=
    local deckdir=

    mountpath=$(real_path "$1" 2>/dev/null)
    local force=$2
    deckdir="$(dirname "$mountpath")/.deck/$(basename "$mountpath")"
    [[ -d "$mountpath" ]] || fatal "mountpath does not exist: $mountpath"
    if deck_isdeck "$mountpath"; then
        find "$(dirname "$deckdir")" -name 'parent' -print \
            | grep -q "^${mountpath}$" \
                    && fatal 'cannot delete a parent deck'
        if deck_ismounted "$mountpath"; then
            deck_umount "$mountpath" "$force"
        fi
        rm -rf "$deckdir"
    fi
    rmdir "$mountpath"
    rmdir --ignore-fail-on-non-empty "$(dirname "$deckdir")"
}

deck_isdirty() {
    local mountpath=
    local deckdir=

    mountpath=$(real_path "$1")
    deckdir="$(dirname "$mountpath")/.deck/$(basename "$mountpath")"
    [[ -n "$(find "$deckdir/upper" -maxdepth 0 -empty)" ]] && return 1
    return 0
}

deck_isdeck() {
    local mountpath=
    local deckdir=

    [[ -d "$1" ]] || return 1
    mountpath=$(real_path "$1")
    deckdir="$(dirname "$mountpath")/.deck/$(basename "$mountpath")"
    [[ -d "$mountpath" ]] || return 1
    [[ -d "$deckdir" ]] || return 1
    [[ -d "$deckdir/work" ]] || return 255
    [[ -d "$deckdir/upper" ]] || return 255
    [[ -e "$deckdir/layers" ]] || return 255
    [[ -e "$deckdir/parent" ]] || return 255
    return 0
}

deck_ismounted() {
    local mountpath=
    mountpath=$(real_path "$1")
    [[ -d "$mountpath" ]] || fatal 'mountpath does not exist'
    if [[ $(id -u) -eq 0 ]]; then
        grep_str="^overlay on $mountpath type overlay"
    else
        grep_str="^fuse-overlayfs on $mountpath type fuse.fuse-overlayfs"
    fi
    mount | grep -q "$grep_str" || return $?
}

deck_version() {

    unset version
    if [[ -n $(readlink "$0") ]]; then
        bin_path="$(readlink "$0")"
    else
        bin_path="${0}"
    fi
    bin_dir="$(dirname "$bin_path")"
    pkg_bin=/usr/bin/deck

    if [[ -f ${bin_dir}/install.txt ]]; then
        version="$(head -1 "$bin_dir/install.txt")"
    elif [[ -f ${bin_dir}/debian/changelog ]]; then
        version="$(cd "$bin_dir" \
            && dpkg-parsechangelog -ldebian/changelog -S Version \
        )"
    elif [[ -d "$bin_dir/.git" ]]; then
        version="$(cd "$bin_dir" && autoversion HEAD)"
    elif [[ "$bin_path" = "$pkg_bin" ]]; then
        pkg_version="$(dpkg-query --showformat='${Version}' --show deck)"
        no_pkg_string="dpkg-query: no packages found matching deck"
        if [[ "$pkg_version" != "$no_pkg_string" ]]; then
            version="$pkg_version"
        fi
    fi
    [[ -z $version ]] && version="$VERSION"

    echo "$version"
    exit 0
}

# check that overlay module is loaded
if ! grep -q overlay <(lsmod); then
    warning "overlayfs module not loaded - attempting to load"
    if modprobe overlay; then
        echo "module loaded successfully, to auto load module everyboot run:"
        echo "echo overlay > /etc/modules-load.d/overlayfs.conf"
    else
        fatal "Loading module failed"
    fi
fi

# use fuse if not root (mount/umount require root)
if [[ $(id -u) -eq 0 ]]; then
    MOUNT=(mount -t overlay overlay)
    UMOUNT=(umount)
    UMOUNT_F=(umount --recursive --force --lazy)
else
    if ! which fuse-overlayfs >/dev/null; then
        fatal "fuse-overlayfs is required for unpriviledged use"
    fi
    MOUNT=(fuse-overlayfs)
    UMOUNT=(fusermount -u)
    # 'fusermount -z' == 'umount --lazy'; but fusermount does not have
    # '--recursive' & '--force' so unpriviledged 'deck --force' is not quite
    # the same
    UMOUNT_F=(fusermount -u -z)
    warning "unprivileged use is experimental - if problems, rerun with sudo"
fi

opts=()
args=()
force=''
umount=
delete=
while [[ -n "$1" ]]; do
    case "$1" in
        -h|--help)  usage;;
        -v|--version)
                    deck_version;;
        -f|--force)
                    force=yes;;
        -u|--umount)
                    umount=yes
                    opts=("${opts[@]}" "$1")
                    ;;
        -D|--delete)
                    delete=yes
                    opts=("${opts[@]}" "$1")
                    ;;
        -*)         opts=("${opts[@]}" "$1");;
        *)          args=("${args[@]}" "$1");;
    esac
    shift
done

[[ ${#args[@]} -eq 0 ]] && usage
[[ ${#opts[@]} -gt 1 ]] && fatal 'conflicting deck options'
[[ ${#opts[@]} -eq 0 ]] && opts=('--mount')
if [[ ${#args[@]} -gt 2 ]]; then
    if [[ -z "$umount" ]] && [[ -z "$delete" ]]; then
        fatal 'too many arguments'
    fi
fi
action=${opts[0]}

case $action in
    -u|--umount|-D|--delete)
        [[ ${#args[@]} -ge 1 ]] || fatal 'missing argument';;
    --isdeck|--ismounted|--show-layers)
        [[ ${#args[@]} -eq 1 ]] || fatal 'missing argument';;
esac

lsmod | grep -q overlay || fatal 'overlay module not loaded'

src="${args[0]}"
maybe_dst="${args[1]}"

case $action in
    -m|--mount)
        if [[ -n "$maybe_dst" ]]; then
            deck_mount "$src" "$maybe_dst"
        else
            deck_remount "$src"
        fi
        ;;
    -u|--umount)
        for arg in "${args[@]}"; do
            deck_umount "$arg" "$force"
        done
        ;;
    -D|--delete)
        for arg in "${args[@]}"; do
            deck_delete "$arg" "$force"
        done
        ;;
    --isdeck)
        deck_isdeck "$src"
        ;;
    --isdirty)
        deck_isdirty "$src"
        ;;
    --ismounted)
        deck_ismounted "$src"
        ;;
    --list-layers)
        deck_isdeck "$src" || fatal "not a deck: $src"
        mountpath=$(real_path "$src")
        deckdir="$(dirname "$mountpath")/.deck/$(basename "$mountpath")"
        echo "$deckdir/upper"
        tac "$deckdir/layers"
        ;;
    *)
        fatal "unrecognized option: $action"
        ;;
esac

exit 0
