joo

Debrecen local backup.sh

joo
Apr 11th, 2019 (edited)
189
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 8.23 KB | None | 0 0
  1. #!/bin/bash
  2.  
  3. fail() { kill -SIGHUP $$; }
  4.  
  5. set -e
  6. shopt -s nullglob
  7. cd $(dirname $0 || fail)
  8.  
  9. name='Debrecen'
  10. rhost=jcrowe
  11. rpath='~/srv/debrecen'
  12. copy_to='/mnt/hydra/archive/game/Minecraft'
  13. target="$name.$(date +%Y-%m-%d || fail).tgz"
  14. keep_recent=10
  15.  
  16. log_file="${0%.sh}.log"
  17. log_truncate=1000
  18.  
  19. min_kb=400 # Minimum transfer size in kB for automatic compression and copying.
  20.  
  21. min_ah=12 # Minimum age in hours of last archive before creation of next.
  22. max_fh=36 # Maximum continuous not-empty period in hours before forced backup.
  23. max_fp=3  # Maximum successive not-empty periods before forced backup.
  24. max_fg=8  # Maximum gap in hours between checks in a not-empty period.
  25.  
  26. ne_fst_f=.not-empty-first # Mod time: start of current not-empty period.
  27. ne_lst_f=.not-empty-last  # Mod time: last check in current not-empty period.
  28. ne_cnt_f=.not-empty-count # Content: number of times $max_fg has been exceeded.
  29. lock_f=.backup.sh.lock
  30.  
  31. st=$'\e[1m'
  32. sd=$'\e[33m'
  33. sf=$'\e[36m'
  34. se=$'\e[31;1m'
  35. e=$'\e[0m'
  36.  
  37. exit_() {
  38.     exit_code="$1"; ! shift; prefix="$1"; ! shift; message="$prefix$@"
  39.     if [[ "$message" != "" ]]; then echo >&2 "$message$e"; fi
  40.     if [[ "$BACKUP_SH_SYSTEMD" == "1" ]]; then sleep 1; fi
  41.     exit $exit_code
  42. }
  43.  
  44. exit_usage() { echo >&2 -n "\
  45. Usage: $(basename $0 || fail) [OPTIONS...]
  46.  
  47.  Options:
  48.    -a --auto   Proceed only if (1) today's archive does not exist, (2) the latest
  49.                archive is at least $min_ah hours old, and (3) the server is empty,
  50.                or has been found continuously non-empty for the last $max_fh hours
  51.                or during the last $max_fp separate continuous periods.
  52.  
  53.    -f --force  Overwrite existing destination files. Incompatible with -a.
  54.  
  55.    -s --sync   Only synchronise local files. Incompatible with -c.
  56.  
  57.    -c --copy   Only copy latest archive downstream. Incompatible with -a and -s.
  58.  
  59.    -h --help   Show this message and exit.
  60. "; exit $1; }
  61.  
  62. if [[ "$BACKUP_SH_CRON" == "1" ]]; then lock_f="$log_file.lock"; fi
  63. if [[ -e $lock_f ]]; then
  64.     exit_ 1 $se "Error: $lock_f exists: another instance may be running. Aborting."
  65. else
  66.     touch $lock_f
  67.     trap "rm -f $lock_f" EXIT
  68. fi
  69.  
  70. if [[ "$BACKUP_SH_CRON" == "1" ]]; then
  71.     BACKUP_SH_CRON=2 "$0" "$@" &>"$log_file.tmp" || cat "$log_file.tmp"
  72.     cat "$log_file.tmp" >> "$log_file"
  73.     echo >> $log_file
  74.     if (( `wc -l "$log_file" | grep -Po '^\S+'` > 2*$log_truncate )); then
  75.         tail "$log_file" -n $log_truncate > "$log_file.tmp"
  76.         cat "$log_file.tmp" > "$log_file"
  77.     fi
  78.     rm "$log_file.tmp"
  79.     exit
  80. fi
  81.  
  82. if [[ "$BACKUP_SH_SYSTEMD" == "1" || "$BACKUP_SH_CRON" == "2" ]]; then
  83.     echo >&2 "${st}--- $(basename $0 || fail) $(date +'%Y-%m-%d %H:%M:%S' || fail) ---$e"
  84. fi
  85.  
  86. ntasks=3
  87. for arg in "$@"; do
  88.     case "$arg" in
  89.         '-f' | '--force')
  90.             force=1
  91.             ;;
  92.         '-a' | '--auto')
  93.             auto=1
  94.             ;;
  95.         '-s' | '--sync')
  96.             sync=1; ntasks=1
  97.             ;;
  98.         '-c' | '--copy')
  99.             copy=1; ntasks=1
  100.             target=( $name.*.tgz ); target=${target[-1]}
  101.             ;;
  102.         '-h' | '--help')
  103.             exit_usage 0
  104.             ;;
  105.         *)
  106.             echo >&2 "${se}Error: unrecognised argument: $arg$e"; exit_usage 2
  107.     esac
  108. done
  109.  
  110. if [[ "$auto" && "$copy" ]]; then
  111.     exit_ 1 $se "Error: --auto and --copy are incompatible."
  112. elif [[ "$auto" && "$force" ]]; then
  113.     exit_ 1 $se "Error: --auto and --force are incompatible."
  114. elif [[ "$sync" && "$copy" ]]; then
  115.     exit_ 1 $se "Error: --sync and --copy are incompatible."
  116. fi
  117.  
  118. if [[ "$copy" != 1 && "$sync" != 1 && "$force" != 1 && -e "$target" ]]; then
  119.     if [[ "$auto" == 1 ]]; then
  120.         rm -f .not-empty-{first,last,count}
  121.         exit_ 0 $sd "Deferred: $target exists."
  122.     else
  123.         exit_ 1 $se "Error: $target exists. Aborting. Run with --force to overwrite."
  124.     fi
  125. fi
  126.  
  127. if [[ "$auto" != 1 && "$sync" != 1 && "$force" != 1 && -e "$copy_to/$target" ]]; then
  128.     exit_ 1 $se "Error: $copy_to/$target exists. Aborting." \
  129.                 "Run with --force to overwrite."
  130. fi
  131.  
  132. if [[ "$auto" == 1 ]]; then
  133.     now=$(date '+%s' || fail)
  134.     files=( $name.*.tgz )
  135.     if (( ${#files[@]} > 0 )); then
  136.         latest_age=$(( now - $(stat -c'%Y' "${files[-1]}" || fail) ))
  137.         if (( latest_age < 3600 * $min_ah )); then
  138.             exit_ 0 $sd "Deferred: ${files[-1]} is" \
  139.                         "$((latest_age/3600)) < $min_ah hours old."
  140.         fi
  141.     fi
  142.  
  143.     if [[ -e $ne_cnt_f ]]; then
  144.         ne_count=$(cat $ne_cnt_f || fail);
  145.     else
  146.         ne_count=0;
  147.     fi
  148.  
  149.     if [[ -e $ne_lst_f ]]; then
  150.         if [[ ! -e $ne_fst_f ]]; then cp -p .not-empty-{last,first}; fi
  151.         ne_fst_age=$(( now - $(stat -c'%Y' $ne_fst_f || fail) ))
  152.         ne_lst_age=$(( now - $(stat -c'%Y' $ne_lst_f || fail) ))
  153.         if (( $ne_lst_age > 3600 * $max_fg )); then
  154.             rm -f .not-empty-{first,last}
  155.             echo $(( ++ne_count )) > $ne_cnt_f
  156.             if (( ne_count > max_fp )); then
  157.                 echo >&2 "${sf}Backup forced: server not empty for" \
  158.                          "$ne_count > $max_fp periods.$e"
  159.                 skip_count=1;
  160.             fi
  161.         elif (( $ne_fst_age >= 3600 * $max_fh )); then
  162.             echo >&2 "${sf}Backup forced: server not empty for" \
  163.                      "$((ne_fst_age/3600)) >= $max_fh hours.$e"
  164.             skip_count=1
  165.         fi
  166.     fi
  167.  
  168.     if (( skip_count != 1 )) \
  169.     && (( $(ssh $rhost "$rpath/backup.sh" --player-count || fail) > 0 )); then
  170.         touch -t $(date -d @$now '+%Y%m%d%H%M.%S' || fail) $ne_lst_f
  171.         exit_ 0 $sd "Deferred: server not empty" \
  172.                     "(for $((ne_fst_age/3600)) < $max_fh hours" \
  173.                     "and $ne_count <= $max_fp periods)."
  174.     fi
  175.     rm -f .not-empty-{first,last,count}
  176. fi
  177.  
  178. if [[ "$auto" != 1 && ! ( -d "$copy_to" && -w "$copy_to" ) ]]; then
  179.     exit_ 1 $se "Error: $copy_to is not a writable directory. Aborting."
  180. fi
  181.  
  182. if [[ "$copy" != 1 ]]; then
  183.     task=$(( task+1 ))
  184.     if (( task > 1 )); then echo >&2; fi
  185.     echo >&2 "[$task/$ntasks] Synchronising local files..."
  186.  
  187.     ssh $rhost "$rpath/backup.sh" --pre
  188.     rs_out=$( \
  189.         rsync -hrtp --delete --info=progress2,stats1 "$rhost:$rpath/" "$name/" \
  190.         | tee >(cat >&2) \
  191.     ) || rs_exit=$?
  192.     ssh $rhost "$rpath/backup.sh" --post
  193.     if (( rs_exit != 0 )); then exit_ $rs_exit; fi
  194. fi
  195.  
  196. if [[ "$copy" != 1 && "$sync" != 1 ]]; then
  197.     task=$(( task+1 ))
  198.     if (( task > 1 )); then echo >&2; fi
  199.     echo >&2 "[$task/$ntasks] Compressing..."
  200.  
  201.     if [[ "$auto" && "$rs_out" =~ 'received '([0-9]+)(\.[0-9]+)?(K?)' bytes' ]]; then
  202.         if ! [[ "${BASH_REMATCH[3]}" ]] || (( ${BASH_REMATCH[1]} < min_kb )); then
  203.             exit_ 0 $sd "Skipped: less than ${min_kb} kB was received."
  204.         fi
  205.     fi
  206.  
  207.     excl="--exclude=$name/minecraft_server.*.jar"
  208.     tar -c $excl $name \
  209.         | pv -fs $(du -bsc $excl $name | grep -Po '^\S+(?=\s+total$)' || fail) \
  210.         | gzip -9c > ${target}.tmp \
  211.         && mv ${target}.tmp $target \
  212.         && du -bhs $target \
  213.         || rm -f $target ${target}.tmp
  214.  
  215.     # Delete all old archives except:
  216.     # 1. The most recent $keep_recent - 1 archives (excluding $target).
  217.     # 2. The first existing archive of each month.
  218.     rm_files=()
  219.     for file in $name.*.tgz; do
  220.         if [[ "$month" == "${file%-??.tgz}" && "$file" != "$target" ]]; then
  221.             rm_files+=("$file")
  222.         fi
  223.         month="${file%-??.tgz}"
  224.     done
  225.     for (( i=0; i<${keep_recent}-1 && ${#rm_files[@]}>0; i++ )); do
  226.         unset 'rm_files[-1]'
  227.     done
  228.     for file in "${rm_files[@]}" $name.*.tgz.tmp; do
  229.         echo rm "$file"
  230.         rm "$file"
  231.     done
  232. fi
  233.  
  234. if [[ "$sync" != 1 ]]; then
  235.     task=$(( task+1 ))
  236.     if (( task > 1 )); then echo >&2; fi
  237.     echo >&2 -e "[$task/$ntasks] Copying..."
  238.     if [[ -d "$copy_to" && -w "$copy_to" ]]; then
  239.         rsync -htp --progress $target $copy_to/$target
  240.         for file in $copy_to/$name.*.tgz $copy_to/.$name.*.tgz.*; do
  241.             if [[ "$file" == "$copy_to/$target" ]]; then continue; fi
  242.             echo rm $file
  243.             rm $file
  244.         done
  245.     else
  246.         echo >&2 "${sd}Skipped: $copy_to is not a writable directory.$e"
  247.     fi
  248. fi
  249.  
  250. exit_ 0
Add Comment
Please, Sign In to add comment