#!/bin/bash - # File: tar-home.sh # Copyright (c) 2018-2019 Justin Hanekom # Licensed under the MIT License # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Setup a safe Bash scripting environment set -o errexit # Exit immediately if an error occurs set -o noclobber # Do not allow files to be overwritten via redirect set -o nounset # Do not allow unset variables # Set the exit code of a pipeline to the rightmost non-zero on error set -o pipefail # Set the internal field separator to newline or tab, but not space IFS=$'\n\t' # Setup a secure Bash scripting environment by: setting a secure path; # clearing all aliases; clearing the command path hash; setting the hard limit # to 0 to turn off core dumps; and setting a secure umask PATH=$(PATH='/bin:/usr/bin' getconf PATH); export PATH builtin unalias -a hash -r ulimit -H -c 0 -- UMASK=002 umask ${UMASK} # Global constant definitions readonly OPTIONS='u:s:d:p:x:k:rvh' readonly LONGOPTS='user:,srcdir:,destdir:,prefix:,suffix:,keep:,remove,verbose,help' readonly TIMESTAMP=$(date '+%Y%m%d%H%M%S') readonly STARTED_AT=$(date '+%s') # Global variable declarations USER_NAME='' SRC_DIR='' DEST_DIR='' PREFIX="home_" SUFFIX='.tar.gz' KEEP=5 IS_REMOVE='' IS_VERBOSE='' ################################################################################ # Function: chomp_slash # Description: Removes any trailing slash ("/") from a string # Arguments: $1 :- String from which to remove any trailing slashes # Ouputs: Prints string with trailing slashes removed function chomp_slash { local dir="$1" while [ "${dir:(-1)}" = '/' ]; do dir=${dir::-1} done echo "${dir}" } ################################################################################ # Function: echo_if_verbose # Description: Prints $@ if IS_VERBOSE is not empty # Arguments: $@ :- Content to echo if ${IS_VERBOSE} is not empty # Requires: ${IS_VERBOSE} set to a non-empty string if verbose is on # Outputs: Prints $@ if ${IS_VERBOSE} is not empty function echo_if_verbose { if [ -n "${IS_VERBOSE}" ]; then echo "$@" fi } ################################################################################ # Function: trim # Description: Trims any/all whitespace from the beginning and end of a string # Arguments: $1 :- The string from which to trim whitespace # Outputs: Prints string with any leading/trailing whitespace removed function trim { local str="$1" str="${str#"${str%%[![:space:]]*}"}" str="${str%"${str##*[![:space:]]}"}" echo "$1" } ################################################################################ # Function: usage_exit # Description: Prints the usage message for this script and then exits # Arguments: $1 :- Exit code; defaults to 1 # Outputs: Prints description of how to call this script function usage_exit { local exit_code=1 if [[ ${1-} =~ ^[0-9]+$ ]]; then exit_code="$1" fi cat << EOT Usage: $(basename "$0") [options]... -u|--user (Required) The owner of the generated archive file -s|--srcdir (Required) The owners home directory -d|--destdir (Required) The dir into which to save the new file -p|--prefix The prefix of the generated archive file ('home_') -x|--suffix The suffix of the generated archive file ('.tar.gz') -k|--keep How many archives to keep (5) -r|--remove Causes previous archive files to be removed -v|--verbose Displays verbose output -h|--help Displays this message and aborts the script NOTE: The , , and arguments are mandatory and *must* be supplied EOT exit "${exit_code}" } ################################################################################ # Function: parse_cmdline # Description: Parses the command-line using the enhanced version of getopt # Arguments: $@ :- Command-line arguments (required) # Requires: Global variables $USER_NAME, $SRC_DIR, $DEST_DIR, $PREFIX, # $SUFFIX, $KEEP, $IS_REMOVE, and $IS_VERBOSE # Outputs: Sets above-mentioned global variables based on given # command-line arguments function parse_cmd_line { # Ensure that the enhanced version of getopt is available ! getopt --test > /dev/null if (( "${PIPESTATUS[0]}" != 4 )); then echo "I'm sorry, the enhanced version of getopt is required. Exiting." >&2 exit 1 fi # Initialize the global variables USER_NAME='' SRC_DIR='' DEST_DIR='' PREFIX="home_" SUFFIX='.tar.gz' KEEP=5 IS_REMOVE='' IS_VERBOSE='' # Parse the command-line options if ! readonly PARSED_OPTIONS=$(getopt --options=${OPTIONS} \ --longoptions=${LONGOPTS} \ --name "$(basename "$0")" -- "$@"); then echo 'Unknown error parsing getopt options. Exiting.' >&2 exit 2 fi eval set -- "${PARSED_OPTIONS}" # Extract command-line options and their arguments, if any while (( $# >= 1 )); do case "$1" in -u|--user) USER_NAME=$(trim "$2") ; shift 2 ;; -s|--srcdir) SRC_DIR=$(chomp_slash "$(trim "$2")") ; shift 2 ;; -d|--destdir) DEST_DIR=$(chomp_slash "$(trim "$2")") ; shift 2 ;; -p|--prefix) PREFIX=$(trim "$2") ; shift 2 ;; -x|--suffix) SUFFIX=$(trim "$2") ; shift 2 ;; -k|--keep) KEEP=$(trim "$2") ; shift 2 ;; -r|--remove) IS_REMOVE='true' ; shift ;; -v|--verbose) IS_VERBOSE='true' ; shift ;; -h|--help) usage_exit 0 ;; --) shift ; break ;; *) usage_exit 3 >&2 ;; esac done # Ensure that required options have been supplied if [[ -z "${USER_NAME}" || -z "${SRC_DIR}" || -z "${DEST_DIR}" ]]; then usage_exit 4 >&2 fi } # parse_cmd_line ################################################################################ # Start of program parse_cmd_line "$@" # Determine the tar options readonly DEST_FILENAME="${DEST_DIR}/${PREFIX}${TIMESTAMP}${SUFFIX}" if [ -n "$IS_VERBOSE" ]; then readonly VERBOSE='--verbose' else readonly VERBOSE='' fi readonly FLAGS="--create --preserve-permissions --atime-preserve \ ${VERBOSE} --use-compress-program='pigz'" TAR_CMD="sudo tar ${FLAGS} --file='${DEST_FILENAME}' '$(basename "${SRC_DIR}")'" [ -z "${IS_VERBOSE}" ] && TAR_CMD="${TAR_CMD} &>/dev/null" # Optionally keep the last ${KEEP}-1 archives if [ -n "${IS_REMOVE}" ]; then files_signature="${DEST_DIR}/${PREFIX}*${SUFFIX}" if (( "${KEEP}" > 0 )); then set +e # Temporarily disable error exiting sudo ls -1tr "${files_signature}" | \ tail +"${KEEP}" | sudo xargs rm --force "${VERBOSE}" set -e # Reenable error exiting else sudo rm --force "${VERBOSE}" "${files_signature}" fi fi exit # Archive the source directory # # Using cd instead of tar's -C (i.e., directory) flag because I was getting # the unexpected result of only the Desktop directory being archived # if this script is executed by root's crontab; # archiving from the $SRC_DIRs parent so that the archive is rooted at the # $SRC_DIRs basename pushd . &>/dev/null cd "$(dirname "${SRC_DIR}")" eval "${TAR_CMD}" || true popd &>/dev/null # Change the owner of the generated archive file sudo chown "${USER_NAME}":"${USER_NAME}" "${DEST_FILENAME}" 2>/dev/null echo_if_verbose "Changed owner of: ${DEST_FILENAME} to: ${USER_NAME}" # Report that we are done, and how long the script took if [ -n "${IS_VERBOSE}" ]; then readonly ENDED_AT=$(date '+%s') readonly DIFF=$(( ENDED_AT - STARTED_AT )) echo "Done, in ${DIFF} seconds"! fi