#!/bin/ksh
#
# $Revision: 1.9 $ $Date: 2024-05-16 00:57:08-04 $
# $Source: /home/vogelke/projects/zfs/snapshot-inc/RCS/100.zsnap-inc,v $
# $Host: furbag.my.domain $
# $UUID: 08e1445e-c448-4a76-b380-10bb9da783c7 $
#
#<zinc: handle incremental backups using ZFS snapshot.
# Walking a spinning-rust filetree for recently-modified files takes too long.

export PATH=/usr/local/bin:/bin:/sbin:/usr/bin
set -o nounset
tag=${0##*/}
umask 022
PS4='${tag}-${LINENO}: '

# Interactive logging goes to the terminal, the rest to LOCAL6 syslog.
#
# If interactive, use "kill $$" to kill the script with signal 15 even
# if we're in a function, and use the trap to avoid the "terminated"
# message you normally get by using "kill".

interactive () {
    test -t 0 || test -t 1 || test -t 2
}

if interactive; then
    trap 'exit 1' 15
    logmsg () { echo "$(date '+%F %T.%3N%:::z') $tag: $@" >&2 ; }
    die ()    { logmsg "FATAL: $@"; kill $$ ; }
else
    logmsg () { logger -t "$tag" -p local6.info "$@" ; }
    die ()    { logmsg "FATAL: $@"; }
fi

# Must be run by root; $EUID test doesn't work in dash,
# a common /bin/sh implementation.
case "$(id -u)" in
    0) ;;
    *) die "must be run by root" ;;
esac

# Variables.
snapname='inc30'
ds=''           # dataset
mp=''           # mountpoint

# Human-readable list of files to ignore.
ignore='/backup/etc/conf-exclude'

# Temporary list of files to copy, and exclusion list.
logmsg start

flist=$(mktemp -q /tmp/$tag.list.XXXXXX)
case "$?" in
    0)  test -f "$flist" || die "$flist: file not found" ;;
    *)  die "can't create flist" ;;
esac

# Temporary exclusion list.
exc=$(mktemp -q /tmp/$tag.exc.XXXXXX)
case "$?" in
    0)  test -f "$exc" || die "$exc: file not found" ;;
    *)  die "can't create exclude list" ;;
esac

awk '{if (length($0) > 0) print; else exit}' $ignore > $exc

# I don't know why files with numbers in parens show up in the diff list.
# Just trim the numbers for now.

junk='
  s/\t(-1)$//
  s/\t(+1)$//
  s/(on_delete_queue)$//
'

# We only want mount-points in use; go down at most 3 levels
# in the directory tree.
skip=$(zpool list -H -oname)

zfs list -d 3 -Ho name,mountpoint -t filesystem | 
    while read ds mp ; do
        # Skip unmounted datasets.
        test "$mp" = "none" &&
            logmsg "SKIPPING $ds, not mounted" &&
            continue

        # Skip redundant datasets.
        case "$ds" in
            /ROOT/*RELEASE) logmsg "SKIPPING $ds, redundant" && continue ;;
            *) ;;
        esac

        # Skip the top-level pools like "tank".
        for p in $skip; do
            test "$ds" = "$p" &&
                logmsg "SKIPPING $ds, top-level pool" &&
                continue 2
        done

        logmsg "$ds on $mp"
        snapshot="$ds@$snapname"    # not an error if missing.

        # Find added or changed files in each snapshot.
        if zfs list -t snapshot -H "$snapshot" > /dev/null 2>&1
        then
            logmsg diff "$snapshot"

            zfs diff -H "$snapshot" 2> /dev/null |
                grep '^[+M]'         |
                cut -c3-             |
                sed -e 's/^/./'      |
                grep -v -f "$exc"    |
                sed -e "$junk"       |  # fix weird diff output
                sort -u >> $flist

            logmsg destroy "$snapshot"
            zfs destroy "$snapshot"
        fi

        # At this point, either an incremental snapshot never existed or
        # we used it and removed it.  Make a new one.
        logmsg create "$snapshot"
        zfs snapshot "$snapshot"
    done

# If list of new files is not empty, copy it to a backup directory.
cd /
dest='/backup/inc'

if test -f "$flist"; then
    bdir=$(date "+${dest}/%Y/%m%d/%H%M")
    mkdir -p "$bdir" || die "$bdir: cannot create"
    logmsg "bdir: $bdir"

    k=$(wc -l < "$flist")
    cpio -pdum --quiet $bdir < $flist

    # Remove empty directories in the backup tree.
    logmsg 'removing empty directories'

    ( cd $bdir && find . -mindepth 1 -depth -type d -print0 |
                  xargs -0 rmdir 2> /dev/null )

    # Need to sleep at least 5 seconds for ZFS transactions to finish.
    sleep 5
    kb=$(du -sk "$bdir" | awk '{print $1}')
    logmsg "copied $k files $kb Kb"
else
    logmsg 'nothing to back up'
fi

# Cleanup.
rm $flist $exc
logmsg done
exit 0
