CCCPaste

tar-home.sh

#!/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