#!/bin/sh

############################################################################
# zogftw (ZFS on geli for the win) 2025-02-23-48a7d58
#
# Automates backups from a ZFS pool to multiple external ones that use
# a single vdev. geli is used for full disk encryption, glabel is used
# so the external pools can be attached without having to specify the
# device name.
#
# Dependencies that aren't already in ElectroBSD's base system:
# gpg, gpg-agent, mbuffer, sudo
#
# Copyright (c) 2010-2024 Fabian Keil <fk@fabiankeil.de>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
############################################################################

# List the available functions and the arguments they take.
zogftw_get_function_prototypes() {
    local \
        zogftw_location

    zogftw_location="${0}"

    if [ "zogftw" != "$(basename "${zogftw_location}")" ]; then
        # Looks like zogftw has been sourced.
        # Try to get the zogftw location through the PATH.
        zogftw_location="$(which zogftw)"
    fi

    if [ "zogftw" != "$(basename "${zogftw_location}")" ]; then
        zogftw_wtf "Failed to figure out zogftw's location to" \
                   "gather the available functions"
        return 1
    fi

    awk '
    /^zogftw_/ {
        function_name = $1;
        next;
    }
    # Print prototypes for functions with parameters
    $1 ~ /^local$/ && $2 !~ /^\\$/ {
        parameters = $0;
        # Ditch keyword local
        gsub(" +local ", "", parameters);
        # Ditch trailing slash which is used to separate local
        # variables that store parameters and those that do not
        gsub(" \\\\", "", parameters);
        # Ditch assignments which are occasionally used if
        # there is only a single parameter
        gsub("=.*$", "", parameters);
        print function_name " " parameters;
        function_name = "";
        next;
    }
    # Print prototypes for functions without parameters
    {
        if (function_name != "") {
            print function_name;
            function_name = "";
        }
    }
    ' "${zogftw_location}" | sort
}

zogftw_wtf_message_emitted_hook() {
    # We can't use zogftw_show_hook_capabilities() here
    # as it would cause an endless recursion.
    if [ "${ZOGFTW_SHOW_AVAILABLE_HOOKS}" != 1 ]; then
        return 0
    fi
    echo 'zogftw_wtf_message_emitted_hook() has access to' \
         'the variable $complaints'
}

zogftw_wtf() {
    local complaints="${*}"
    if [ -z "${complaints}" ]; then
        complaints="zogftw_wtf(): No complaints?"
    fi
    zogftw_fyi "${complaints}" 1>&2
    zogftw_wtf_message_emitted_hook
    return 1
}

# Can be overwritten to create a prefix for log messages
zogftw_get_fyi_prefix() {
    true;
}

zogftw_fyi_message_emitted_hook() {
    # We can't use zogftw_show_hook_capabilities() here
    if [ "${ZOGFTW_SHOW_AVAILABLE_HOOKS}" != 1 ]; then
        return 0
    fi
    echo 'zogftw_fyi_message_emitted_hook() has access to' \
         'the variables $information $zogftw_fyi_prefix' \
         'and $log_message'
}

zogftw_fyi() {
    local information \
        zogftw_fyi_prefix log_message

    information="${*}"
    zogftw_fyi_prefix="$(zogftw_get_fyi_prefix)"
    log_message="${zogftw_fyi_prefix}${information}"

    echo "${log_message}"
    zogftw_fyi_message_emitted_hook information zogftw_fyi_prefix log_message
}

zogftw_sudo() {
    local cmd_to_run_with_sudo
    sudo $ZOGFTW_SUDO_FLAGS "${@}"
}

zogftw_show_hook_capabilities() {
    local hook_name variables \
        variable_name variable_content accessible_variables

    hook_name="${1}"
    variables="${2}"

    if [ "${ZOGFTW_SHOW_AVAILABLE_HOOKS}" != 1 ]; then
        return 0
    fi

    for variable_name in $variables;
    do
        if [ -n "${accessible_variables}" ]; then
            accessible_variables="${accessible_variables},"
        fi
        variable_content="$(eval echo "\$${variable_name}")"
        accessible_variables="${accessible_variables} ${variable_name}: '${variable_content}'"
    done

    zogftw_fyi "${hook_name}() has access to:${accessible_variables}"
}

# Replaces a pattern in a string with a replacement.
# The pattern is just a bunch of characters, not a regular expression.
zogftw_replace_pattern() {
    local pattern replacement string \
        left_side right_side

    pattern="${1}"
    replacement="${2}"
    string="${3}"

    if [ -z "${pattern}" ]; then
        zogftw_wtf "zogftw_replace: No pattern provided"
        return 1
    fi
    if [ -z "${replacement}" ]; then
        zogftw_wtf "zogftw_replace: No replacement provided"
        return 1
    fi
    if [ -z "${string}" ]; then
        zogftw_wtf "zogftw_replace: No string provided"
        return 1
    fi

    while [ "${string}" != "${string#*${pattern}}" ];
    do
        left_side="${string%%${pattern}*}"
        right_side="${string#*${pattern}}"
        string="${left_side}${replacement}${right_side}"
    done

    echo "${string}"
}

# ZFS dataset names may contain spaces which is inconvenient
# as zogftw already uses spaces as dataset delimiters. This
# function allows to escape spaces that are part of the name
# using %20.
#
# Given that ZFS does not allow percentage signs in dataset names,
# this function does not bother to allow escaped percentage signs
# either.
zogftw_unescape_spaces() {
    local string

    string="${1}"
    if [ -z "${string}" ]; then
        zogftw_wtf "zogftw_unescape_spaces: No string provided"
        return 1
    fi

    zogftw_replace_pattern "%20" " " "${string}"
}

# Replaces literal spaces with %20 which doesn't cause
# unintentional value splitting.
zogftw_escape_spaces() {
    local string

    string="${1}"
    if [ -z "${string}" ]; then
        zogftw_wtf "zogftw_escape_spaces: No string provided"
        return 1
    fi

    zogftw_replace_pattern " " "%20" "${string}"
}

# Changes the space escaping from "\ ", which is more convenient
# for a user, to "%20", which is more convenient for a shell script.
zogftw_change_space_escaping() {
    local string

    string="${1}"
    if [ -z "${string}" ]; then
        echo ""
        return 0
    fi

    zogftw_replace_pattern "\ " "%20" "${string}"
}


zogftw_get_all_snapshots() {
    local dataset \
        special_sorting zfs_list_cmd

    dataset="${1}"
    special_sorting=""

    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_get_all_snapshots(): No dataset provided"
        return 1
    fi

    if [ "${ZOGFTW_SORTING_SNAPSHOTS_BY_NAME_KEEPS_CHRONOLOGICAL_ORDER}" = 1 ]; then
        special_sorting="-s name"
    fi

    zfs list -t snapshot -H -o name $special_sorting -r -d 1 "${dataset}"
}

zogftw_get_last_snapshot_from_property() {
    local dataset \
        last_snapshot_property

    dataset="${1}"
    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_get_last_snapshot_from_property(): No dataset provided"
        return 1
    fi

    last_snapshot_property="$(zfs get -H -s local "de.fabiankeil:zogftw:last_snapshot" "${dataset}" | \
                              cut -w -f 3)"
    if [ -z "${last_snapshot_property}" -o "${last_snapshot}" = "-" ]; then
        # Looks like the last snapshot hasn't been stored as property (yet).
        return 0
    fi

    echo "${last_snapshot_property}"

    return 0
}

zogftw_delete_last_snapshot_property() {
    local dataset

    dataset="${1}"
    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_delete_last_snapshot_property(): No dataset provided"
        return 1
    fi

    zogftw_sudo zfs inherit "de.fabiankeil:zogftw:last_snapshot" "${dataset}"
}

zogftw_get_last_snapshot() {
    local dataset \
        last_snapshot

    dataset="${1}"
    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_get_last_snapshot(): No dataset provided"
        return 1
    fi

    if [ "${ZOGFTW_STORE_LAST_SNAPSHOT_AS_PROPERTY}" = 1 ]; then
        last_snapshot="$(zogftw_get_last_snapshot_from_property "${dataset}")"
        if [ -n "${last_snapshot}" ]; then
            echo "${last_snapshot}"
            return 0
        fi
    fi
    last_snapshot="$(zogftw_get_all_snapshots "${dataset}" | tail -n 1)"
    if [ -z "${last_snapshot}" ]; then
        return 1
    fi
    echo "${last_snapshot}"
    return 0
}

zogftw_get_first_snapshot() {
    local dataset \
        first_snapshot

    dataset="${1}"
    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_get_first_snapshot(): No dataset provided"
        return 1
    fi

    first_snapshot="$(zogftw_get_all_snapshots "${dataset}" | head -n 1)"
    if [ -z "${first_snapshot}" ]; then
        return 1
    fi
    echo "${first_snapshot}"
    return 0
}

zogftw_get_last_snapshot_name() {
    local dataset \
        last_snapshot last_snapshot_name

    dataset="${1}"
    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_get_last_snapshot_name(): No dataset provided"
        return 1
    fi

    last_snapshot="$(zogftw_get_last_snapshot "${dataset}")"
    if [ -z "${last_snapshot}" ]; then
        return 1
    fi
    last_snapshot_name="${last_snapshot##*@}"
    if [ "${last_snapshot}" = "${last_snapshot_name}" ]; then
        zogftw_wtf "zogftw_get_last_snapshot_name(): Failed to get name for ${last_snapshot}"
        return 1
    fi

    echo "${last_snapshot_name}"
    return 0
}

# Check whether or not the user has a given permission on
# a given dataset (in which case sudo isn't necessary).
#
# XXX: Does not sanity-check permissions and is thus only
# expected to work with permissions ZFS actually supports.
zogftw_user_has_zfs_permission() {
    local permission dataset \
        user_name

    permission="${1}"
    dataset="${2}"
    user_name="$(id -un)"

    if [ -z "${permission}" ]; then
        zogftw_wtf "zogftw_user_has_zfs_permission(): Missing permission to check"
        return 1
    fi
    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_user_has_zfs_permission(): Missing dataset to check"
        return 1
    fi
    if ! zogftw_dataset_does_exist "${dataset}"; then
        zogftw_wtf "zogftw_user_has_zfs_permission(): Dataset ${dataset} does not exist"
        return 1
    fi
    if [ "$(id -u)" = 0 ]; then
        return 0
    fi

    zfs allow "${dataset}" | grep "user ${user_name} " | grep -q "${permission}"
}

# To be able to "zfs receive" into bla/fasel/dieda, bla and bla/fasel
# have to exist already. This functions creates the missing datasets
# if there are any, but only uses sudo if necessary (with the exception
# noted below).
zogftw_create_missing_parent_datasets() {
    local dataset \
        parent_dataset zfs_create_cmd grandparent_dataset

    dataset="${1}"
    parent_dataset="${dataset%/*}"

    if [ -z "${parent_dataset}" ]; then
        # dataset has no parent, apparently some clown provided a pool name
        return 1
    fi

    if ! zogftw_dataset_does_exist "${parent_dataset}"; then
        zogftw_fyi "Creating ${parent_dataset} as parent for ${dataset}"

        # Build the zfs command. To properly deal with whitespace,
        # we can't use a named variable and have to use "${@}" instead.
        set -- zfs create ${ZOGFTW_ZFS_CREATE_FLAGS} "${parent_dataset}"

        # XXX: While this will err on the safe side if grandparent_dataset
        #      doesn't exist either, a better approach would be to check
        #      the permissions for the first existing dataset above the
        #      one the caller wants to create.
        grandparent_dataset="${parent_dataset%/*}"
        if ! zogftw_dataset_does_exist "${grandparent_dataset}" ||
           ! zogftw_user_has_zfs_permission create "${grandparent_dataset}"; then
            set -- zogftw_sudo "${@}"
        fi
        if ! "${@}"; then
            if zogftw_dataset_does_exist "${grandparent_dataset}"; then
                # We can continue as long as the dataset got created.
                #
                # For example it's not a big deal if the user removed
                # -u from the $ZOGFTW_ZFS_CREATE_FLAGS and mounting
                # fails due to lack of privileges or because the mount
                # point can't be created.
                zogftw_fyi "'${*}' wasn't entirely successful but" \
                           "'${parent_dataset}' got created. Moving on."
                return 0
            fi
            return 1
        fi
    fi
}

############

# No longer called internally, kept for backwards compatibility
zogftw_send_dataset_incrementally() {
    local src_dataset dest_dataset

    src_dataset="${1}"
    dest_dataset="${2}"

    if [ -z "${src_dataset}" ]; then
        zogftw_wtf "zogftw_send_dataset_incrementally(): Missing source dataset"
        return 1;
    fi

    if [ -z "${dest_dataset}" ]; then
        zogftw_wtf "zogftw_send_dataset_incrementally(): Missing destination dataset"
        return 1;
    fi

    zogftw_transfer_last_snapshot -I "${src_dataset}" "${dest_dataset}"
}

zogftw_snapshot_successfully_sent_hook() {
    zogftw_show_hook_capabilities 'zogftw_snapshot_successfully_sent_hook' \
        'src_dataset dest_dataset incremental_mode dest_zpool'
}

zogftw_transfer_is_not_necessary_last_snapshots_are_equal_hook() {
    zogftw_show_hook_capabilities \
        'zogftw_transfer_is_not_necessary_last_snapshots_are_equal_hook' \
        'dest_zpool incremental_mode src_dataset dest_dataset
         src_snapshot_name dest_snapshot_name'
}

zogftw_transfer_is_necessary_hook() {
    zogftw_show_hook_capabilities \
        'zogftw_transfer_is_necessary_hook' \
        'dest_zpool incremental_mode src_dataset dest_dataset
         src_snapshot_name dest_snapshot_name'
}

zogftw_transfer_is_impossible_no_snapshot_found_on_src_dataset_hook() {
    zogftw_show_hook_capabilities \
        'zogftw_transfer_is_impossible_no_snapshot_found_on_src_dataset_hook' \
        'src_dataset src_snapshot_name'
}

zogftw_pipe_buffer() {
    mbuffer $ZOGFTW_MBUFFER_FLAGS
}

zogftw_dataset_has_snapshots() {
    local dataset

    dataset="${1}"

    [ -n "$(zogftw_get_last_snapshot "${dataset}")" ]
}

# Creates the destination dataset by sending the first snapshot
# from the source dataset. Fails if the destination dataset
# already exists and contains snapshots.
zogftw_transfer_first_snapshot() {
    local src_dataset dest_dataset \
        src_snapshot dest_zpool

    src_dataset="${1}"
    dest_dataset="${2}"

    dest_zpool="${dest_dataset%%/*}" # Only used for the hook

    if [ -z "${src_dataset}" ]; then
        zogftw_wtf "zogftw_transfer_first_snapshot(): Missing source dataset"
        return 1;
    fi

    if [ -z "${dest_dataset}" ]; then
        zogftw_wtf "zogftw_transfer_first_snapshot(): Missing destination dataset"
        return 1;
    fi

    if ! zogftw_dataset_does_exist "${src_dataset}"; then
        zogftw_wtf "zogftw_transfer_first_snapshot(): Source dataset '${src_dataset}' does not exist"
        return 1
    fi

    if zogftw_dataset_does_exist "${dest_dataset}" && \
        zogftw_dataset_has_snapshots "${dest_dataset}"; then
        # An existing dataset without snapshots is acceptable as it may
        # be an empty parent dataset to be able to receive a child.
        #
        # If the dataset already contains snapshots, this function
        # shouldn't have been called, though, so this is treated as
        # error even though automatically wiping it would be possible.
        zogftw_wtf "zogftw_transfer_first_snapshot():" \
            "Destination dataset '${dest_dataset}' already exists and contains snapshots"
        return 1
    fi

    src_snapshot="$(zogftw_get_first_snapshot "${src_dataset}")"

    if [ -z "${src_snapshot}" ]; then
        zogftw_fyi "No snapshot to transfer found on '${src_dataset}'"
        return 1
    fi

    zogftw_create_missing_parent_datasets "${dest_dataset}" || return 1

    zogftw_sudo zfs send ${ZOGFTW_ZFS_SEND_FLAGS} "${src_snapshot}" \
        | zogftw_pipe_buffer | zogftw_sudo zfs receive $ZOGFTW_ZFS_INCREMENTAL_RECEIVE_FLAGS "${dest_dataset}"

    if [ $? = 0 ]; then
        zogftw_snapshot_successfully_sent_hook
    else
        return 1
    fi
}

# Transfers the last snapshot on the source dataset to the destination
# dataset. If the destination dataset doesn't exist yet, it is created
# first by receiving the first snapshot from the source dataset.
#
# The incremental mode controls whether or not incremental snapshots
# are sent.
zogftw_transfer_last_snapshot() {
    local incremental_mode src_dataset dest_dataset \
        dest_snapshot_name dest_zpool src_snapshot_name \
        missing_dataset_created

    incremental_mode="${1}"
    src_dataset="${2}"
    dest_dataset="${3}"

    dest_zpool="${dest_dataset%%/*}" # Only used for the hook
    missing_dataset_created=0

    case "${incremental_mode}" in
    "");; "-i");; "-I");;
    *)
        zogftw_wtf "zogftw_transfer_last_snapshot(): Invalid incremental mode: ${incremental_mode}"
        return 1
        ;;
    esac

    if [ -z "${src_dataset}" ]; then
        zogftw_wtf "zogftw_transfer_last_snapshot(): Missing source dataset"
        return 1;
    fi

    if [ -z "${dest_dataset}" ]; then
        zogftw_wtf "zogftw_transfer_last_snapshot(): Missing destination dataset"
        return 1;
    fi

    if ! zogftw_dataset_does_exist "${src_dataset}"; then
        zogftw_wtf "zogftw_transfer_last_snapshot(): Source dataset '${src_dataset}' does not exist"
        return 1
    fi

    src_snapshot_name="$(zogftw_get_last_snapshot_name "${src_dataset}")"
    if [ -z "${src_snapshot_name}" ]; then
        # Allow the hook to deal with this by creating a snapshot
        zogftw_transfer_is_impossible_no_snapshot_found_on_src_dataset_hook
    fi

    if [ -z "${src_snapshot_name}" ]; then
        zogftw_fyi "No snapshot to transfer found on '${src_dataset}'"
        return 1
    fi

    if ! zogftw_dataset_does_exist "${dest_dataset}"; then
        zogftw_fyi "Destination dataset '${dest_dataset}' doesn't seem to exist yet"
        zogftw_transfer_first_snapshot "${src_dataset}" "${dest_dataset}" || return 1
        missing_dataset_created=1
    fi

    dest_snapshot_name="$(zogftw_get_last_snapshot_name "${dest_dataset}")"

    if [ -z "${dest_snapshot_name}" ]; then
        zogftw_fyi "No snapshots found on '${dest_dataset}'. Starting from the beginning"
        zogftw_transfer_first_snapshot "${src_dataset}" "${dest_dataset}" || return 1
        missing_dataset_created=1
        dest_snapshot_name=$(zogftw_get_last_snapshot_name "${dest_dataset}")
    fi

    if [ -z "${dest_snapshot_name}" ]; then
        zogftw_wtf "zogftw_transfer_last_snapshot(): Failed again to get last snapshot on '${dest_dataset}'"
        return 1
    fi

    if [ "${src_snapshot_name}" = "${dest_snapshot_name}" ]; then
        # Only call the hook if nothing was sent at all
        if [ $missing_dataset_created = 0 ]; then
            zogftw_transfer_is_not_necessary_last_snapshots_are_equal_hook
        fi
        return 0
    fi

    zogftw_transfer_is_necessary_hook

    if [ "${ZOGFTW_STORE_LAST_SNAPSHOT_AS_PROPERTY}" = 1 ]; then
        # Remove property in case the transfer fails
        # after transferring more than one snapshot
        zogftw_delete_last_snapshot_property "${dest_dataset}"
    fi

    zogftw_sudo zfs send ${ZOGFTW_ZFS_SEND_FLAGS} $incremental_mode \
                "@${dest_snapshot_name}" "${src_dataset}@${src_snapshot_name}" \
        | zogftw_pipe_buffer | zogftw_sudo zfs receive $ZOGFTW_ZFS_INCREMENTAL_RECEIVE_FLAGS "${dest_dataset}"

    if [ $? = 0 ]; then
        if [ "${ZOGFTW_STORE_LAST_SNAPSHOT_AS_PROPERTY}" = 1 ]; then
            # Store the last snapshot as property so we can look it up
            # faster the next time we sync.
            zogftw_sudo zfs set \
                        "de.fabiankeil:zogftw:last_snapshot=${dest_dataset}@${src_snapshot_name}" \
                        "${dest_dataset}"
        fi
        zogftw_snapshot_successfully_sent_hook
    else
        return 1
    fi
}

# Get the receive_resume_token or fake a non-existent
# one if the property isn't available.
zogftw_get_receive_resume_token() {
    local dest_dataset="${1}" \
          receive_resume_token

    if [ -z "${dest_dataset}" ]; then
        zogftw_wtf "zogftw_get_receive_resume_token(): Missing destination dataset"
        return 1
    else
        receive_resume_token="$(zogftw_get_zfs_property_value \
                               receive_resume_token "${dest_dataset}" 2>/dev/null)"
    fi
    if [ -z "${receive_resume_token}" ]; then
        echo "-"
        return 1
    fi
    echo "${receive_resume_token}"
    return 0
}

# Confirm that the receive_resume_token matches a snapshot
# on the given src dataset, so we can be sure that passing
# the token to "zfs send -t" results in data being read
# from the dataset we expect.
zogftw_receive_resume_token_belongs_to_dataset() {
    local receive_resume_token src_dataset \
          parsed_token_content toguid toname \
          snapshot guid

    receive_resume_token="${1}"
    src_dataset="${2}"

    if [ -z "${receive_resume_token}" ]; then
        zogftw_wtf "zogftw_validate_receive_resume_token():"\
                   "Missing receive_resume_token"
        return 1
    fi
    if [ -z "${src_dataset}" ]; then
        zogftw_wtf "zogftw_validate_receive_resume_token():"\
                   "Missing source dataset"
        return 1
    fi
    if ! zogftw_dataset_does_exist "${src_dataset}"; then
        zogftw_wtf "zogftw_validate_receive_resume_token():"\
                   "Dataset ${src_dataset} does not exist"
        return 1
    fi

    parsed_token_content="$(zfs send -P -n -t "${receive_resume_token}" 2>&1)"
    if [ -z "${parsed_token_content}" ]; then
        zogftw_wtf "zogftw_validate_receive_resume_token():"\
                   "Failed to parse token"
        return 1
    fi

    toguid="$(echo "${parsed_token_content}" | awk '/toguid = / {print $3}')"
    if [ -z "${toguid}" ]; then
        zogftw_wtf "zogftw_validate_receive_resume_token():" \
                   "Failed to get toguid"
        return 1
    fi
    toguid="$(printf %u "${toguid}")" || return 1

    toname="$(echo "${parsed_token_content}" | awk '/toname = / {print $3}')"
    if [ -z "${toname}" ]; then
        zogftw_wtf "zogftw_validate_receive_resume_token():" \
                   "Failed to get toname"
        return 1
    fi

    for snapshot in $(zogftw_get_all_snapshots "${src_dataset}"); do
        if [ "${snapshot}" = "${toname}" ]; then
            guid="$(zogftw_get_zfs_property_value guid "${snapshot}")" || return 1
            if [ "${guid}" = "${toguid}" ]; then
                return 0
            fi
            zogftw_fyi "zogftw_validate_receive_resume_token():" \
                       "Found snapshot ${snapshot} but guid ${guid}" \
                       "does not match toguid ${toguid}"
            return 1
        fi
    done

    zogftw_wtf "zogftw_validate_receive_resume_token():" \
               "No snapshot ${toname} found on ${src_dataset}"
    return 1
}

# Resume a transfer using the given receive_resume_token.
#
# The token is considered trustworthy and not validated by zogftw in any way!
zogftw_resume_transfer() {
    local receive_resume_token dest_dataset

    receive_resume_token="${1}"
    dest_dataset="${2}"

    if [ -z "${receive_resume_token}" ]; then
        zogftw_wtf "zogftw_resume_transfer(): Missing receive_resume_token"
        return 1
    fi
    if [ -z "${dest_dataset}" ]; then
        zogftw_wtf "zogftw_resume_transfer(): Missing destination dataset"
        return 1
    fi

    zogftw_fyi "Resuming transfer to ${dest_dataset} using token ${receive_resume_token}"
    zogftw_sudo zfs send ${ZOGFTW_ZFS_SEND_FLAGS} -t "${receive_resume_token}" |
        zogftw_pipe_buffer |
        zogftw_sudo zfs receive $ZOGFTW_ZFS_INCREMENTAL_RECEIVE_FLAGS "${dest_dataset}"
}

# Resume a transfer after getting the receive_resume_token
# from the given destination dataset.
#
# The token is considered trustworthy and not validated by zogftw in any way!
zogftw_auto_resume() {
    local dest_dataset \
          receive_resume_token

    dest_dataset="${1}"

    if [ -z "${dest_dataset}" ]; then
        zogftw_wtf "zogftw_auto_resume(): Missing destination dataset"
        return 1
    fi

    receive_resume_token="$(zogftw_get_receive_resume_token ${dest_dataset})"
    if [ $? != 0 ]; then
        zogftw_wtf "zogftw_auto_resume(): No receive_resume_token" \
                   "found on ${dest_dataset}"
        return 1
    fi

    zogftw_resume_transfer "${receive_resume_token}" "${dest_dataset}"
}

zogftw_dataset_does_exist() {
    local dataset="${1}"

    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_dataset_does_exist(): No dataset provided"
        return 1
    fi

    zfs list -H -o name -s name "${dataset}" >/dev/null 2>&1
}

zogftw_zpool_does_exist() {
    local zpool_name

    zpool_name="${1}"
    if [ -z "${zpool_name}" ]; then
        zogftw_wtf "zogftw_zpool_does_exist(): No zpool provided"
        return 1
    fi
    zpool list -H -o name "${zpool_name}" > /dev/null 2>&1
}

# This function is a bit silly and thus no longer used by zogftw itself.
zogftw_get_zpool_size() {
    local dest_zpool="${1}"

    if [ -z "${dest_zpool}" ]; then
        zogftw_wtf "zogftw_get_zpool_size(): No zpool name provided"
        return 1
    fi

    zpool list -H -o size "${dest_zpool}" |
        awk '/^[0-9.]+T$/ {factor=1024*1024}
             /^[0-9.]+G$/ {factor=1024}
             /^[0-9.]+M$/ {factor=1}
             /^[0-9.]+[TGM]$/ {
                  split($0, a, "[TGM]$");
                  size = a[1] * factor;
                  print int(size)
             }'
}

zogftw_zpool_is_puny() {
    local zpool_name \
        zpool_size_bytes zpool_size_mega_bytes

    zpool_name="${1}"
    zpool_size_bytes="$(zpool list -H -p -o size "${zpool_name}")"

    if [ -z "${zpool_size_bytes}" ]; then
        zogftw_wtf "zogftw_zpool_is_puny(): Can't figure out the zpool size for '${zpool_name}'"
        return 1
    fi

    zpool_size_mega_bytes="$(expr "${zpool_size_bytes}" / 1024 / 1024 2>/dev/null)"

    if [ -z "${zpool_size_mega_bytes}" ]; then
        zogftw_wtf "zogftw_zpool_is_puny(): Can't figure out the zpool size for '${zpool_name}'"
        return 1
    fi

    [ "${zpool_size_mega_bytes}" -lt "${ZOGFTW_MAX_SPACE_CONSTRAINED_ZPOOL_SIZE}" ]
}

zogftw_echo_list_members_with_attribute() {
    local attribute list \
        list_member

    # $list is kept around for zogftw_get_function_prototypes().
    attribute="${1}"; shift

    for list_member in "${@}"
    do
        echo "${list_member} ${attribute}"
    done
}

zogftw_get_src_dataset_information() {
    zogftw_echo_list_members_with_attribute 'required' \
        $(zogftw_change_space_escaping "${ZOGFTW_REQUIRED_SRC_DATASETS}")
    zogftw_echo_list_members_with_attribute 'optional' \
        $(zogftw_change_space_escaping "${ZOGFTW_OPTIONAL_SRC_DATASETS}")
    zogftw_echo_list_members_with_attribute 'external' \
        $(zogftw_change_space_escaping "${ZOGFTW_EXTERNAL_SRC_DATASETS}")
}

# Emits the sorted and deduplicated datasets with their attributes.
zogftw_get_sorted_src_dataset_information() {
    local \
        dataset attribute previous_dataset

    # We deduplicate the reverse sorted dataset information
    # so the attribute with the highest priority (required>optional>external)
    # comes first. We revert the reversion again to make sure parent datasets
    # come before their children.
    zogftw_get_src_dataset_information | sort -k 1,2 -r | while read dataset attribute;
    do
        if [ "${previous_dataset}" != "${dataset}" ]; then
            echo "${dataset}" "${attribute}"
        fi
        previous_dataset="${dataset}"
    done | sort
}

zogftw_sync_zpool_hook() {
    zogftw_show_hook_capabilities \
        'zogftw_sync_zpool_hook' \
        'dest_zpool required_parent_dataset incremental_mode'
}

zogftw_zpool_gets_new_datasets() {
    local dest_zpool \
          required_parent_dataset new_dataset_property

    dest_zpool="${1}"
    if [ -z "${dest_zpool}" ]; then
        zogftw_wtf "zogftw_zpool_gets_new_datasets(): No pool specified"
        return 1
    fi

    required_parent_dataset="${dest_zpool}/${ZOGFTW_DEST_POOL_PREFIX}"
    if ! zogftw_dataset_does_exist "${required_parent_dataset}"; then
        zogftw_wtf "zogftw_zpool_gets_new_datasets():" \
                   "'${required_parent_dataset}' doesn't exist on pool '${dest_zpool}'"
        return 1
    fi

    new_dataset_property="$(zogftw_get_zfs_property_value \
        "de.fabiankeil:zogftw:new_datasets" "${required_parent_dataset}" 2>/dev/null)"
    if [ -z "${new_dataset_property}" -o "${new_dataset_property}" = "-" ]; then
        # Default for backwards-compatibility
        return 0
    elif [ "${new_dataset_property}" = "yes" ]; then
        return 0
    fi

    return 1
}

zogftw_sync_zpool() {
    local dest_zpool \
        required_parent_dataset actual_incremental_mode \
        incremental_mode incremental_mode_override \
        src_dataset dest_dataset dataset_type last_dest_snapshot \
        receive_resume_token zpool_gets_new_datasets

    dest_zpool="${1}"
    if [ -z "${dest_zpool}" ]; then
        zogftw_wtf "zogftw_sync_zpool(): No destination pool specified"
        return 1
    fi

    if ! zogftw_zpool_does_exist "${dest_zpool}"; then
        zogftw_wtf "zogftw_sync_zpool(): Destination pool '${dest_zpool}' does not exist"
        return 1
    fi

    required_parent_dataset="${dest_zpool}/${ZOGFTW_DEST_POOL_PREFIX}"
    if ! zogftw_dataset_does_exist "${required_parent_dataset}"; then
        zogftw_wtf "'${required_parent_dataset}' doesn't exist. Skipping pool '${dest_zpool}'"
        return 1
    fi

    if zogftw_zpool_is_puny "${dest_zpool}"; then
        # Only send the diff between the current and the
        # last snapshot, as the medium is space-constrained
        incremental_mode='-i'
    else
        # Send all the snapshots
        incremental_mode='-I'
    fi

    if zogftw_zpool_gets_new_datasets "${dest_zpool}"; then
        zpool_gets_new_datasets=true
    else
        zpool_gets_new_datasets=false
    fi

    zogftw_sync_zpool_hook

    zogftw_get_sorted_src_dataset_information | while read src_dataset dataset_type
    do
        src_dataset="$(zogftw_unescape_spaces "${src_dataset}")"
        if ! zogftw_dataset_does_exist "${src_dataset}"; then
            if [ "${dataset_type}" = "required" -o "${dataset_type}" = "optional" ]; then
                zogftw_wtf "Can't find ${dataset_type} source dataset '${src_dataset}'"
                return 1
            else
                if [ "${dataset_type}" != "external" ]; then
                    zogftw_wtf "Invalid dataset type ${dataset_type} for src dataset '${srcdataset}'"
                    return 1
                elif [ "${ZOGFTW_LOG_SKIPPED_DATASETS}" = 1 ]; then
                    zogftw_fyi "Skipping external src dataset '${src_dataset}': not available"
                fi
                continue
            fi
        fi

        dest_dataset="${dest_zpool}/${ZOGFTW_DEST_POOL_PREFIX}/${src_dataset}"

        if [ "${dataset_type}" = "required" ] && ! "${zpool_gets_new_datasets}"; then
            if ! zogftw_dataset_does_exist "${dest_dataset}" ||
               ! zogftw_dataset_has_snapshots "${dest_dataset}"; then
                if [ "${ZOGFTW_LOG_SKIPPED_DATASETS}" = 1 ]; then
                    zogftw_fyi "Skipping ${dataset_type} src dataset '${src_dataset}':" \
                               "pool '${dest_zpool}' does not get new datasets."
                fi
                continue
            fi
        fi

        if zogftw_dataset_does_exist "${dest_dataset}"; then
            receive_resume_token=$(zogftw_get_receive_resume_token "${dest_dataset}")
            if [ "${receive_resume_token}" != "-" ]; then
                if ! zogftw_receive_resume_token_belongs_to_dataset \
                     "${receive_resume_token}" "${src_dataset}"; then
                    zogftw_wtf "Token '${receive_resume_token}' appears to be invalid for ${src_dataset}"
                    return 1
                fi
                zogftw_resume_transfer "${receive_resume_token}" "${dest_dataset}" || return 1
                # The snapshot whose transfer we just successfully resumed
                # may not have been the last one, thus we treat the dataset
                # as if we didn't sync anything yet.
            fi
        fi

        if [ "${dataset_type}" = "required" ] || zogftw_dataset_does_exist "${dest_dataset}"; then
            if [ "${dataset_type}" != "required" ]; then
                # If there's a required dataset bla/fasel/dieda and an optional
                # dataset bla/fasel, the first synchronization will create bla/fasel
                # on the destination zpool to be able to receive bla/fasel/dieda.
                #
                # The bla/fasel on the destination zpool has no snapshots and is
                # unrelated to the dataset on the source zpool with the same name.
                #
                # Thus usually if a snapshotless dataset is found on the receiving
                # zpool there's nothing to do here and we are done, unless we are
                # dealing with a required dataset.
                #
                # If it is a required dataset, not having any snapshots on the
                # destination dataset is an error. The user is free to completely
                # replace the snapshotless dataset by adding '-F' to the
                # $ZOGFTW_ZFS_INCREMENTAL_RECEIVE_FLAGS
                last_dest_snapshot="$(zogftw_get_last_snapshot "${dest_dataset}")"
                if [ -z "${last_dest_snapshot}" ]; then
                    if [ "${ZOGFTW_LOG_SKIPPED_DATASETS}" = 1 ]; then
                        zogftw_fyi "Skipping ${dataset_type} src dataset '${src_dataset}':" \
                                   "No snapshots found on '${dest_dataset}'"
                    fi
                    continue
                fi
            fi

            incremental_mode_override="$(zogftw_get_zfs_property_value \
                    "de.fabiankeil:zogftw:incremental-mode" "${dest_dataset}" 2>/dev/null)"
            if [ -n "${incremental_mode_override}" -a "${incremental_mode_override}" != "-" ]; then
                # Use the incremental mode configured on the destination dataset.
                actual_incremental_mode="${incremental_mode_override}"
            else
                actual_incremental_mode="${incremental_mode}"
            fi

            zogftw_transfer_last_snapshot "${actual_incremental_mode}" "${src_dataset}" "${dest_dataset}" || return 1
        elif [ "${ZOGFTW_LOG_SKIPPED_DATASETS}" = 1 ]; then
            zogftw_fyi "Skipping ${dataset_type} src dataset '${src_dataset}': '${dest_dataset}' not found"
        fi
    done
}

zogftw_get_sync_worthy_zpools() {
    local \
        sync_worthy_zpools required_parent_dataset IFS

    sync_worthy_zpools=""
    IFS="
"
    for zpool_name in $(zpool list -H -o name);
    do
        if [ "${zpool_name}" != "$(zogftw_escape_spaces "${zpool_name}")" ]; then
            zogftw_wtf "Skipping unsupported ZFS pool '${zpool_name}'"
            continue
        fi
        required_parent_dataset="${zpool_name}/${ZOGFTW_DEST_POOL_PREFIX}"
        if zogftw_dataset_does_exist "${required_parent_dataset}"; then
            sync_worthy_zpools="${sync_worthy_zpools} ${zpool_name}"
        fi
    done
    if [ -n "${sync_worthy_zpools}" ]; then
        # Drop the leading whitespace added by the first loop iteration
        sync_worthy_zpools="${sync_worthy_zpools# }"
        echo "${sync_worthy_zpools}"
    fi
}

zogftw_sync() {
    local dest_zpool \
        zpool_candidates

    dest_zpool="${1}"
    if [ -z "${dest_zpool}" ]; then
        zpool_candidates="$(zogftw_get_sync_worthy_zpools)"
        if [ -z "${zpool_candidates}" ]; then
            zogftw_wtf "No destination pool specified and no prepared pools detected"
            return 1
        fi
        zogftw_fyi "No destination pool specified. Synchronizing all" \
                   "pools prepared for receiving: ${zpool_candidates}"
    else
        zpool_candidates="${dest_zpool}"
    fi

    for zpool_candidate in ${zpool_candidates};
    do
        zogftw_sync_zpool "${zpool_candidate}" || return 1
    done
}

############

zogftw_get_dataset_from_path() {
    local path="${1}"

    if [ ! -d "${path}" -o ! -x "${path}" ]; then
        return 1
    fi
    # Make sure the path is absolute
    path="$(cd "${path}"; pwd)"
    zfs list -H -o name -s name "${path}"
}

zogftw_dataset_has_been_specified_by_path() {
    local potential_path="${1}"
    echo "${potential_path}" | egrep -q '^[/.]|/$' || test -d "${potential_path}"
}

zogftw_snapshot_not_yet_created_hook() {
    zogftw_show_hook_capabilities 'zogftw_snapshot_not_yet_created_hook' \
        'dataset snapshot_name'
}

zogftw_snapshot_successfully_created_hook() {
    zogftw_show_hook_capabilities 'zogftw_snapshot_successfully_created_hook' \
        'dataset snapshot snapshot_name'
}

zogftw_get_zfs_property_value() {
    local value dataset

    value="${1}"
    dataset="${2}"

    if [ -z "${value}" ]; then
        zogftw_wtf "zogftw_get_zfs_property_value(): No property provided"
        return 1
    fi
    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_get_zfs_property_value(): No dataset provided"
        return 1
    fi
    zfs get -H -o value "${value}" "${dataset}"
}

zogftw_get_new_snapshot_name() {
    date "+%Y-%m-%d_%H:%M"
}

zogftw_normalize_dataset() {
    local dataset="${1}"
    if zogftw_dataset_has_been_specified_by_path "${dataset}"; then
        zogftw_get_dataset_from_path "${dataset}" 2>/dev/null
        if [ $? != 0 ]; then
            zogftw_wtf "zogftw_normalize_dataset():" \
                       "${dataset} is neither a valid dataset name" \
                       "nor an existing directory you have access to"
            return 1
        fi
    else
        echo "${dataset}"
    fi
    return 0
 }

zogftw_snapshot_dataset() {
    local dataset \
        snapshot_name snapshot

    dataset="${1}"
    snapshot_name="$(zogftw_get_new_snapshot_name)"

    if [ -z "${dataset}" ]; then
        zogftw_wtf "zogftw_snapshot_dataset(): No dataset to snapshot provided."
        return 1;
    fi

    dataset="$(zogftw_normalize_dataset "${dataset}")" || return 1

    if ! zogftw_dataset_does_exist "${dataset}"; then
        zogftw_wtf "Dataset '${dataset}' does not exist"
        return 1
    fi

    # Allow the hook to abort the snapshotting and
    # don't consider this a failure.
    if ! zogftw_snapshot_not_yet_created_hook; then
        return 0
    fi

    snapshot="${dataset}@${snapshot_name}"
    set -- zfs snapshot "${snapshot}"

    if ! zogftw_user_has_zfs_permission snapshot "${dataset}"; then
        set -- zogftw_sudo "${@}"
    fi

    "${@}"
    if [ $? != 0 ]; then
        return 1
    fi
    zogftw_snapshot_successfully_created_hook
}

zogftw_snapshot() {
    local dataset="${1}"
    if [ -z "${dataset}" ]; then
        if [ -z "${ZOGFTW_DEFAULT_SNAPSHOT_DATASETS}" ]; then
            zogftw_wtf "zogftw_snapshot(): No dataset to snapshot" \
                       "provided and no default specified."
            return 1;
        fi
        datasets_to_snapshot="$(zogftw_change_space_escaping "${ZOGFTW_DEFAULT_SNAPSHOT_DATASETS}")"
        zogftw_fyi "No dataset to snapshot specified. Snapshotting: ${ZOGFTW_DEFAULT_SNAPSHOT_DATASETS}"
    else
        # Make sure the following for loop doesn't treat spaces as delimiters.
        datasets_to_snapshot="$(zogftw_escape_spaces "${dataset}")"
    fi

    for dataset_to_snapshot in $datasets_to_snapshot;
    do
        dataset_to_snapshot="$(zogftw_unescape_spaces "${dataset_to_snapshot}")"
        zogftw_snapshot_dataset "${dataset_to_snapshot}" || return 1
    done
}

############

zogftw_get_passphrase() {
    local passphrase_file \
        gpg_agent_flag

    passphrase_file="${1}"
    if [ ! -f "${passphrase_file}" ]; then
        zogftw_wtf "zogftw_get_passphrase: Passphrase file '${passphrase_file}' does not exist"
        return 1
    fi

    if [ "${ZOGFTW_USE_GPG_AGENT}" = 1 ]; then
        gpg_agent_flag="--use-agent"
    fi

    gpg --decrypt $ZOGFTW_GPG_DECRYPT_FLAGS $gpg_agent_flag "${passphrase_file}"
}

# Makes sure the sudo password is cached so sudo and
# gpg don't request input at the same time later on
zogftw_prime_sudo() {
    zogftw_sudo -v || return 1
}

zogftw_geli_attach() {
    local zpool_name \
        passphrase_file keyfile geli_attach_flags

    zpool_name="${1}"
    passphrase_file="${ZOGFTW_GELI_PASSPHRASE_DIR}/${zpool_name}.gpg"
    keyfile="${ZOGFTW_GELI_KEYFILE_DIR}/${zpool_name}.key"

    if [ -f "${keyfile}" ]; then
        zogftw_fyi "Using geli keyfile ${keyfile}"
        set -- "-k" "${keyfile}" "/dev/label/${zpool_name}"
    elif [ -e "${keyfile}" ]; then
        # This is reached if ${keyfile} is a symlink whose target got lost.
        zogftw_fyi "Potential keyfile '${keyfile}' exists but can't be accessed as file."
        return 1
    else
        set -- "/dev/label/${zpool_name}"
    fi

    zogftw_prime_sudo || return 1

    if [ -f "${passphrase_file}" ]; then
        zogftw_get_passphrase "${passphrase_file}" | zogftw_sudo geli attach -j - "${@}" || return 1
    else
        zogftw_sudo geli attach "${@}" || return 1
    fi

    zogftw_fyi "'${zpool_name}' attached"
}

zogftw_zpool_import_successful_hook() {
    zogftw_show_hook_capabilities 'zogftw_zpool_import_successful_hook' \
        'zpool_name'
}

zogftw_import_zpool() {
    local zpool_name \
        force_flag no_mount_flag

    while [ -n "${1}" ]; do
        if [ "${1}" = "-f" ]; then
            force_flag="-f"
            shift
        elif [ "${1}" = "-N" ]; then
            no_mount_flag="-N"
            shift
        else
            break
        fi
    done

    if [ -n "${1}" ]; then
        zpool_name="${1}"
    fi

    if ! zogftw_unattached_label_exists "${zpool_name}"; then
        zogftw_wtf "zogftw_import_zpool(): No label for '${zpool_name}' found"
        return 1
    fi

    zogftw_geli_attach "${zpool_name}" || return 1

    if zogftw_zpool_does_exist "${zpool_name}"; then
        # If the zpool already exists, we can skip the import.
        zogftw_fyi "zpool '${zpool_name}' already exists. Import skipped."
    else
        zogftw_sudo zpool import ${force_flag} ${no_mount_flag} -d /dev/label "${zpool_name}" || return 1
        zogftw_fyi "'${zpool_name}' imported"
        zogftw_zpool_import_successful_hook
    fi
}

zogftw_geli_geom_exists() {
    local geli_geom

    geli_geom="${1}"

    if [ -z "${geli_geom}" ]; then
        zogftw_wtf "zogftw_geli_geom_exists: No geli geom to check provided"
        return 1
    fi
    geli status -s "${geli_geom}" >/dev/null 2>&1
}

zogftw_get_unattached_labels() {
    local \
        label unattached_labels

    unattached_labels=""

    for label in $(glabel status -s | awk '/^ *label/ {print $1}');
    do
        if ! zogftw_geli_geom_exists "${label}.eli"; then
            unattached_labels="${unattached_labels} ${label##label/}"
        fi
    done

    if [ -z "${unattached_labels}" ]; then
        return 1
    fi

    # Drop the leading whitespace added by the first loop iteration
    unattached_labels="${unattached_labels# }"

    echo "${unattached_labels}"
}

zogftw_unattached_label_exists() {
    local label \
        unattached_label

    label="${1}"

    if [ -z "${label}" ]; then
        zogftw_wtf "zogftw_unattached_label_exists(): No label provided"
        return 1
    fi

    for unattached_label in $(zogftw_get_unattached_labels);
    do
        if [ "${label}" = "${unattached_label}" ]; then
            return 0
        fi
    done

    return 1
}

zogftw_import_got_no_labels_to_attach_hook() {
    zogftw_show_hook_capabilities 'zogftw_import_got_no_labels_to_attach_hook' 'labels_to_attach zpool_name'
}

zogftw_import_got_labels_to_attach_hook() {
    zogftw_show_hook_capabilities 'zogftw_import_got_labels_to_attach_hook' 'labels_to_attach zpool_name'
}

zogftw_import() {
    local zpool_name \
        force_flag no_mount_flag labels_to_attach gpg_agent_started return_code

    while [ -n "${1}" ]; do
        if [ "${1}" = "-f" ]; then
            force_flag="-f"
            shift
        elif [ "${1}" = "-N" ]; then
            no_mount_flag="-N"
            shift
        else
            break
        fi
    done

    if [ -n "${1}" ]; then
        zpool_name="${1}"
    fi

    gpg_agent_started=0
    labels_to_attach="$(zogftw_get_unattached_labels)"
    return_code=0

    if [ -z "${labels_to_attach}" ]; then
        zogftw_fyi "No labels to attach found."
        zogftw_import_got_no_labels_to_attach_hook
        if [ -z "${labels_to_attach}" ]; then
            return 1;
        fi
    fi

    if [ -z "${zpool_name}" ]; then
        zogftw_fyi "No pool name specified. Trying all unattached labels: ${labels_to_attach}"
    else
        # XXX: Should make sure the label exists
        labels_to_attach="${zpool_name}"
    fi
    zogftw_import_got_labels_to_attach_hook

    if [ "${ZOGFTW_START_GPG_AGENT}" = 1 -a -z "${GPG_AGENT_INFO}" ]; then
        eval "$(gpg-agent --daemon)"
        if [ -n "${GPG_AGENT_INFO}" ]; then
            gpg_pid="${GPG_AGENT_INFO#*:}"
            gpg_pid="${gpg_pid%:*}"
            gpg_agent_started=1
        fi
    fi

    for zpool_name in $labels_to_attach;
    do
        zpool_name="$(basename "${zpool_name}")"
        if [ -c /dev/label/"${zpool_name}".eli ]; then
            zogftw_fyi "'${zpool_name}' is already attached. Skipping."
        else
            zogftw_import_zpool ${force_flag} ${no_mount_flag} "${zpool_name}"
            if [ $? != 0 ]; then
                return_code=1
            fi
        fi
    done

    if [ "${gpg_agent_started}" = 1 ]; then
        kill "${gpg_pid}" || return 1
    fi

    return $return_code
}

###########

zogftw_exporting_external_zpool_hook() {
    zogftw_show_hook_capabilities 'zogftw_exporting_external_zpool_hook' \
        'external_zpool labeled_vdev'
}

zogftw_exporting_external_zpool_failed_hook() {
    zogftw_show_hook_capabilities 'zogftw_exporting_external_zpool_failed_hook' \
        'external_zpool labeled_vdev export_failure'
}

zogftw_zpool_export_successful_hook() {
    zogftw_show_hook_capabilities 'zogftw_zpool_export_successful_hook' \
        'external_zpool'
}

zogftw_export_external_zpool() {
    local external_zpool \
        force_flag labeled_vdev export_failure

    if [ "${1}" = "-f" ]; then
        force_flag="-f"
        shift
    fi
    external_zpool="${1}"
    labeled_vdev="label/${external_zpool}.eli"

    if ! zogftw_dataset_does_exist "${external_zpool}"; then
        zogftw_wtf "Can't export ${external_zpool}: No such pool"
        return 1
    fi

    zogftw_fyi "Exporting ${external_zpool}"
    zogftw_exporting_external_zpool_hook
    zogftw_sudo zpool export ${force_flag} "${external_zpool}"
    export_failure=$?

    if [ "${export_failure}" != 0 ]; then
        zogftw_exporting_external_zpool_failed_hook
    fi
    if [ "${export_failure}" != 0 ]; then
        return 1
    fi

    if zogftw_geli_geom_exists "${labeled_vdev}"; then
        zogftw_sudo geli detach "${labeled_vdev}"
        zogftw_zpool_export_successful_hook
    else
        zogftw_fyi "${labeled_vdev} is already gone!"
    fi
}

zogftw_get_exportable_zpools() {
    local \
        label zpool_name exportable_pools

    exportable_pools=""
    for label in $(geli status -s | awk '/^ *label/ {print $1}');
    do
        zpool_name="${label%%.eli}"
        zpool_name="${zpool_name##label/}"
        if zpool status "${zpool_name}" 2>/dev/null | grep -A 2 NAME | grep -q "${label}"; then
            exportable_pools="${exportable_pools} ${zpool_name}"
        fi
    done

    if [ -z "${exportable_pools}" ]; then
        return 1
    fi

    # Drop the leading whitespace added by the first loop iteration
    exportable_pools="${exportable_pools# }"

    echo "${exportable_pools}"
}

zogftw_export() {
    local zpools_to_export \
        external_zpool force_flag

    if [ "${1}" = "-f" ]; then
        force_flag="-f"
        shift
    fi
    zpools_to_export="${1}"

    if [ -z "${zpools_to_export}" ]; then
        zpools_to_export=$(zogftw_get_exportable_zpools)
        if [ -z "${zpools_to_export}" ]; then
            zogftw_fyi "No zpool specified and no exportable zpools detected"
            return 0
        fi
        zogftw_fyi "No zpool specified. Exporting all external ones: ${zpools_to_export}"
    fi

    for external_zpool in $zpools_to_export;
    do
        zogftw_export_external_zpool ${force_flag} "${external_zpool}"
    done
}

zogftw_foreach_zpool() {
    local zpool_subcommand prefix_args postfix_args \
        external_zpools external_zpool

    zpool_subcommand="${1}"
    prefix_args="${2}"
    postfix_args="${3}"

    if [ -z "${zpool_subcommand}" ]; then
        zogftw_wtf "No zpool subcommand specified"
        return 1
    fi

    external_zpools="$(zogftw_get_exportable_zpools)"
    if [ -z "${external_zpools}" ]; then
        zogftw_wtf "No imported zpools managed by zogftw detected"
        return 1
    fi

    for external_zpool in $external_zpools;
    do
        # XXX: This log message has superfluous white-space if the args aren't specified.
        zogftw_fyi "Executing: zpool ${zpool_subcommand} ${prefix_args} ${external_zpool} ${postfix_args}"
        # The $(prefix|postfix)_args are intentionally not quoted to allow whitespace splitting
        zogftw_sudo zpool "${zpool_subcommand}" ${prefix_args} "${external_zpool}" ${postfix_args} || return
    done
}

###########

zogftw_clear_device() {
    local device_name="${1}"

    if [ -z "${device_name}" ]; then
        zogftw_wtf "zogftw_clear_device(): No device to clear provided"
        return 1
    fi

    zogftw_fyi "Clearing ${device_name}. Feel free to abort this with ctrl-C"
    zogftw_sudo dd bs=1M if=/dev/zero of="${device_name}"
}

zogftw_label_device() {
    local device_label device_name

    device_label="${1}"
    device_name="${2}"

    if [ -z "${device_label}" ]; then
        zogftw_wtf "zogftw_label_device(): No label provided"
        return 1
    fi
    if [ -z "${device_name}" ]; then
        zogftw_wtf "zogftw_label_device(): No device to label provided"
        return 1
    fi
    if [ "${device_label}" != "$(zogftw_escape_spaces "${device_label}")" ]; then
        # While FreeBSD (11.0-CURRENT r271610) can create labels with spaces,
        # tasting such labels fails. As glabel still reports success, we have
        # to check for spaces manually.
        zogftw_wtf "Label '${device_label}' contains spaces which is currently unsupported"
        return 1
    fi

    zogftw_fyi "Labeling ${device_name} ${device_label}"
    zogftw_sudo glabel label "${device_label}" "${device_name}"
}

zogftw_prepared_pool_for_receiving_hook() {
    zogftw_show_hook_capabilities 'zogftw_prepared_pool_for_receiving_hook' \
        'dest_zpool backup_dataset'
}

zogftw_prepare_pool_for_receiving() {
    local dest_zpool="${1}"

    backup_dataset="${dest_zpool}/${ZOGFTW_DEST_POOL_PREFIX}"
    zogftw_fyi "Creating backup structure ${backup_dataset}"
    zogftw_sudo zfs create $ZOGFTW_ZFS_CREATE_FLAGS "${backup_dataset}" || return 1
    zogftw_sudo zfs set compression=$ZOGFTW_BACKUP_DATASET_COMPRESSION "${backup_dataset}" || return 1
    zogftw_prepared_pool_for_receiving_hook
}

zogftw_request_passphrase() {
    local prompt passphrase_variable

    prompt="${1}"
    passphrase_variable="${2}"

    echo -n "${prompt}"
    stty -echo || return 1
    # This looks like an error, but ${passphrase_variable}
    # contains the name of the variable to set.
    read "${passphrase_variable}" || return 1
    stty echo || return 1
    echo
}

zogftw_create_encrypted_geli_passphrase_file() {
    local passphrase_file \
        passphrase confirmed_passphrase return_code

    passphrase_file="${1}"
    return_code=0

    if [ -z "${passphrase_file}" ]; then
        zogftw_wtf "No passphrase file provided"
        return 1
    fi

    if [ -f "${passphrase_file}" ]; then
        zogftw_wtf "Passphrase file '${passphrase_file}' already exists"
        return 1
    fi

    zogftw_request_passphrase "Enter geli passphrase: "   passphrase
    zogftw_request_passphrase "Reenter geli passphrase: " confirmed_passphrase

    if [ "${passphrase}" != "${confirmed_passphrase}" ]; then
        zogftw_wtf "Passphrases do not match!"
        return_code=1
    else
        zogftw_fyi "Writing passphrase to ${passphrase_file}"
        builtin echo "${passphrase}" | gpg --encrypt --output "${passphrase_file}" || return_code=1
    fi

    # This might not do anything useful, but it doesn't hurt either.
    unset passphrase
    unset confirmed_passphrase

    return $return_code
}

zogftw_geli_initialize() {
    local provider \
        get_passphrase_cmd geli_init_flags keyfile \
        metadata_backup_file passphrase_file pool_name

    provider="${1}"
    pool_name="${provider##*/}"
    keyfile="${ZOGFTW_GELI_KEYFILE_DIR}/${pool_name}.key"
    passphrase_file="${ZOGFTW_GELI_PASSPHRASE_DIR}/${pool_name}.gpg"
    metadata_backup_file="${ZOGFTW_GELI_METADATA_BACKUP_DIR}/${pool_name}.eli"

    # Start building the geli init flags in "${@}"
    if [ -n "${ZOGFTW_GELI_INIT_FLAGS}" ]; then
        set -- ${ZOGFTW_GELI_INIT_FLAGS}
    else
        # Add a placeholder to clear "${@}"
        set -- ""
    fi

    if [ -n "${ZOGFTW_GELI_METADATA_BACKUP_DIR}" ]; then
        if [ -f "${metadata_backup_file}" ]; then
            zogftw_wtf "zogftw_geli_initialize(): Metadata backup file:" \
                       "'${metadata_backup_file}' already exists"
            return 1;
        fi
        set -- "${@}" "-B" "${metadata_backup_file}"
    fi

    if [ -f "${keyfile}" ]; then
        zogftw_fyi "Using geli keyfile ${keyfile}"
        set -- "${@}" "-K" "${keyfile}"
    else
        zogftw_fyi "No geli keyfile found at ${keyfile}. Not using any."
    fi

    zogftw_prime_sudo || return 1

    if [ -f "${passphrase_file}" ]; then
        get_passphrase_cmd="zogftw_get_passphrase"
        zogftw_fyi "Getting the passphrase for ${provider} with: ${get_passphrase_cmd}"
        set -- "${@}" "-J" "-"
    else
        get_passphrase_cmd="false"
    fi

    if [ -z "${ZOGFTW_GELI_INIT_FLAGS}" ]; then
        # We added a placeholder, drop it again.
        shift
    fi

    zogftw_fyi "Initializing ${provider} with: ${*}"

    $get_passphrase_cmd "${passphrase_file}" | zogftw_sudo geli init "${@}" "${provider}"
}


zogftw_create() {
    local pool_name device_name \
        glabel_device geli_device geli_init_flags geli_passphrase_file

    pool_name="${1}"
    device_name="${2}"
    gpg_agent_started=0
    geli_passphrase_file="${ZOGFTW_GELI_PASSPHRASE_DIR}/${pool_name}.gpg"

    if [ -z "${pool_name}" ]; then
        zogftw_wtf "zogftw_create(): No pool name given"
        return 1;
    fi

    if [ -z "${device_name}" ]; then
        zogftw_wtf "zogftw_create(): No device name given"
        return 1;
    fi

    if [ ! -c "${device_name}" ]; then
        zogftw_wtf "zogftw_create(): No such device: ${device_name}"
        return 1
    fi

    zogftw_label_device "${pool_name}" "${device_name}" || return 1

    glabel_device="/dev/label/${pool_name}"

    if [ "${ZOGFTW_START_GPG_AGENT}" = 1 -a -z "${GPG_AGENT_INFO}" ]; then
        eval "$(gpg-agent --daemon)"
        if [ -n "${GPG_AGENT_INFO}" ]; then
            gpg_pid="${GPG_AGENT_INFO#*:}"
            gpg_pid="${gpg_pid%:*}"
            gpg_agent_started=1
        fi
    fi

    if [ ! -f "${geli_passphrase_file}" -a "${ZOGFTW_CREATE_ENCRYPTED_GELI_PASSPHRASE_FILE}" = 1 ]; then
        zogftw_create_encrypted_geli_passphrase_file "${geli_passphrase_file}" || return 1
    fi

    zogftw_geli_initialize "${glabel_device}" || return 1

    zogftw_geli_attach "${pool_name}" || return 1

    if [ "${gpg_agent_started}" = 1 ]; then
        kill "${gpg_pid}"
    fi

    geli_device="${glabel_device}.eli"

    if [ "${ZOGFTW_INITIALIZE_WITHOUT_DEVICE_ZEROING}" = 0 ]; then
        zogftw_clear_device "${geli_device}"
    fi

    zogftw_fyi "Creating ZFS pool ${pool_name}"
    zogftw_sudo zpool create $ZOGFTW_ZPOOL_CREATE_FLAGS "${pool_name}" "${geli_device}" || return 1

    zogftw_prepare_pool_for_receiving "${pool_name}" || return 1

    zogftw_export_external_zpool "${pool_name}" || return 1
}

###########
# Restore datasets on specified pool from external pools
zogftw_restore() {
    local destination_zpool \
          external_zpool external_zpools \
          backup_root backup_datasets incremental_mode

    destination_zpool="${1}"
    if [ -z "${destination_zpool}" ]; then
        zogftw_wtf "No destination zpool specified"
        return 1
    fi

    external_zpools="$(zogftw_get_exportable_zpools)"
    if [ -z "${external_zpools}" ]; then
        zogftw_wtf "No imported zpools managed by zogftw detected"
        return 1
    fi

    for external_zpool in $external_zpools;
    do
        backup_root="${external_zpool}/${ZOGFTW_DEST_POOL_PREFIX}/${destination_zpool}"
        backup_datasets="$(zogftw_get_datasets_recursively "${backup_root}")"
        for backup_dataset in ${backup_datasets}; do
            incremental_mode="-I"
            dest_dataset="${backup_dataset##${external_zpool}/${ZOGFTW_DEST_POOL_PREFIX}/}"
            if zogftw_dataset_has_snapshots "${backup_dataset}"; then
                zogftw_transfer_last_snapshot "${incremental_mode}" "${backup_dataset}" "${dest_dataset}" || return 1
            else
                zogftw_fyi "Skipping ${backup_dataset} due to lack of snapshots"
            fi
        done
    done
}

zogftw_get_datasets_recursively() {
    local dataset

    zfs list -r -H -o name -s name -t filesystem,volume "${dataset}"
}

###########

zogftw_usage_hook() {
    zogftw_show_hook_capabilities 'zogftw_usage_hook' \
        'script_name flag'
}

zogftw_usage() {
    local flag \
        script_name

    flag="${1}"

    script_name="$(basename "${0}")"
    cat<<EOF
$script_name (ZFS on geli for the win) 2025-02-23-48a7d58
usage: $script_name cmd command-to-execute
       $script_name config
       $script_name create zpool-name device-to-label
       $script_name export [-f] [zpool-to-export]
       $script_name help [-v]
       $script_name init
       $script_name import [-f] [-N] [zpool-to-import]
       $script_name snap[shot] [dataset-to-snapshot-specified-by-name-or-path]
       $script_name source
       $script_name sync [receiving-zpool]
       $script_name update-last-snapshot-properties zpool
       $script_name zcmd function-without-prefix-to-execute
       $script_name zpool subcommand [prefix_args] [postfix_args]
EOF
    zogftw_usage_hook

    if [ "${flag}" = "-v" ]; then
        echo Internal functions and their parameters:
        zogftw_get_function_prototypes
    fi
}

zogftw_get_config_variables() {
    echo \
         ZOGFTW_BACKUP_DATASET_COMPRESSION \
         ZOGFTW_CONFIG_FILE \
         ZOGFTW_CREATE_ENCRYPTED_GELI_PASSPHRASE_FILE \
         ZOGFTW_DEFAULT_SNAPSHOT_DATASETS \
         ZOGFTW_DEST_POOL_PREFIX \
         ZOGFTW_EXTERNAL_SRC_DATASETS \
         ZOGFTW_GELI_INIT_FLAGS \
         ZOGFTW_GELI_KEYFILE_DIR \
         ZOGFTW_GELI_METADATA_BACKUP_DIR \
         ZOGFTW_GELI_PASSPHRASE_DIR \
         ZOGFTW_GPG_DECRYPT_FLAGS \
         ZOGFTW_INITIALIZE_WITHOUT_DEVICE_ZEROING \
         ZOGFTW_LOG_SKIPPED_DATASETS \
         ZOGFTW_MAX_SPACE_CONSTRAINED_ZPOOL_SIZE \
         ZOGFTW_MBUFFER_FLAGS \
         ZOGFTW_OPTIONAL_SRC_DATASETS \
         ZOGFTW_REQUIRED_SRC_DATASETS \
         ZOGFTW_SHOW_AVAILABLE_HOOKS \
         ZOGFTW_SORTING_SNAPSHOTS_BY_NAME_KEEPS_CHRONOLOGICAL_ORDER \
         ZOGFTW_STORE_LAST_SNAPSHOT_AS_PROPERTY \
         ZOGFTW_SUDO_FLAGS \
         ZOGFTW_START_GPG_AGENT \
         ZOGFTW_USE_GPG_AGENT \
         ZOGFTW_ZFS_CREATE_FLAGS \
         ZOGFTW_ZFS_INCREMENTAL_RECEIVE_FLAGS \
         ZOGFTW_ZFS_NON_INCREMENTAL_RECEIVE_FLAGS \
         ZOGFTW_ZFS_SEND_FLAGS \
         ZOGFTW_ZPOOL_CREATE_FLAGS \
         # This line intentionally not left blank
}

zogftw_initialize_unset_optional_config_variables() {
    local \
        short_hostname

    short_hostname="$(hostname -s)"
    if [ -z "${short_hostname}" ]; then
        zogftw_wtf "zogftw_initialize_unset_optional_config_variables()" \
                   "Failed to get the hostname to set ZOGFTW_DEST_POOL_PREFIX"
        return 1
    fi

    # XXX: should leverage zogftw_get_config_variables()
    : "${ZOGFTW_BACKUP_DATASET_COMPRESSION=lz4}"
    : "${ZOGFTW_CREATE_ENCRYPTED_GELI_PASSPHRASE_FILE=0}"
    : "${ZOGFTW_DEFAULT_SNAPSHOT_DATASETS=}"
    : "${ZOGFTW_DEST_POOL_PREFIX=backup/${short_hostname}}"
    : "${ZOGFTW_GELI_INIT_FLAGS=}"
    : "${ZOGFTW_GELI_KEYFILE_DIR=${ZOGFTW_CONFIG_DIRECTORY}/geli/keyfiles}"
    : "${ZOGFTW_GELI_METADATA_BACKUP_DIR=${ZOGFTW_CONFIG_DIRECTORY}/geli/metadata-backups}"
    : "${ZOGFTW_GELI_PASSPHRASE_DIR=${ZOGFTW_CONFIG_DIRECTORY}/geli/passphrases}"
    : "${ZOGFTW_GPG_DECRYPT_FLAGS=--quiet}"
    : "${ZOGFTW_INITIALIZE_WITHOUT_DEVICE_ZEROING=0}"
    : "${ZOGFTW_LOG_SKIPPED_DATASETS=0}"
    : "${ZOGFTW_MAX_SPACE_CONSTRAINED_ZPOOL_SIZE=4000}"
    : "${ZOGFTW_MBUFFER_FLAGS=-m 100M}"
    : "${ZOGFTW_OPTIONAL_SRC_DATASETS=}"
    : "${ZOGFTW_REQUIRED_SRC_DATASETS=}"
    : "${ZOGFTW_SHOW_AVAILABLE_HOOKS=0}"
    : "${ZOGFTW_SORTING_SNAPSHOTS_BY_NAME_KEEPS_CHRONOLOGICAL_ORDER=0}"
    : "${ZOGFTW_STORE_LAST_SNAPSHOT_AS_PROPERTY=1}"
    : "${ZOGFTW_SUDO_FLAGS=-c -}"
    : "${ZOGFTW_START_GPG_AGENT=0}"
    : "${ZOGFTW_USE_GPG_AGENT=0}"
    : "${ZOGFTW_ZFS_CREATE_FLAGS=-p -u}"
    : "${ZOGFTW_ZFS_INCREMENTAL_RECEIVE_FLAGS=}"
    : "${ZOGFTW_ZFS_NON_INCREMENTAL_RECEIVE_FLAGS=}"
    : "${ZOGFTW_ZFS_SEND_FLAGS=}"
    : "${ZOGFTW_ZPOOL_CREATE_FLAGS=}"
}

zogftw_load_config_file() {
    local mode \
        config_file_failure

    mode="${1}"

    : "${ZOGFTW_CONFIG_FILE=${HOME}/.config/zogftw/zogftw.conf}"
    # This is used for defaults and optionally in the config file itself,
    # so it can't be set in zogftw_initialize_unset_optional_config_variables()
    ZOGFTW_CONFIG_DIRECTORY="${ZOGFTW_CONFIG_FILE%/*}"

    # Let's be optimistic ...
    config_file_failure=0

    if [ -n "${ZOGFTW_CONFIG_FILE}" ]; then
        if [ ! -f "${ZOGFTW_CONFIG_FILE}" ]; then
            # A missing ${ZOGFTW_CONFIG_FILE} is an error unless the user
            # is trying to create it or looking for help.
            if [ "${mode}" != "init" -a  "${mode}" != "help" ]; then
                zogftw_wtf "Config file '${ZOGFTW_CONFIG_FILE}' does not exist." \
                           "Use 'zogftw init' to create it."
                config_file_failure=1
            fi
        else
            . "${ZOGFTW_CONFIG_FILE}" || return 1
        fi
    fi

    zogftw_initialize_unset_optional_config_variables || return 1

    return $config_file_failure
}

zogftw_config() {
    local \
        config_variable
    for config_variable in $(zogftw_get_config_variables);
    do
        eval "echo ${config_variable}=\\'\$${config_variable}\\'"
    done
}

###########

# Execute whatever is passed as arguments.
#
# Could be reduced to "${@}", but then zogftw_list_prototypes()
# wouldn't be able to print a close-enough representation of
# the prototype.
zogftw_cmd() {
    local command_to_execute
    "${@}"
}

###########

zogftw_init() {
    local \
        directory

    for directory in "${ZOGFTW_CONFIG_DIRECTORY}" \
                     "${ZOGFTW_GELI_METADATA_BACKUP_DIR}" \
                     "${ZOGFTW_GELI_KEYFILE_DIR}" \
                     "${ZOGFTW_GELI_PASSPHRASE_DIR}";
    do
        if [ -n "${directory}" ]; then
            if [ -d "${directory}" ]; then
                zogftw_fyi "${directory} already exists"
            else
                zogftw_fyi "Creating ${directory}"
                mkdir -p "${directory}"
            fi
        fi
    done

    if [ -n "${ZOGFTW_CONFIG_FILE}" ]; then
        if [ -f "${ZOGFTW_CONFIG_FILE}" ]; then
            zogftw_fyi "${ZOGFTW_CONFIG_FILE} already exists"
        else
            zogftw_fyi "Creating ${ZOGFTW_CONFIG_FILE}"
            touch "${ZOGFTW_CONFIG_FILE}"
        fi
    fi

    zogftw_fyi "Please verify that the locations make sense and that" \
               "the permissions match your expectations."
}

###########

zogftw_update_last_snapshot_properties() {
    local zpool_to_update=$1 \
          dataset last_snapshot

    if [ -z "${zpool_to_update}" ]; then
        zogftw_wtf "No zpool to update specified"
        return 1
    fi

    required_parent_dataset="${zpool_to_update}/${ZOGFTW_DEST_POOL_PREFIX}"
    if ! zogftw_dataset_does_exist "${required_parent_dataset}"; then
        zogftw_wtf "'${required_parent_dataset}' doesn't exist. Nothing to update."
        return 1
    fi

    zogftw_fyi "Updating de.fabiankeil:zogftw:last_snapshot properties for ${zpool_to_update} ..."

    # Make sure we don't get the last snapshot from an existing
    # property which may be out of date. Split across two lines so
    # zogftw_get_function_prototypes doesn't pick the variable up.
    local \
        ZOGFTW_STORE_LAST_SNAPSHOT_AS_PROPERTY=0

    for dataset in $(zfs list -H -r -o name -t filesystem,volume ${required_parent_dataset}); do
        last_snapshot=$(zogftw_get_last_snapshot ${dataset})
        if [ -z "${last_snapshot}" ]; then
            zogftw_fyi "${dataset} contains no snapshots. Skipping it."
            continue
        fi
        zogftw_fyi "Setting last snapshot property value ${last_snapshot}"
        zogftw_sudo zfs set \
                    "de.fabiankeil:zogftw:last_snapshot=${last_snapshot}" \
                    "${dataset}"
    done
}

###########

zogftw_main_hook() {
    zogftw_show_hook_capabilities 'zogftw_main_hook' 'mode param_1 param_2'
}

zogftw_unregistered_mode_hook() {
    zogftw_show_hook_capabilities 'zogftw_unregistered_mode_hook' 'mode param_1 param_2'
}

zogftw_main() {
    local mode param_1 param_2 \
          ret

    mode="${1}"
    param_1="${2}"
    param_2="${3}"
    param_3="${4}"

    zogftw_load_config_file "${mode}" || return 1

    zogftw_main_hook

    case "${mode}" in
        create)
            zogftw_create "${param_1}" "${param_2}"
            ;;
        cmd)
            shift;
            zogftw_cmd "${@}"
            ;;
        config)
            zogftw_config
            ;;
        "export")
            zogftw_export "${param_1}" "${param_2}"
            ;;
        help)
            zogftw_usage "${param_1}"
            ;;
        init)
            zogftw_init
            ;;
        import)
            zogftw_import "${param_1}" "${param_2}" "${param_3}"
            ;;
        restore)
            zogftw_restore "${param_1}"
            ;;
        snap|snapshot)
            zogftw_snapshot "${param_1}"
            ;;
        source)
            # So all the cool kids can let their shell "source zogftw source".
            # A lot of of zogftw's functions are useful on their own.
            ;;
        sync)
            zogftw_sync "${param_1}"
            ;;
        update-last-snapshot-properties)
            zogftw_update_last_snapshot_properties "${param_1}"
            ;;
        zcmd)
            shift;
            zogftw_cmd "zogftw_""${@}"
            ;;
        zpool)
            shift;
            zogftw_foreach_zpool "${@}"
            ;;
        nop)
            # Does nothing either, can be set by zogftw_main_hook
            # to prevent the action requested by the user, or to
            # implement custom methods.
            ;;
        *)
            # Allow the hook to silence the error message
            zogftw_unregistered_mode_hook
            ret=$?
            if [ "${mode}" != "nop" ]; then
                zogftw_usage 1>&2
                echo "Invalid or missing parameters!" 1>&2
                return 1
            fi
            return $ret
            ;;
    esac
}

# XXX: Parts of the content, for example the SYNOPSIS,
#      should be generated instead of being hardcoded.
zogftw_get_asciidoc() {
    cat<<EOF
ZOGFTW(1)
===========
:doctype: manpage

NAME
----
zogftw - Creates redundant backups on encrypted external ZFS pools

SYNOPSIS
--------

    zogftw cmd command-to-execute
    zogftw config
    zogftw create zpool-name device-to-label
    zogftw export [-f] [zpool-to-export]
    zogftw help [-v]
    zogftw init
    zogftw import [-f] [-N] [zpool-to-import]
    zogftw snap[shot] [dataset-to-snapshot-specified-by-name-or-path]
    zogftw source
    zogftw sync [receiving-zpool]
    zogftw update-last-snapshot-properties zpool
    zogftw zcmd function-without-prefix-to-execute
    zogftw zpool subcommand [prefix_args] [postfix_args]

DESCRIPTION
-----------
*zogftw* makes managing backups on multiple external ZFS storage pools
on encrypted devices more convenient. Currently most of its subcommands
are only expected to work on FreeBSD and FreeBSD-based systems like
ElectroBSD.

*zogftw* is configured through variables that are provided through the
environment or the configuration file *ZOGFTW_CONFIG_FILE* that is
sourced as shell script.
They are uppercased and prefixed with *ZOGFTW_*.

*zogftw's* behaviour can be further customized by providing *hooks*,
which are shell functions that are called in certain events,
or by overwriting any other internal function providing the
behaviour expected by the caller isn't affected.

All of *zogftw's* internal functions are lowercased and prefixed
with *zogftw_*, hooks are additionally postfixed with *_hook*.

*zogftw* uses *sudo* for operations that are expected to require
root privileges. Running *zogftw* itself as root is neither necessary
nor recommended.

OPTIONS
-------

*zogftw cmd command-to-execute*

Execute the *command-to-execute* in the context of *zogftw*.
Useful to call *zogftw* functions that aren't reachable through
their own subcommands. Use *zogftw help -v* to see which functions
are available.

*zogftw config*

Show the configuration variables and their content. Useful to
confirm that the configuration file is interpreted as expected.

*zogftw create zpool-name device-to-label*

Create a new external ZFS pool that can be managed with *zogftw*.
The *device-to-label* is labeled *zpool-name* using *glabel*,
the labeled device is initialized with *geli* using the optional
*ZOGFTW_GELI_INIT_FLAGS*, afterwards it is attached and used as
vdev for the newly created ZFS pool *zpool-name*.

A *geli* passphrase and keyfile are read from the locations
documented in the *zogftw* *import* section. If the files
do not exist at the expected locations, no keyfile is used
and the passphrase has to be entered manually. If
*ZOGFTW_CREATE_ENCRYPTED_GELI_PASSPHRASE_FILE* is set to *1*
and the passphrase file doesn't exist yet, it will be created.

The ZFS pool is created with the *zpool* flags specified with
*ZOGFTW_ZPOOL_CREATE_FLAGS*.

The dataset *ZOGFTW_DEST_POOL_PREFIX* is created to mark the
ZFS pool as receiving target for *zogftw* *sync*, the dataset's
compression property is set to *ZOGFTW_BACKUP_DATASET_COMPRESSION*.

Finally the ZFS pool is exported and the geli provider
detached.

As the ZFS pool only has a single vdev, there is no redundancy
by default. Instead, the recommendation is to use multiple
independent devices with the same content on their own ZFS pools,
that can be stored in different locations and accessed independently.

*zogftw export [-f] [zpool-to-export]*

Export either *zpool-to-export* or all attached ZFS pools
that are stored on a encrypted vdev on a label with the
same name as the ZFS pool. If the *-f* flag is specified,
pools are exported forcefully.

*zogftw help [-v]*

Show the available subcommands. If the *-v* flag is specified,
the internal functions and their parameter names are listed
as well. They can be accessed through the subcommands *cmd*
or directly from the shell after sourcing *zogftw*.

*zogftw init*

Create the directories and configuration files expected by *zogftw*
as shown by *zogftw config* without second-guessing their locations
or the user's umask settings. Using this subcommand or creating the
files manually is optional, but a configuration file has to exist
unless *ZOGFTW_CONFIG_FILE* is set to an empty string through the
environment.

*zogftw import [-f] [-N] [zpool-to-import]*

Attach the label *zpool-to-import* with *geli* and import the
ZFS pool on it.
If no *zpool-to-import* is specified, all labeled devices are
attached and imported.
If the *-f* flag is specified, pools a forceful import
is attempted. This is required to import pools that were
previously imported by another system.
If the *-N* flag is specified, pools are imported
without mounting any file systems.

If *ZOGFTW_GELI_PASSPHRASE_DIR*/*zpool-to-import*.gpg exists,
it is decrypted using *gpg* and the content used as *geli*
passphrase. If *gpg-agent* is being used, this allows to attach
multiple devices without having to enter the passphrase manually
for each device.

If *ZOGFTW_GELI_KEYFILE_DIR*/*zpool-to-import*.key exists,
it is used a *geli* keyfile.

*zogftw nop*

This is an alias for *zogftw source*. Can be used when modifying
the subcommand through a hook that implements a custom subcommand
to prevent a syntax warning.

*zogftw snap[shot] [dataset-to-snapshot-specified-by-name-or-path]*

Create a ZFS snapshot on the dataset specified by name or path.
A timestamp is used as snapshot name to make sure that ordering
the snapshots by name reflects the chronological order as well.

If no dataset is specified, a ZFS snapshot is created on all
datasets specified by *ZOGFTW_DEFAULT_SNAPSHOT_DATASETS*.

*zogftw source*

Exit after reading the configuration file without executing one of
the other subcommands.

Useful to source *zogftw* in the current shell to directly access
its functions later on without having to prefix them with
*zogftw cmd* and rereading the configuration file each time.

Note that not all shells support passing parameters to sourced
scripts, though.

*zogftw sync [receiving-zpool]*

Send the last snapshots on the configured datasets to the
*receiving-zpool*. If no ZFS pool is specified, send to all
attached ZFS pools with a layout as created by *zogftw* *create*.
*ZOGFTW_ZFS_SEND_FLAGS* can be used to enable *zfs send* flags like
*-L* or *-e*. The appropriate incremental flags are set automatically
and should not be included in the *ZOGFTW_ZFS_SEND_FLAGS*.

*mbuffer* is used with the flags specified with *ZOGFTW_MBUFFER_FLAGS*
in the pipe between *zfs send* and *zfs receive*.

Snapshots are received below *receiving-zpool*/*ZOGFTW_DEST_POOL_PREFIX*.

Snapshots on datasets specified with *ZOGFTW_REQUIRED_SRC_DATASETS*
will be sent to the receiving ZFS pool unless they already exist
there or the property *de.fabiankeil:zogftw:new_datasets* is set
on *receiving-zpool*/*ZOGFTW_DEST_POOL_PREFIX* and does not contain
the value *yes*.

If the dataset doesn't already exist it is created, otherwise
an incremental snapshot is sent. Intermediary snapshots are sent as
well, provided the receiving ZFS pool is bigger than
*ZOGFTW_MAX_SPACE_CONSTRAINED_ZPOOL_SIZE*. This logic can be
overridden by setting the *de.fabiankeil:zogftw:incremental-mode*
property on the receiving dataset to *-i* or *-I*.

Incremental snapshots are received using the zfs flags specified
with *ZOGFTW_ZFS_INCREMENTAL_RECEIVE_FLAGS*, in case of non-incremental
snapshots the zfs flags specified with
*ZOGFTW_ZFS_NON_INCREMENTAL_RECEIVE_FLAGS* are used instead.
If the system supports it (FreeBSD r289362 or later and pool feature
*extensible_dataset* enabled), adding the *-s* flag is highly recommended
as it allows *zogftw* to resume failed transfers the next time
the *sync* command is being used.

Datasets specified with *ZOGFTW_OPTIONAL_SRC_DATASETS* are
treated like required datasets, except that they are only sent
if the dataset itself already exists on the receiving ZFS pool
and contains at least one snapshot.

Datasets specified with *ZOGFTW_EXTERNAL_SRC_DATASETS*
are treated like optional source datasets but do not have to be
available on the source at the time the synchronization occurs.

To figure out which snapshots are missing, *zogftw* by default
lets *zfs* list them sorted in chronological order. If it is known
that sorting them by name keeps them in chronological order,
*ZOGFTW_SORTING_SNAPSHOTS_BY_NAME_KEEPS_CHRONOLOGICAL_ORDER* can
be set to 1 to significantly speed up the listing on recent
FreeBSD-based systems.

In case of synchronization errors, *zogftw sync* exits right away
(without syncing other pools) to increase the chances that the
user notices the problem.

*zogftw update-last-snapshot-properties zpool*

Update the *de.fabiankeil:zogftw:last_snapshot* properties on
the given pool. Useful if the zpool has been created before
the property was introduced or has been updated with
*ZOGFTW_STORE_LAST_SNAPSHOT_AS_PROPERTY* being set to *0*.
Running *zogftw sync* automatically creates or updates the
property but only does it for datasets that receive updates.

*zogftw zcmd function-without-prefix-to-execute*

Works like *zogftw cmd*, but the command is prefixed with *zogftw_*
so the prefix doesn't need to be typed manually.

*zogftw zpool subcommand [prefix_args] [postfix_args]*

For each imported ZFS pool that is managed by zogftw, execute *zpool*
with the specified *subcommand* using the optional arguments before
and after the pool name.

EXIT STATUS
-----------
Everything but *0* is an error, *0* usually means success.

BUGS
----
The documentation is poorly formatted and incomplete. The OPTIONS
section should be called SUBCOMMANDS. The system used to build the
documentation doesn't support this without fiddling, though,
and as the documentation system will hopefully be replaced in the near
future, said fiddling currently doesn't seem like time well spent.

ZFS permissions aren't consistently checked when figuring out
whether or not a command requires *sudo*.

*zogftw create* currently does not put an unencrypted ElectroBSD
system on the beginning of the disk that plays a selection of
*Die Ärzte - 5 6 7 8 Bullenstaat* in a loop when booted, to entertain
whoever gets hold of it by tricking a judge into signing a search
warrant.

SEE ALSO
--------
*gpg2(1)*, *gpg-agent(1)*, *zfs(8)*, *zpool(8)*

AUTHOR
------
Fabian Keil <fk@fabiankeil>

RESOURCES
---------
<https://www.fabiankeil.de/gehacktes/zogftw/>

EOF
}

zogftw_main "${@}"
