# backuper.sh - simple framework for customizable, safe and reliable backups # # Copyright (C) 2016 Robin Obůrka # # 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 . ## Settings GPG_RECIPIENTS="" MBUFFER_SIZE="100M" ## Internal variables - backup process NAME="" REMOTE="" GENERATE="" COMMAND="" PLACES="" EXCLUDE="" TAR_EXTRA_PARAMS="" STORE="" AFTER="" FILE_TYPE="" DST="" FILENAME="" FILENAME_BASE="" ## Pipeline variables RUNDIR="" PID_GENERATE="" PID_STORE="" PID_FILE="pids" STAGE="0" PIPELINE_FILE_TYPE="bz2.gpg" CUSTOM_STAGE_FILE="custom_stage" ## Internal variables - program logic CONFIG_FILE_NAME=".backuper.sh.conf" ## Output helpers title() { TXT="$1" printf "\n%s\n==========================================\n" "$TXT" } get_size() { FILE="$1" du -h "$FILE" | cut -f1 } time_fmt() { TXT="$1" TIME_FMT="\n$TXT:\t%E" echo "$TIME_FMT" } ## Getting data helpers get_home() { CURRENT_USER="$(id --real --user --name)" CU_HOME="$(getent passwd "$CURRENT_USER" | cut -d: -f6)" echo "$CU_HOME" } result_file() { [ -z "$COMMAND" -a -z "$PLACES" ] && error "Specify type of backup (command or places) before filename query" [ -z "$STORE" ] && error "Specify store mechanism before filename query" if [ -n "$DST" ]; then echo "$DST"/"$FILENAME" else echo "$FILENAME" fi } compute_gpg_recipients() { GPG_RECIPIENTS_LIST="" for R in $GPG_RECIPIENTS; do GPG_RECIPIENTS_LIST="$GPG_RECIPIENTS_LIST --recipient "$R"" done } ## Program control error() { if [ -n "$1" ]; then echo "ERROR: $1" >&2 fi ## -6 ~ SIGABRT ## This is the only SIGNAL that cleanup doesn't reset kill -6 "$$" } ## Interface functions name() { NAME="$1" compute_variables } remote() { REMOTE="$@" } generate() { GENERATE="$@" } cmd() { COMMAND="$@" adjust_file_name } file_type() { [ -n "$PLACES" ] && error "Do not change file type with places type of backup" FILE_TYPE="$1" adjust_file_name } places() { PLACES="$1" [ "$#" -gt 1 ] && error "Specify places as string" adjust_file_name } exclude() { STR="" for EXC in "$@"; do STR="$STR --exclude=$EXC" done EXCLUDE="$STR" } tar_extra_param() { PARAM="$1" if ! echo "$TAR_EXTRA_PARAMS" | grep -q -- "$PARAM" ; then TAR_EXTRA_PARAMS="$TAR_EXTRA_PARAMS $PARAM" fi } no_file_change() { tar_extra_param "--warning='no-file-changed'" } numeric_owner() { tar_extra_param "--numeric-owner" } store() { STORE="$@" } after() { AFTER="$@" } run() { [ -z "$COMMAND" -a -z "$PLACES" ] && error "Specify backup before run command" title "Run command" echo "$@" echo $@ | /usr/bin/time -f "$(time_fmt "Command took")" $REMOTE sh -s } store_file() { [ -z "$1" ] && error "Specify destination" DST="$1" store "sh -c 'cat > ${DST}/${FILENAME}'" } ## This is not standart feature but it is common for several project I know store_ssh() { [ -z "$1" ] && error "Specify server" CONNECTION="$1" add_custom_stage "echo_and_copy "$FILENAME"" store "ssh "$CONNECTION"" } echo_and_copy() { echo "$1" ; cat } add_custom_stage() { echo "$@" >> "$RUNDIR"/"$CUSTOM_STAGE_FILE" } ## Internal functions test_input() { [ -z "$NAME" ] && error "Specify backup name" [ -z "$COMMAND" -a -z "$PLACES" ] && error "Specify backup subject" [ -n "$COMMAND" -a -n "$PLACES" ] && error "Do not specify two types of backup at once" [ -z "$STORE" ] && error "Specify store command" [ -z "$GPG_RECIPIENTS" ] && error "Set GPG recipient for encrypted backups" } compute_variables() { FILENAME_BASE="backup_"$NAME"_"$TS"" compute_gpg_recipients } adjust_file_name() { FILENAME="$FILENAME_BASE" [ -n "$FILE_TYPE" ] && FILENAME="$FILENAME"."$FILE_TYPE" [ -n "$PLACES" ] && FILENAME="$FILENAME".tar [ -n "$PIPELINE_FILE_TYPE" ] && FILENAME="$FILENAME"."$PIPELINE_FILE_TYPE" } prepare() { trap 'error_handler' EXIT INT QUIT TERM ABRT HUP ILL TRAP BUS FPE SEGV UTS="$(date +"%s")" TS="$(date +"%Y-%m-%d_%H-%M-%S")" RUNDIR="/tmp/backuper_run_dir_$$_$UTS" mkdir -p "$RUNDIR" chmod 700 "$RUNDIR" } error_handler() { cleanup exit 42 } cleanup() { trap - EXIT INT QUIT TERM HUP ILL TRAP BUS FPE SEGV if [ -f "$RUNDIR"/"$PID_FILE" ]; then for PID in $(cat "$RUNDIR"/"$PID_FILE"); do if ps --ppid $$ | grep -q "$PID"; then kill "$PID" fi done fi [ -n "$PID_GENERATE" ] && if ps --ppid $$ | grep -q "$PID_GENERATE"; then kill "$PID_GENERATE" fi [ -n "$PID_STORE" ] && if ps --ppid $$ | grep -q "$PID_STORE"; then kill "$PID_STORE" fi if [ -d "$RUNDIR" ]; then rm -rf "$RUNDIR" fi } backup_done() { if [ -n "$AFTER" ]; then title "After backup hook" echo "$AFTER" | /usr/bin/time -f "$(time_fmt "Command took")" sh -s || error "After-backup hook failed" fi } load_config() { HOME="$(get_home)" [ -f "$HOME"/"$CONFIG_FILE_NAME" ] && . "$HOME"/"$CONFIG_FILE_NAME" } add_stage() { ## Prepare pipes NEXTSTAGE="$((STAGE+1))" [ ! -p "$RUNDIR"/fifo_"$STAGE" ] && mkfifo "$RUNDIR"/fifo_"$STAGE" [ ! -p "$RUNDIR"/fifo_"$NEXTSTAGE" ] && mkfifo "$RUNDIR"/fifo_"$NEXTSTAGE" ## Run requested command and store PID $@ > "$RUNDIR"/fifo_"$NEXTSTAGE" < "$RUNDIR"/fifo_"$STAGE" || error "One of the pipeline commands failed" & echo "$!" >> "$RUNDIR"/"$PID_FILE" ## Process started - increment STAGE="$NEXTSTAGE" } start_store() { STORE="$STORE < "$RUNDIR"/fifo_"$NEXTSTAGE"" echo "$STORE" | sh -s } start_command() { title "Create backup" if [ -n "$COMMAND" ]; then echo "$COMMAND" | /usr/bin/time -f "$(time_fmt "Backup took")" $REMOTE $GENERATE sh -s > "$RUNDIR"/fifo_0 else ## Exclude keep without quotes /usr/bin/time -f "$(time_fmt "Backup took")" $REMOTE $GENERATE tar c --preserve-permissions $TAR_EXTRA_PARAMS $EXCLUDE $PLACES > "$RUNDIR"/fifo_0 fi } start_pipeline() { add_stage "mbuffer -q -m "$MBUFFER_SIZE"" add_stage "pbzip2" add_stage "mbuffer -q -m "$MBUFFER_SIZE"" add_stage "gpg --trust-model always --batch --yes -z0 $GPG_RECIPIENTS_LIST -e" ## OK, this is strange. If store command fail, mbuffer forks itself with ## PID that I'm not able to get (event from list of children). ## ... and it takes 100% CPU. So, for now: Do not use mbuffer as the last one. #add_stage "mbuffer -q -m "$MBUFFER_SIZE"" if [ -f "$RUNDIR"/"$CUSTOM_STAGE_FILE" ]; then while read LINE; do add_stage "$LINE" done <