#!/bin/bash

# Simple Snapshot Command Wrapper
# Copyright (c) 2014-2025 John Goerzen
#   This program 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.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

set -e
ZFSCMD=/sbin/zfs
ZFS=runzfs
EXCLUDEPROP="org.complete.simplesnap:exclude"
BOOKMARKPROP="org.complete.simplesnap:createbookmarks"

logit () {
   logger -p info -t "`basename $0`[$$]" "$1"
}

exiterror () {
   logit "$1"
   exit 10
}

runzfs () {
  logit "Running: $ZFSCMD $*"
  if $ZFSCMD "$@"; then
      logit "zfs exited successfully."
      return 0
  else
      RETVAL="$?"
      logit "zfs exited with error: $RETVAL"
      return $RETVAL
  fi
}

if [ "$(uname)" = "Darwin" ] ; then
   PATH=$PATH:/usr/local/bin
   ZFSCMD="zfs"
fi

# POSIX compatible program validation
command -v gdate > /dev/null && DATE="gdate" || DATE="date"
command -v gsed  > /dev/null && SED="gsed"   || SED="sed"
command -v ggrep > /dev/null && GREP="ggrep" || GREP="grep"
command -v ghead > /dev/null && HEAD="ghead" || HEAD="head"
command -v pv    > /dev/null && PV="pv"      || PV=""

# template - $1
# dataset - $2
listsnaps () {
   $ZFS list -t snapshot -r -d 1 -H -o name "$2" | $GREP "@$1" || true
}

# template - $1
# dataset - $2
listbookmarks () {
   $ZFS list -t bookmark -r -d 1 -H -o name "$2" | $GREP "#$1" || true
}

PATTERN="^[a-zA-Z0-9_/. -]\+\$"
if [ "$1" = "reinvoked" ]; then
  shift
  logit "Reinvoked with parameters \"$*\""
  shift # Drop the call to simplesnapwrap

  # Validate again.  Just to be sure.
  echo ".$*" | $GREP -q "${PATTERN}" || exiterror "Invalid characters found on re-validate; pattern is ${PATTERN}"
  echo ".$*" | $GREP -vq '\.\.' || exiterror "Found .. in input; aborting."

  # We don't want any parameters to contain a space or leading dash.
  for ARG; do
    echo ",${ARG}" | $GREP -vq '^,-' || exiterror "Found leading '-' in parameter \"${ARG}\"; aborting."
    echo ",${ARG}" | $GREP -vq ' ' || exiterror "Found space in parameters; aborting."
  done
  MODE="$1"
  shift || exiterror "Missing mode."
  case "$MODE" in
    "listfs")
       logit "Listing ZFS datasets without ${EXCLUDEPROP}=on"
       $ZFS list -t filesystem,volume -o "name,${EXCLUDEPROP}" -H | \
          $GREP -v $'\ton$' | $SED $'s/\t.*$//'
       ;;
    "sendback")
       # Next lines shared with reap
       TEMPLATE="__simplesnap_$1_"
       TEMPLATEPATTERN="^[a-zA-Z0-9]\+\$"
       echo "a$1" | $GREP -q "${TEMPLATEPATTERN}" || exiterror "Invalid characters in sendback template \"$1\"; pattern is ${TEMPLATEPATTERN}"
       DATASET="$2"
       echo "_${DATASET}" | $GREP -vq " " || exiterror "Space found in dataset name"
       [ -z "${DATASET}" ] && exiterror "No dataset given."
       [ -z "$1" ] && exiterror "No template given."

       logit "Listing snapshots on \"${DATASET}\" with template \"${TEMPLATE}\""

       USINGBOOKMARK=0
       OLDESTSNAP="`listsnaps \"${TEMPLATE}\" \"${DATASET}\" | ${HEAD} -n 1`"
       if [ -z "${OLDESTSNAP}" ]; then
         logit "Found no existing snapshot, looking for a bookmark."
         OLDESTSNAP="`listbookmarks \"${TEMPLATE}\" \"${DATASET}\" | ${HEAD} -n 1`"
         if [ -z "${OLDESTSNAP}" ]; then
           logit "Found no existing bookmark either; will send full stream."
         else
           USINGBOOKMARK=1
           logit "Will use bookmark ${OLDESTSNAP} as basis."
         fi
       else
         logit "Will use ${OLDESTSNAP} as basis."
       fi

       # Make a new snapshot.
       NEWSNAP="${TEMPLATE}`$DATE +%FT%T`__"
       NEWFULLSNAP="${DATASET}@${NEWSNAP}"
       logit "Making snapshot ${NEWFULLSNAP}"
       $ZFS snapshot "${NEWFULLSNAP}"

       # simplesnap requested tranmission bandwidth
       LIMIT="$3"
       # check if pv is installed, if not discard limit to keep things working
       [ -n "${PV}" ] || LIMIT=""
       if [ -z "${OLDESTSNAP}" ]; then
         # No existing snapshot.  Send the whole thing.
         logit "Sending non-incremental stream."
         if [ -z "${LIMIT}" ]; then
           $ZFS send "${NEWFULLSNAP}"
         else
           $ZFS send "${NEWFULLSNAP}" | ${PV} -L "${LIMIT}"
         fi
       else
         # Use the oldest existing snapshot.
         logit "Sending incremental stream back to ${OLDESTSNAP}"
         if [ "${USINGBOOKMARK}" = "1" ]; then
             # bookmarks don't support -I.
             # see https://github.com/zfsonlinux/zfs/issues/7263
           if [ -z "$LIMIT" ]; then
             $ZFS send -i "${OLDESTSNAP}" "${NEWFULLSNAP}"
           else
             $ZFS send -i "${OLDESTSNAP}" "${NEWFULLSNAP}" | ${PV} -L "${LIMIT}"
           fi
         else
           if [ -z "${LIMIT}" ]; then
             $ZFS send -I "${OLDESTSNAP}" "${NEWFULLSNAP}"
           else
             $ZFS send -I "${OLDESTSNAP}" "${NEWFULLSNAP}" | ${PV} -L "${LIMIT}"
           fi
         fi
       fi

       if $ZFS get -o value -H "${BOOKMARKPROP}" "${DATASET}" | grep -q "on"; then
           NEWBM="`echo "${NEWFULLSNAP}" | sed 's/@/#/'`"
           logit "${BOOKMARKPROP}=on; converting snapshot ${NEWFULLSNAP} to bookmark ${NEWBM}"
           $ZFS bookmark "${NEWFULLSNAP}" "${NEWBM}"
           $ZFS destroy "${NEWFULLSNAP}"
       fi
       ;;
    "reap")
       # Next lines shared with sendback
       TEMPLATE="__simplesnap_$1_"
       TEMPLATEPATTERN="^[a-zA-Z0-9_]\+\$"
       echo "_$TEMPLATE" | $GREP -q "${TEMPLATEPATTERN}" || exiterror "Invalid characters in reap template; pattern is ${TEMPLATEPATTERN}"
       DATASET="$2"
       echo "_${DATASET}" | $GREP -vq " " || exiterror "Space found in dataset name"
       [ -z "${DATASET}" ] && exiterror "No dataset given."
       [ -z "$1" ] && exiterror "No template given."

       # We always save the most recent.
       SNAPSTOREMOVE="`listsnaps \"${TEMPLATE}\" \"${DATASET}\" | ${HEAD} -n -1`"
       if [ -z "${SNAPSTOREMOVE}" ]; then
         logit "No snapshots to remove."
       else
         for REMOVAL in ${SNAPSTOREMOVE}; do
            logit "Destroying snapshot ${REMOVAL}"
            echo "_${REMOVAL}" | $GREP -q '@' || exiterror "PANIC: snapshot name doesn't contain '@'"
            $ZFS destroy "${REMOVAL}"
         done
       fi

       # We always save the most recent.
       BOOKSTOREMOVE="`listbookmarks \"${TEMPLATE}\" \"${DATASET}\" | ${HEAD} -n -1`"
       if [ -z "${BOOKSTOREMOVE}" ]; then
         logit "No bookmarks to remove."
       else
         for REMOVAL in ${BOOKSTOREMOVE}; do
            logit "Destroying bookmark ${REMOVAL}"
            echo "_${REMOVAL}" | $GREP -q '#' || exiterror "PANIC: bookmark name doesn't contain '#'"
            $ZFS destroy "${REMOVAL}"
         done
       fi
       ;;
    *)
       exiterror "Invalid mode \"${MODE}\" specified."
       ;;
    esac
  logit "Exiting successfully."
  exit 0
fi

CMD="${SSH_ORIGINAL_COMMAND}"

logit "Invoked with parameters \"$*\" and SSH_ORIGINAL_COMMAND: \"${CMD}\""

if [ -z "${SSH_ORIGINAL_COMMAND}" ]; then
  echo "simplesnapwrap: This program is to be run from ssh."
  exiterror "Not run from ssh."
fi

# Sanitize input.  We don't want special characters here.

echo ".${CMD}" | $GREP -q "${PATTERN}" || exiterror "Invalid characters in input; pattern is: ${PATTERN}"
echo ".${CMD}" | $GREP -vq '\.\.' || exiterror "Found .. in input; aborting."

logit "Reinvoking to parse parameters"
exec "$0" reinvoked ${CMD}

