#!/bin/ksh
# Network-based backup script
# Author: Perette Barella
# Copyright 2003 - 2018 Devious Fish.  All rights reserved.
VERSION='$Id: netbackup 114 2025-12-17 18:36:40Z perette $'

arg0=$(basename "$0")
USAGE='Abcunv'
[[ $(getopts '[-][12:abc]' flag --abc; print -- 0$flag) == "012" ]] &&
	NAMEOPTS="-a $arg0" &&
	USAGE=$'
[-1?'$VERSION$']
[+NAME?netbackup - back up user account to remote server]
[+DESCRIPTION?\b\f?\f\b backs up a user account to a remote server
using \brsync\b(1).  Built-in and user-supplied rules are applied to
exclude unnecessary files, keeping backups to a reasonable size and
minimizing bandwidth.]
[+?Two configuration files are supplied.  The \ahost configuration\a is
read first, usually containing a \adestination\a for the backups.  The
destination server is then checked for a \asite configuration\a.  If found, it is retrieved, cached, and read.]
[A:archive?Invoke \bnetarchive\b(1) to make a timestamped snapshot if
the backup is successful.]
[b:backup-host?Specify the destination.]:[host]
[c:configure?Specify a configuration file instead ~/.netbackuprc.  Use "-" to specify stdin.]:[config-file]
[H:hostname?Overrides the hostname, which sets the destination directory.]:[hostname]
[u:unconfigured?Suppress built-in configuration.]
[v:verbose?Verbose output.]
[n:dry-run?Perform dry run, but do not actually do a backup.]
[+CONFIGURATION FILE?The configuration files are text files.  \b#\b marks a line as a comment; blank lines are ignored.  Other lines have a keyword and value, whitespace separated.  Keywords are:]
{
  [+destination?Specify a remote destination.  This is a \vname@host\v format as used by \bssh\b(1).]
  [+local_destination?Specify a local hostname.  \b\f?\f\b checks if the host is available; if it is, this hostname is used instead of \adestination\a, \bremote_excludes\b are ignored, and compression over \brsync\b is disabled.]
  [+exclude?Exclude files matching a pattern.  See \brsync\b(1) for pattern format.]
  [+remote_exclude?Exclude files \aonly\a when using a remote destination.  Use this with \blocal_destination\b to backup large, non-critical files only on a LAN and not over the Internet, for example.  \aNote that when\a \bremote_excludes\b \aapply, file deletions are not synchronized during backups.\a]
  [+archive?Enables archiving, unless the parameter is \vno\v or \voff\v.]
  [+recycle?Used with archiving, specifies a count of extra snapshots retained for reuse.]
  [+verbose?Enables verbose output, unless the parameter is \vno\v or \voff\v.]
}
[+EXIT STATUS?0 on success, non-0 on error.]
[+FILES?The following files are used:]
{
  [+~/.netbackuprc?Host configuration file.]
  [+~/.netbackuprc-site?Site configuration file.]
}
[+SEE ALSO?\bnetarchive\b(1), \brsync\b(1), \bssh\b(1)]



[-author?Perette Barella <perette@deviousfish.com>]
'


function load_configuration {
	typeset name="${1:-unknown}"
	integer line=0 status=0
	while read command parameter
	do
		let line++
		[ "$command" = "" ] && continue
		if [ "$parameter" = "" ]
		then
			print -- "$name:$line: $command missing parameter." 1>&2
			status=1
			continue
		fi
		case "$command" in
		    '#'*)
			continue
			;;
		    exclude)
			EXCLUDES="${EXCLUDES} --exclude=\"$parameter\""
			;;
		    remote_exclude)
			REMOTE_EXCLUDES="${REMOTE_EXCLUDES} --exclude=\"$parameter\""
			;;
		    hostname)
			HOST="$parameter"
			;;
		    destination)
			TARGET="$parameter"
			;;
		    local_destination)
			if ssh -n "$parameter" true >/dev/null
			then
				REMOTE=true
				TARGET="$parameter"
				$VERBOSE && print -- "$TARGET: Found on local network, using that."
				COMPRESS=FALSE
			else
				$VERBOSE && print -- "$parameter: Not on local network, using $TARGET."
			fi
			;;
		    archive)
			ARCHIVE=true
			[[ $parameter == off || $parameter == no ]] &&
				ARCHIVE=false
			;;
		    verbose)
			VERBOSE=true
			[[ $parameter == off || $parameter == no ]] &&
				VERBOSE=false
			;;
		    recycle)
			integer recycle
			if let recycle="$parameter" >/dev/null 2>&1
			then
				let RECYCLE=recycle
			else
				status=$?
				print "$name:$line: recycle requires a numeric parameter." 1>&2
			fi
			;;
		    *)
			print -- "$name:$line: Command $command unknown." 1>&2
			status=1
			;;
		esac
	done
	return $status
}



user="$(id -n -u)" || exit 1
HOST="$(hostname | cut -d. -f1)"
status=0

cd ~/.. || exit 1

EXCLUDES=""
REMOTE_EXCLUDES=""
TARGET=
VERBOSE=false
REMOTE=true
COMPRESS="-z"
DRYRUN=false
ARCHIVE=false
BUILTIN_CONF=true
DEFAULT_CONF=true
RECYCLE=0

while getopts $NAMEOPTS "$USAGE" option
do
	case "$option" in
	    A)
		ARCHIVE=true
		;;
	    b)
		TARGET="$OPTARG"
		;;
	    c)
		DEFAULT_CONF=false
		if [[ $OPTARG == "-" ]]
		then
			load_configuration "stdin"
		else
			load_configuration < "$OPTARG" || exit $?
		fi
		;;
	    H)
		HOST="$OPTARG"
		;;
	    u)
		BUILTIN_CONF=false
		;;
	    v)
		VERBOSE=true
		;;
	    n)
		DRYRUN=true
		VERBOSE=true
		;;
	esac
done

shift $((OPTIND - 1))
if (( $# > 0))
then
	OPTIND=0
	getopts $NAMEOPTS "$USAGE" option --short
	exit 1
fi

if $BUILTIN_CONF
then
	load_configuration "built-in configuration" <<- EOF
		# Don't back up thumb drives or other temporarily mounted filesystems
		exclude .gvfs
		exclude Desktop/Dropbox

		# Omit cache files
		exclude .cache
		exclude Library/Arq/Cache.noindex/*
		exclude Library/Caches/*
		exclude Pictures/*.photoslibrary/iPod Photo Cache/*
		exclude .nv/GLCache/*
		exclude .config/sublime-text-3/Cache/*
		exclude **/cache*
		exclude **/Cache*

		# Omit files in the trash
		exclude .local/share/Trash/*
		exclude .Trash/*

		# Omit backups
		exclude Library/Application Support/MobileSync/Backup/*
		exclude Pictures/*.photoslibrary/Backup/*
		exclude .config/sublime-text-3/Backup/*
		exclude *.old
		exclude *.bak

		# Omit programs installed under WINE
		exclude .wine/drive_c/Program Files (x86)

		# Omit various heavy, transient or unimportant application data
		exclude **/*.swp
		exclude Library/Developer/*
		exclude Library/Application Support/Unison/*
		exclude Library/Application Support/SyncServices/*
		exclude Library/Saved Application State/*
		exclude Library/PubSub/*
		exclude .pingus/demos
		exclude .mozilla/firefox/**/datareporting/*

		# Omit media files that can be restored from original discs
		exclude Music/iTunes/*
		exclude Music/Media/*

		# Omit software update bundles
		exclude Library/iTunes/i* Updates/*

		# Omit files related to security
		exclude .subversion/*
		exclude .ssh/*

		# Omit files the user has put in an excluded-from-backup area
		exclude Nobackup/*

		# Cloud documents are already backed up in a repository
		exclude Documents/Cloud

		# Omit object files and target files
		exclude Sites/**/*.html
		exclude **/*.o
		exclude **/*.a
		exclude **/stddefs.m4
	EOF
fi

# Load local configuration
if $DEFAULT_CONF && [[ -f ~/.netbackuprc ]]
then
	load_configuration "~/.netbackuprc" < ~/.netbackuprc || exit $?
fi

if [ "$TARGET" = "" ]
then
	print "$arg0: destination not set.  Add 'destination' entry to .netbackuprc"
	exit 1
fi

# Retrieve a remote configuration, if it exists, then import it.
rsync $TARGET:.netbackuprc-site $user/.netbackuprc-site >/dev/null 2>&1
if [[ -f ~/.netbackuprc-site ]]
then
	load_configuration "~/.netbackuprc-site" < ~/.netbackuprc-site || exit $?
fi



# Do the work
dest="$TARGET:Backups/$HOST"
verbose=

$VERBOSE && verbose="--itemize-changes"
$DRYRUN && verbose="-n --itemize-changes"



delete_excluded=
[[ $REMOTE == false || $REMOTE_EXCLUDES == "" ]] &&
	delete_excluded="--delete-excluded"
$REMOTE || REMOTE_EXCLUDES=""

# IN CASE OF MODIFICATION
# Note: Don't use the --delete-excluded option when targeting home
# directory of the remote server, however desirable,
# because then it deletes .ssh on target which destroys the autologin
# capability which prevents further backups.

# The iTunes "keep files organized" option (on advanced settings
# preferences page) causes iTunes to rename directories if the
# band names of multiple albums aren't capitalized the same.
# You listen to one song by "Lords of Acid",
# another by "Lords Of Acid" and rsync decides to delete all the
# files and send them again.
# Either turn the option off in iTunes, or don't back up
# music files when running remotely (remote_exclude).

command="rsync -a -H -x -E --fuzzy --delete --delete-after --force $verbose $COMPRESS $delete_excluded $EXCLUDES $REMOTE_EXCLUDES $user/ $dest"
$VERBOSE && print "$arg0: Executing $command" 1>&2
eval "$command" || exit $?

if $ARCHIVE
then
	if whence -q netarchive
	then
		$VERBOSE && verbose="-v"
		$DRYRUN && verbose="-n -v -p"
		command="netarchive $verbose -r $RECYCLE \"$TARGET\""
		$VERBOSE && print "$arg0: Executing $command" 1>&2
		eval "exec $command"
		exit $?
	fi
	print "$arg0: Please install netarchive to perform archiving." 1>&2
	exit 1
fi

exit 0

