Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Backup script for Starbound server

Overview

I run a private Starbound server, which I have described in great detail on this site. Of course I need backups!

The script

The backup task, and the corresponding restore task, are all bundled into one self-contained script! This whole project is just one shell script. Er, and of course a cron job.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/bin/sh
# File: bup_v2_starbound.sh
# License: CC-BY-SA 4.0
# Author: bgstack15
# Startate: 2020-08-09 13:24
# Title: Incremental Backup and Restore Solution for Starbound
# Purpose: Take backups, full once a week and incremental daily. Full backup happens on Mondays.
# Usage:
#    Variables:
#       OUTDIR path where backup tarballs are placed
#       INFILE input directory to be backed up
#       RESTORE=0 Use =1 to choose the restore action instead of backup action.
#       BUP_DATE Override builtin TODAY variable (YYYY-MM-DD), primarily for testing purposes
#       LOGFILE
#       RESTOREPOINT=YYYY-MM-DD  Date to restore to, if RESTORE=1. Accepts string "today"
#       DESTDIR  Path to restore to. It is recommended to restore elsewhere, validate, and then place in real location.
#       DRYRUN=  Use =1 to not take any actions. Useful with DEBUG=1
#       DEBUG=  Use =1 to be verbose
#       SERVERACTIONSOFF=  Use =1 to skip the start/stop service commands
#    Examples:
#       cronjob: 30 3 * * * steam /usr/bin/sh /home/steam/bin/bup_v2_starbound.sh 1>/dev/null 2>&1
#       restore with:
#          time SERVERACTIONSOFF=1 DESTDIR=/tmp/universe.restore RESTOREPOINT=today DEBUG=1 RESTORE=1 sh bup_v2_starbound.sh
# Reference:
#    https://www.computernetworkingnotes.com/linux-tutorials/create-and-restore-incremental-backups-in-linux-with-tar.html
# Improve:
# Documentation:
#    The overall architecture of this tool uses a weekly full backup with daily incrementals strategy. These are mostly hard-coded and will be difficult to change the cadence.

# Define variables
OUTDIR=/mnt/public/Support/Systems/server4/home/steam/Steam/storage
INFILE=/home/steam/Steam/storage/universe
test -z "${RESTORE}" && RESTORE=0 # if RESTORE=1, then we want to perform a restore action.
EXITFILE="$( mktemp )"
trap '_ect=$? ; trap "" 1; rm -f "${EXITFILE:-NOTHINGTODELETE}" ; exit ${_ect} ;' 1

# calculated variables
TODAY="$( date "+%F" )"
test -n "${BUP_DATE}" && TODAY="${BUP_DATE}" # use BUP_DATE to override real date, for testing purposes.
WEEK="$( date -d "${TODAY}" "+%G-W%V" )"
# use incremental backup...
BUP_TYPE="inc"                                      
# unless today is Monday. If you change day of week, then the WEEK value as used for snapshot file might not be useful or correct.
test "$( date -d "${TODAY}" "+%u" )" = "1" && BUP_TYPE="full" 
test -z "${LOGFILE}" && LOGFILE="${OUTDIR}/bup.${TODAY}.log"
INFILENAME="$( basename "${INFILE}" )"

# Functions
backup_v2() {
    # use variables OUTFILE SNAPSHOTFILE INFILENAME INFILE
    # assume already changed directory to the working directory
   cd "$( dirname "${INFILE}" )"
    tar -czg "${SNAPSHOTFILE}" -f "${OUTFILE}" "${INFILENAME}"
}

restore_v2() {
    # needed input: RESTOREPOINT="2020-08-03" DESTDIR="/home/steam/Steam/storage/universe.restored" OUTDIR
   # goals: this function calculates which tarballs are needed, that would match that week number up until that date. So that last full one, up until the exact date requested.
   matchfile="$( find "${OUTDIR}" -name "${INFILENAME}*" | grep "${RESTOREPOINT}" )"
   if test "$( echo "${matchfile}" | wc -l )" != "1" || test "${matchfile}" = "\r" || test "${matchfile}" = "" ;
   then
      # if the number of matching files for the requested date is not 1, then we have a problem.
      echo "Unable to determine which files among ${matchfile} to use. Aborted."
      echo "1" > "${EXITFILE}" ; return 1
   else
      # but now that we have just the one, we need to find its parents back up to the full.
      # this awk will look at the 8 preceding filenames to the exact one and then find the last "full" one and all following ones, including the requested one.
      #all_files="$( ls -1 "${OUTDIR}" | grep -B8 "${RESTOREPOINT}" | sort -t'-' -k2,3,4 | awk 'BEGIN{a=0} /full/{a=a+1;if(a>1){a=1;for(i in b){b[i]=""};x=0};} a==1{b[x++]=$0;} END{for (i in b){if (b[i]!="")print b[i]}}' )"
      all_files="$( find "${OUTDIR}" -maxdepth 1 -name '*z' -printf '%f\n' | sort | grep -B8 "${RESTOREPOINT}" | awk 'BEGIN{a=0} /full/{a=a+1;if(a>1){a=1;for(i in b){b[i]=""};x=0};} a==1{b[x++]=$0;} END{for (i in b){if (b[i]!="")print b[i]}}' | sort )"

      # begin the restore
      test -n "${_RESTOREDRYRUN}" && lecho "Would restore to date ${RESTOREPOINT} to ${DESTDIR}"
      test -n "${_RESTOREDRYRUN}" || { mkdir -p "${DESTDIR}" ; cd "${DESTDIR}" ; }
      echo "${all_files}" | while read thisfile ;
      do
         if test -n "${_RESTOREDRYRUN}" ;
         then
            lecho "Would extract file ${thisfile}"
         else
            # do extractions for real
            lecho "Extract file ${thisfile}"
            tar -zxf "${OUTDIR}/${thisfile}"
         fi
      done

   fi
}

# Main
{

   ## calculate derived values
   OUTFILE="${OUTDIR}/${INFILENAME}.${WEEK}.${TODAY}.${BUP_TYPE}.tar.gz"
   SNAPSHOTFILE="${OUTDIR}/snapshot.${WEEK}"

   ## debug info
   test -n "${DEBUG}" && {
      case "${RESTORE}" in
         0|bup|backup)
            echo "OUTFILE ${OUTFILE}"
            echo "SNAPSHOTFILE ${SNAPSHOTFILE}"
            echo "RESTORE ${RESTORE}"
            ;;
         1|restore)
            echo "DESTDIR ${DESTDIR}"
            echo "RESTOREPOINT ${RESTOREPOINT}"
            ;;
      esac
      echo "LOGFILE ${LOGFILE}"
      echo "DRYRUN ${DRYRUN}"
      echo "SERVERACTIONSOFF ${SERVERACTIONSOFF}"
   }

   case "${RESTORE}" in
      0|bup|backup) # so take a backup
            if test -z "${DRYRUN}" ;
            then
            test -z "${SERVERACTIONSOFF}" && {
               lecho "STOPPING starbound server"
               sudo systemctl stop starbound-server.service
            }
            lecho "START starbound bup to ${OUTFILE}"
                backup_v2
                lecho "STOP starbound bup"
            test -z "${SERVERACTIONSOFF}" && {
               lecho "STARTING starbound server"
               sudo systemctl start starbound-server.service
            }
            else
            test -z "${SERVERACTIONSOFF}" && lecho "Would STOP starbound server"
            lecho "Would run backup"
            test -z "${SERVERACTIONSOFF}" && lecho "Would START starbound server"
            fi
         ;;
      1|restore) # restore from backup
         test -z "${DESTDIR}" && {
            echo "Need to provide DESTDIR value. Aborted." 1>&2
            echo "1" > "${EXITFILE}" ; exit 1
         }
         test -z "${RESTOREPOINT}" && {
            echo "Need to provide YYYY-MM-DD date for RESTOREPOINT. Aborted." 1>&2
            echo "1" > "${EXITFILE}" ; exit 1
         }
         test "${RESTOREPOINT}" = "today" && RESTOREPOINT="$( date "+%F" )"
         if test -z "${DRYRUN}" ;
         then
            test -z "${SERVERACTIONSOFF}" && {
               lecho "STOPPING starbound server"
               sudo systemctl stop starbound-server.service
            }
                lecho "START starbound restore to ${DESTDIR}"
            restore_v2
            lecho "STOP starbound restore"
            test -z "${SERVERACTIONSOFF}" && {
               lecho "STARTING starbound server"
               sudo systemctl start starbound-server.service
            }
         else
            test -z "${SERVERACTIONSOFF}" && lecho "Would STOP starbound server"
            _RESTOREDRYRUN=1 restore_v2
            test -z "${SERVERACTIONSOFF}" && lecho "Would START starbound server"
         fi   
         ;;
      *)
         echo "Unknown action ${RESTORE}. No action taken."
         ;;
   esac

} 2>&1 | tee -a "${LOGFILE}"

_ec="$( cat "${EXITFILE}" 2>/dev/null )" ; test -z "${_ec}" && _ec=0
rm -f "${EXITFILE}"
exit ${_ec}

It's important to include all the restore logic. What's the point of incremental backups if you cannot properly restore from them?

Comments