#!/bin/sh

usage() {
    cat << EOF
usage: tmpoverlay [OPTIONS] [SOURCE...] DEST

Create a tmpfs-backed overlayfs at DEST starting with SOURCEs. If no SOURCE is
specified, use DEST as the source. To free the memory, simply umount DEST.

options:
  -c, --no-canonicalize  don't canonicalize paths
  -h, --help             print this help
  -o, --overlayfs OPTS   add overlayfs mount options, e.g. redirect_dir/metacopy
  -n, --no-mtab          don't write to /etc/mtab
  -N, --mount-name NAME  source name for mount (default "overlay")
  -t, --tmpfs OPTS       add tmpfs mount options, e.g. size
  -v, --verbose          verbose mode

examples:
  tmpoverlay / /new_root       # make a thin copy of root
  tmpoverlay /etc              # make read-only /etc writable
  tmpoverlay /a /b /c /merged  # merge /a, /b, /c, and a fresh tmpfs
EOF
}

log() {
    fmt=$1
    shift
    printf "tmpoverlay: $fmt\n" "$@" >&2
}

logv() {
    if [ -n "$verbose" ]; then
        log "$@"
    fi
}

die() {
    log "$@"
    exit 1
}

canon() {
    if [ -n "$no_canon" ]; then
        printf '%s\n' "$1"
    else
        realpath "$1"
    fi
}

my_getopt() {
    getopt \
        -l no-canonicalize \
        -l help \
        -l overlayfs: \
        -l no-mtab \
        -l mount-name: \
        -l tmpfs: \
        -l verbose \
        -n tmpoverlay \
        -- \
        cho:nN:t:v \
        "$@"
}

args=$(my_getopt "$@") || { usage >&2; exit 1; }
eval set -- "$args"
unset args

unset no_canon no_mtab extra_overlayfs_opts tmpfs_opts tmpdir verbose
while true; do
    case "$1" in
        -c|--no-canonicalize) no_canon=-c; shift;;
        -h|--help) usage; exit 0;;
        -o|--overlayfs)
            [ -n "$2" ] && extra_overlayfs_opts="$extra_overlayfs_opts,$2"
            shift 2
            ;;
        -n|--no-mtab) no_mtab=-n; shift;;
        -N|--mount-name) mount_name=$2; shift 2;;
        -t|--tmpfs)
            [ -n "$2" ] && tmpfs_opts="${tmpfs_opts:+$tmpfs_opts,}$2"
            shift 2
            ;;
        -v|--verbose) verbose=-v; shift;;
        --) shift; break;;
        *) die "getopt failure"
    esac
done

[ $# != 0 ] || { log 'no paths specified'; usage >&2; exit 1; }

unset lowerdir
while [ -n "$2" ]; do
    d=$(canon "$1")
    if [ -h "$d" ] || ! [ -d "$d" ]; then
        die 'source "%s" is not a directory' "$d"
    fi
    lowerdir=${lowerdir+:}$d
    shift
done

dest=$(canon "$1")
echo lowerdir=$lowerdir dest=$dest
[ -d "$dest" ] || die 'destination "%s" is not a directory' "$dest"
[ "$dest" != / ] || log 'overmounting root, use with caution'

if [ -z "$lowerdir" ]; then
    lowerdir=$dest
fi

err() {
    r=$?
    trap - INT TERM QUIT
    logv 'an error occurred, cleaning up'
    if [ -d "$tmpdir" ]; then
        if mountpoint -q "$tmpdir"; then
            logv 'unmounting tmpdir'
            umount $no_mtab "$tmpdir"
        fi
        logv 'deleting tmpdir'
        rmdir "$tmpdir"
    fi
    case $1 in
        [A-Z]*) logv 'reraising SIG%s' $1; kill -$1 $$ && read _
    esac
    logv 'exiting'
    if [ $r = 0 ]; then
        exit 1
    fi
    exit $r
}

trap 'err INT' INT
trap 'err TERM' TERM
trap 'err QUIT' QUIT

if [ -z "$tmpdir" ]; then
    logv 'creating tmpdir'
    tmpdir=$(umask 077; mktemp -dt tmpoverlay.XXXXXXXXXX) || exit 1
    logv 'created tmpdir: %s' "$tmpdir"
fi
mount -t tmpfs ${tmpfs_opts:+-o "$tmpfs_opts"} ${verbose:+-v} tmpfs "$tmpdir" || err

upperdir="$tmpdir/upper"
workdir="$tmpdir/work"
logv 'creating upper/work dirs'
mkdir "$upperdir" "$workdir" || err

# try to match perms/attrs. this is not race-free but it's impossible without
# atomic (CAS) chown/chmod/setfattr. chown --from is not atomic, not portable,
# and also doesn't cover chmod/setfattr.
lastlowerdir=${lowerdir##*:}
logv 'copying lowerdir owner to upperdir'
owner=$(stat -c %u:%g "$lastlowerdir") || err
chown $owner "$upperdir" || err
logv 'copying lowerdir perms to upperdir'
mode=$(stat -c %a "$lastlowerdir") || err
chmod $mode "$upperdir" || err
# -m - covers ACLs (system.posix_acl_access) and file caps
# (security.capability). theoretically someone might have get/setcap and/or
# get/setfacl but not get/setxattr, but this is unlikely since libcap/acl
# require attr.
logv 'copying root attrs'
if attr=$(cd "$lastlowerdir" && getfattr -d -m - . 2>/dev/null); then
    if [ -n "$attr" ]; then
        printf '%s\n' "$attr" | (cd "$upperdir"; setfattr --restore=-) || err
    fi
else
    log 'getfattr not found or failed, skipping xattrs'
fi

logv 'mounting overlay'
overlayfs_opts="lowerdir=$lowerdir,upperdir=$upperdir,workdir=$workdir,volatile$extra_overlayfs_opts"
mount -t overlay -o "$overlayfs_opts" $no_canon $no_mtab $verbose ${mount_name-overlay} "$dest" || err

logv 'cleaning up'
umount "$tmpdir" || exit
rmdir "$tmpdir" || exit

logv 'done, exiting'