Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/bin/bash
- fail() { kill -SIGHUP $$; }
- set -e
- shopt -s nullglob
- cd $(dirname $0 || fail)
- name='Debrecen'
- rhost=jcrowe
- rpath='~/srv/debrecen'
- copy_to='/mnt/hydra/archive/game/Minecraft'
- target="$name.$(date +%Y-%m-%d || fail).tgz"
- keep_recent=10
- log_file="${0%.sh}.log"
- log_truncate=1000
- min_kb=400 # Minimum transfer size in kB for automatic compression and copying.
- min_ah=12 # Minimum age in hours of last archive before creation of next.
- max_fh=36 # Maximum continuous not-empty period in hours before forced backup.
- max_fp=3 # Maximum successive not-empty periods before forced backup.
- max_fg=8 # Maximum gap in hours between checks in a not-empty period.
- ne_fst_f=.not-empty-first # Mod time: start of current not-empty period.
- ne_lst_f=.not-empty-last # Mod time: last check in current not-empty period.
- ne_cnt_f=.not-empty-count # Content: number of times $max_fg has been exceeded.
- lock_f=.backup.sh.lock
- st=$'\e[1m'
- sd=$'\e[33m'
- sf=$'\e[36m'
- se=$'\e[31;1m'
- e=$'\e[0m'
- exit_() {
- exit_code="$1"; ! shift; prefix="$1"; ! shift; message="$prefix$@"
- if [[ "$message" != "" ]]; then echo >&2 "$message$e"; fi
- if [[ "$BACKUP_SH_SYSTEMD" == "1" ]]; then sleep 1; fi
- exit $exit_code
- }
- exit_usage() { echo >&2 -n "\
- Usage: $(basename $0 || fail) [OPTIONS...]
- Options:
- -a --auto Proceed only if (1) today's archive does not exist, (2) the latest
- archive is at least $min_ah hours old, and (3) the server is empty,
- or has been found continuously non-empty for the last $max_fh hours
- or during the last $max_fp separate continuous periods.
- -f --force Overwrite existing destination files. Incompatible with -a.
- -s --sync Only synchronise local files. Incompatible with -c.
- -c --copy Only copy latest archive downstream. Incompatible with -a and -s.
- -h --help Show this message and exit.
- "; exit $1; }
- if [[ "$BACKUP_SH_CRON" == "1" ]]; then lock_f="$log_file.lock"; fi
- if [[ -e $lock_f ]]; then
- exit_ 1 $se "Error: $lock_f exists: another instance may be running. Aborting."
- else
- touch $lock_f
- trap "rm -f $lock_f" EXIT
- fi
- if [[ "$BACKUP_SH_CRON" == "1" ]]; then
- BACKUP_SH_CRON=2 "$0" "$@" &>"$log_file.tmp" || cat "$log_file.tmp"
- cat "$log_file.tmp" >> "$log_file"
- echo >> $log_file
- if (( `wc -l "$log_file" | grep -Po '^\S+'` > 2*$log_truncate )); then
- tail "$log_file" -n $log_truncate > "$log_file.tmp"
- cat "$log_file.tmp" > "$log_file"
- fi
- rm "$log_file.tmp"
- exit
- fi
- if [[ "$BACKUP_SH_SYSTEMD" == "1" || "$BACKUP_SH_CRON" == "2" ]]; then
- echo >&2 "${st}--- $(basename $0 || fail) $(date +'%Y-%m-%d %H:%M:%S' || fail) ---$e"
- fi
- ntasks=3
- for arg in "$@"; do
- case "$arg" in
- '-f' | '--force')
- force=1
- ;;
- '-a' | '--auto')
- auto=1
- ;;
- '-s' | '--sync')
- sync=1; ntasks=1
- ;;
- '-c' | '--copy')
- copy=1; ntasks=1
- target=( $name.*.tgz ); target=${target[-1]}
- ;;
- '-h' | '--help')
- exit_usage 0
- ;;
- *)
- echo >&2 "${se}Error: unrecognised argument: $arg$e"; exit_usage 2
- esac
- done
- if [[ "$auto" && "$copy" ]]; then
- exit_ 1 $se "Error: --auto and --copy are incompatible."
- elif [[ "$auto" && "$force" ]]; then
- exit_ 1 $se "Error: --auto and --force are incompatible."
- elif [[ "$sync" && "$copy" ]]; then
- exit_ 1 $se "Error: --sync and --copy are incompatible."
- fi
- if [[ "$copy" != 1 && "$sync" != 1 && "$force" != 1 && -e "$target" ]]; then
- if [[ "$auto" == 1 ]]; then
- rm -f .not-empty-{first,last,count}
- exit_ 0 $sd "Deferred: $target exists."
- else
- exit_ 1 $se "Error: $target exists. Aborting. Run with --force to overwrite."
- fi
- fi
- if [[ "$auto" != 1 && "$sync" != 1 && "$force" != 1 && -e "$copy_to/$target" ]]; then
- exit_ 1 $se "Error: $copy_to/$target exists. Aborting." \
- "Run with --force to overwrite."
- fi
- if [[ "$auto" == 1 ]]; then
- now=$(date '+%s' || fail)
- files=( $name.*.tgz )
- if (( ${#files[@]} > 0 )); then
- latest_age=$(( now - $(stat -c'%Y' "${files[-1]}" || fail) ))
- if (( latest_age < 3600 * $min_ah )); then
- exit_ 0 $sd "Deferred: ${files[-1]} is" \
- "$((latest_age/3600)) < $min_ah hours old."
- fi
- fi
- if [[ -e $ne_cnt_f ]]; then
- ne_count=$(cat $ne_cnt_f || fail);
- else
- ne_count=0;
- fi
- if [[ -e $ne_lst_f ]]; then
- if [[ ! -e $ne_fst_f ]]; then cp -p .not-empty-{last,first}; fi
- ne_fst_age=$(( now - $(stat -c'%Y' $ne_fst_f || fail) ))
- ne_lst_age=$(( now - $(stat -c'%Y' $ne_lst_f || fail) ))
- if (( $ne_lst_age > 3600 * $max_fg )); then
- rm -f .not-empty-{first,last}
- echo $(( ++ne_count )) > $ne_cnt_f
- if (( ne_count > max_fp )); then
- echo >&2 "${sf}Backup forced: server not empty for" \
- "$ne_count > $max_fp periods.$e"
- skip_count=1;
- fi
- elif (( $ne_fst_age >= 3600 * $max_fh )); then
- echo >&2 "${sf}Backup forced: server not empty for" \
- "$((ne_fst_age/3600)) >= $max_fh hours.$e"
- skip_count=1
- fi
- fi
- if (( skip_count != 1 )) \
- && (( $(ssh $rhost "$rpath/backup.sh" --player-count || fail) > 0 )); then
- touch -t $(date -d @$now '+%Y%m%d%H%M.%S' || fail) $ne_lst_f
- exit_ 0 $sd "Deferred: server not empty" \
- "(for $((ne_fst_age/3600)) < $max_fh hours" \
- "and $ne_count <= $max_fp periods)."
- fi
- rm -f .not-empty-{first,last,count}
- fi
- if [[ "$auto" != 1 && ! ( -d "$copy_to" && -w "$copy_to" ) ]]; then
- exit_ 1 $se "Error: $copy_to is not a writable directory. Aborting."
- fi
- if [[ "$copy" != 1 ]]; then
- task=$(( task+1 ))
- if (( task > 1 )); then echo >&2; fi
- echo >&2 "[$task/$ntasks] Synchronising local files..."
- ssh $rhost "$rpath/backup.sh" --pre
- rs_out=$( \
- rsync -hrtp --delete --info=progress2,stats1 "$rhost:$rpath/" "$name/" \
- | tee >(cat >&2) \
- ) || rs_exit=$?
- ssh $rhost "$rpath/backup.sh" --post
- if (( rs_exit != 0 )); then exit_ $rs_exit; fi
- fi
- if [[ "$copy" != 1 && "$sync" != 1 ]]; then
- task=$(( task+1 ))
- if (( task > 1 )); then echo >&2; fi
- echo >&2 "[$task/$ntasks] Compressing..."
- if [[ "$auto" && "$rs_out" =~ 'received '([0-9]+)(\.[0-9]+)?(K?)' bytes' ]]; then
- if ! [[ "${BASH_REMATCH[3]}" ]] || (( ${BASH_REMATCH[1]} < min_kb )); then
- exit_ 0 $sd "Skipped: less than ${min_kb} kB was received."
- fi
- fi
- excl="--exclude=$name/minecraft_server.*.jar"
- tar -c $excl $name \
- | pv -fs $(du -bsc $excl $name | grep -Po '^\S+(?=\s+total$)' || fail) \
- | gzip -9c > ${target}.tmp \
- && mv ${target}.tmp $target \
- && du -bhs $target \
- || rm -f $target ${target}.tmp
- # Delete all old archives except:
- # 1. The most recent $keep_recent - 1 archives (excluding $target).
- # 2. The first existing archive of each month.
- rm_files=()
- for file in $name.*.tgz; do
- if [[ "$month" == "${file%-??.tgz}" && "$file" != "$target" ]]; then
- rm_files+=("$file")
- fi
- month="${file%-??.tgz}"
- done
- for (( i=0; i<${keep_recent}-1 && ${#rm_files[@]}>0; i++ )); do
- unset 'rm_files[-1]'
- done
- for file in "${rm_files[@]}" $name.*.tgz.tmp; do
- echo rm "$file"
- rm "$file"
- done
- fi
- if [[ "$sync" != 1 ]]; then
- task=$(( task+1 ))
- if (( task > 1 )); then echo >&2; fi
- echo >&2 -e "[$task/$ntasks] Copying..."
- if [[ -d "$copy_to" && -w "$copy_to" ]]; then
- rsync -htp --progress $target $copy_to/$target
- for file in $copy_to/$name.*.tgz $copy_to/.$name.*.tgz.*; do
- if [[ "$file" == "$copy_to/$target" ]]; then continue; fi
- echo rm $file
- rm $file
- done
- else
- echo >&2 "${sd}Skipped: $copy_to is not a writable directory.$e"
- fi
- fi
- exit_ 0
Add Comment
Please, Sign In to add comment