#!/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.

VERSION=2.0rc+

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") [ -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=$(real_path "$1")
    local 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

    local 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"
        local 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"
    local lowerdir="$(cat "$deckdir/layers" | tr '\n' ':' | sed 's/:$//')"
    local options="lowerdir=$lowerdir,upperdir=$upperdir,workdir=$workdir"
    mount -t overlay overlay -o "$options" "$mountpath"
}

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

real_umount() {
    local 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 --recursive --force --lazy "$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 submount; do
                    umount "$submount" || fatal "unmounting busy path $submount failed"
                done
                umount "$mountpath" || fatal "Cannot unmount $mountpath"
            fi
        fi
    fi
}

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

deck_delete() {
    local mountpath=$(real_path "$1" 2>/dev/null)
    local force=$2
    local deckdir="$(dirname "$mountpath")/.deck/$(basename "$mountpath")"
    [[ -d "$mountpath" ]] || fatal 'mountpath does not exist'
    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
            real_umount "$mountpath" "$force"
        fi
        rm -rf "$deckdir"
    fi
    rmdir "$mountpath"
    rmdir --ignore-fail-on-non-empty "$(dirname "$deckdir")"
}

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

deck_isdeck() {
    [[ -d "$1" ]] || return 1
    local mountpath=$(real_path "$1")
    local 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=$(real_path "$1")
    [[ -d "$mountpath" ]] || fatal 'mountpath does not exist'
    mount | grep -q "^overlay on $mountpath type overlay" || 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
}

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

[[ ${#args[@]} -eq 0 ]] && usage
[[ ${#args[@]} -gt 2 ]] && fatal 'too many arguments'
[[ ${#opts[@]} -gt 1 ]] && fatal 'conflicting deck options'
[[ ${#opts[@]} -eq 0 ]] && opts=('--mount')
action=${opts[0]}

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

[ "$(id -u)" != "0" ] && fatal 'must be run as root'
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)
        deck_umount "$src" "$force"
        ;;
    -D|--delete)
        deck_delete "$src" "$force"
        ;;
    --isdeck)
        exit $(deck_isdeck "$src")
        ;;
    --isdirty)
        exit $(deck_isdirty "$src")
        ;;
    --ismounted)
        exit $(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
