diff options
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | Readme.md | 65 | ||||
-rw-r--r-- | backends/borg.sh | 84 | ||||
-rw-r--r-- | backends/bup.sh | 71 | ||||
-rw-r--r-- | backends/tar.sh | 73 | ||||
-rwxr-xr-x | server.sh | 348 | ||||
-rw-r--r-- | serverconf.sh | 27 | ||||
-rwxr-xr-x | tests.sh | 122 |
8 files changed, 706 insertions, 105 deletions
@@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Jonas Gunz and Contributors + +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. @@ -1,18 +1,31 @@ # minecraft-server-tools -My minecraft server management script with safe online Backup. +My minecraft server management script with safe online backup. + +Supports backing up using several backends to multiple local and remote directories. + +## Dependencies + +- [tmux](https://github.com/tmux/tmux) + +Either of backup backends: +- tar +- [bup](https://github.com/bup/bup) +- [borgbackup](https://github.com/borgbackup/borg) + +All of them are available on the Debian repository. ## Configuration -Config-variables are located at the top of `server.sh` +Config variables are located at `serverconf.sh`. ## Usage -`./server.sh start|stop|attach|status|backup` +`./server.sh start|stop|attach|status|backup|fbackup|restore|ls` ### start -Creates a `screen` session and starts a minecraft server within. +Creates a `tmux` session and starts a minecraft server within. Fails, if a session is already running with the same sessionname. ### stop @@ -21,26 +34,54 @@ Sends `stop` command to running server instance to safely shut down. ### attach -attaches to `screen` session. Exit with `CTRL + A d` +Attaches to tmux session with a server. Detach with `CTRL + A d`. ### status -lists active screen sessions with `SCREEN_SESSIONNAME`. +Shows if the server is running. ### backup -Backs up the world as a `tar.gz` archive in `./backup/`. -If a running server is detected, +Creates a backup of the current world: + +- If a running server is detected, the world is flushed to disk and autosave is disabled temporarily to prevent chunk corruption. -The command specified in `$BACKUP_HOOK` is +- Initializes backup directories if needed. + +- Backs up the world **if there are players on the server**. +The backup has `$BACKUP_NAME_<current time>` prefix. + +- Performs tests: backup is pulled from each backup directory and is compared to the current world. +This behaviour is controlled with `$BACKUP_CHECK_MODE`. + +- The command specified in `$BACKUP_HOOK` is executed on every successful backup. `$ARCHNAME` contains the relative path to the archive. This can be used to further process the created backup. +This is recommended for automated backups. + +**Warning** If all players leave just before a backup, progress is not saved. + +### fbackup + +Does the same as `backup`, but does not check for presence of players. + +This is not recommended for automated use except for deduplicating backup backends (bup and borgbackup). + +### restore + +Restores a backup from selected directory. +Old world is preserved with a current timestamp. + +### ls + +Lists backups in all directories. + ## Start automatically Create user and group `minecraft` with home in `/var/minecraft`. -Populate the directory with server.sh and a server jar. +Populate the directory with `server.sh`, `serverconf.sh`, `backends` and a server jar. Place `minecraft.service` in `/etc/systemd/system/` and run `systemctl start minecraft` to start once or `systemctl enable minecraft` to enable autostarting. @@ -62,3 +103,7 @@ The scripts are provided as-is at no warranty. They are in no way idiot-proof. Improvements are welcome. + +## TODO +- Allow non-forced backup to be run one time with no players on the server. +- Reach similar automated behaviour without depending on systemd? diff --git a/backends/borg.sh b/backends/borg.sh new file mode 100644 index 0000000..ec39bc8 --- /dev/null +++ b/backends/borg.sh @@ -0,0 +1,84 @@ +function borg_init() { + local encryption + if [ -z "$BACKUP_PASSCOMMAND" ] ; then + log_info "borg: no password given, repository is not protected" + encryption="none" + else + encryption="repokey-blake2" + fi + + export BORG_PASSCOMMAND="$BACKUP_PASSCOMMAND" + for backup_dir in ${BACKUP_DIRS[*]} + do + log_debug "Initializing repo at $backup_dir " + # borg will check if repo exists + borg init --encryption="$encryption" "$backup_dir" + done +} + +function borg_create_backup() { + export BORG_PASSCOMMAND="$BACKUP_PASSCOMMAND" + local retcode=255 + for backup_dir in ${BACKUP_DIRS[*]} + do + export BORG_REPO="$backup_dir" + + trap 'echo [WARNING] $( date ) Backup interrupted >&2; exit 2' INT TERM + + log_info "borg: backing up to \"$backup_dir\"" + + borg create \ + "${backup_dir}::${BACKUP_NAME}_$(date +'%F_%H-%M-%S')" \ + "$WORLD_NAME" \ + --filter AME \ + --compression lz4 \ + --exclude-caches \ + + local backup_exit=$? + + log_debug "borg: pruning repository at \"$backup_dir\"" + + borg prune \ + --prefix '{hostname}-' \ + --keep-minutely 2 \ + --keep-hourly 24 \ + --keep-daily 7 \ + --keep-weekly 4 \ + --keep-monthly 6 \ + "$backup_dir" + + local prune_exit=$? + + # use highest exit code as global exit code + local global_exit=$(( backup_exit > prune_exit ? backup_exit : prune_exit )) + retcode=$(( global_exit > retcode ? global_exit : retcode )) + + if [ ${global_exit} -eq 0 ]; then + log_debug "borg: backup and prune finished successfully" + elif [ ${global_exit} -eq 1 ]; then + log_info "borg: backup and/or prune finished with warnings" + else + log_error "borg: backup and/or prune finished with errors" + fi + #exit ${global_exit} + done + return $retcode +} + +# server_restore relies on output format of this function +function borg_ls() { + export BORG_PASSCOMMAND="$BACKUP_PASSCOMMAND" + borg list "$1" | cut -d' ' -f1 | sort -r +} + +function borg_restore() { + export BORG_PASSCOMMAND="$BACKUP_PASSCOMMAND" + local remote="$1" + local snapshot="$2" + local dest="$3" + + export BORG_REPO="$remote" + cd "$dest" + borg extract "${remote}::${snapshot}" + cd - > /dev/null +} diff --git a/backends/bup.sh b/backends/bup.sh new file mode 100644 index 0000000..6aa92b6 --- /dev/null +++ b/backends/bup.sh @@ -0,0 +1,71 @@ +# use first not-remote backup directory as bup's local repository +# defaults to ".bup" +function bup_local() { + for backup_dir in ${BACKUP_DIRS[*]} + do + if [[ $backup_dir != *:* ]]; then + echo "$backup_dir" + return + fi + done + echo ".bup" +} + +function bup_init() { + bup -d "$(bup_local)" index "$WORLD_NAME" + local status=$? + if [ $status -ne 0 ]; then + log_debug "bup: no local repo found, creating..." + bup -d "$(bup_local)" init -r "$(bup_local)" + log_debug "bup: created local repo at $(bup_local)" + fi +} + +function bup_create_backup() { + log_debug "bup: backup started" + + bup -d "$(bup_local)" index "$WORLD_NAME" + + # 0 if saved to at least one non-local repository + # TODO make more strict? + local retcode=1 + for backup_dir in ${BACKUP_DIRS[*]} + do + log_info "bup: backing up to \"$backup_dir\"" + # try to save to remote + bup -d "$(bup_local)" save -r "$backup_dir" -n "$BACKUP_NAME" "$WORLD_NAME" + local status=$? + # if failed - reinit remote and try again + if [ $status -ne 0 ]; then + log_debug "bup: failed backing up to \"$backup_dir\", reinitializing remote..." + bup -d "$(bup_local)" init -r "$backup_dir" + status=$? + if [ $status -eq 0 ]; then + log_debug "bup: created remote at \"$backup_dir\"" + bup -d "$(bup_local)" save -r "$backup_dir" -n "$BACKUP_NAME" "$WORLD_NAME" + else + log_error "bup: failed to make remote at \"$backup_dir\", moving on" + fi + else + if [ ! "$backup_dir" = "$(bup_local)" ]; then + retcode=0 + fi + fi + done + + log_debug "bup: backup finished" + return $retcode +} + +# server_restore relies on output format of this function +function bup_ls() { + local backup_dir="$1" + bup -d "$(bup_local)" ls -r "$backup_dir" "$BACKUP_NAME" | sort -r +} + +function bup_restore() { + local remote="$1" + local snapshot="$2" + local dest="$3" + bup -d "$(bup_local)" restore -r "$remote" --outdir "$dest" "$BACKUP_NAME/$snapshot/$PWD/." +} diff --git a/backends/tar.sh b/backends/tar.sh new file mode 100644 index 0000000..6748ac9 --- /dev/null +++ b/backends/tar.sh @@ -0,0 +1,73 @@ +function tar_init() { + # nothing to do for tar? + : +} + +# TODO: Make default .tar with optional bup +function tar_create_backup() { + log_debug "tar: backing up..." + + local status + + # save world to a temporary archive + local archname="/tmp/${BACKUP_NAME}_`date +%F_%H-%M-%S`.tar.gz" + tar -czf "$archname" "./$WORLD_NAME" + status=$? + if [ $status -ne 0 ]; then + log_error "tar: failed to save the world" + rm "$archname" #remove (probably faulty) archive + return 1 + fi + log_debug "tar: world saved to $archname, pushing it to backup directories..." + + # 0 if could save to at least one backup dir + # TODO: make more strict? + local retcode=1 + for backup_dir in ${BACKUP_DIRS[*]} + do + log_info "tar: backing up to \"$backup_dir\"" + # scp acts as cp for local destination directories + scp "$archname" "$backup_dir/" + status=$? + if [ $status -ne 0 ]; then + log_error "tar: failed pushing to \"$backup_dir\", moving on" + else + retcode=0 + fi + done + + rm "$archname" + + log_debug "tar: backup finished" + + return $retcode +} + +# server_restore relies on output format of this function +function tar_ls() { + local backup_dir="$1" + + if [[ "$backup_dir" == *:* ]]; then + local remote="$(echo "$backup_dir" | cut -d: -f1)" + local remote_dir="$(echo "$backup_dir" | cut -d: -f2)" + ssh "$remote" "ls -1 $remote_dir" | grep "tar.gz" | sort -r + else + ls -1 "$backup_dir" | grep "tar.gz" | sort -r + fi +} + +function tar_restore() { + local remote="$1" + local snapshot="$2" + local dest="$3" + local status + + scp "$remote/$snapshot" "/tmp/" + status=$? + if [ $status -ne 0 ]; then + log_error "tar: failed to get archive from \"$remote/$snapshot\"" + return 1 + fi + + tar -xzf "/tmp/$snapshot" -C "$dest" +} @@ -4,39 +4,51 @@ if [ -e "serverconf.sh" ] then source "serverconf.sh" else - echo No configuration found in PWD. Exiting. + log_error "No configuration found in PWD. Exiting." exit 1 fi +source "backends/tar.sh" +source "backends/bup.sh" +source "backends/borg.sh" + +function log_debug() { + if [ $VERBOSE -ne 0 ]; then + printf "[DEBUG] $1\n" >&2 + fi +} +log_info() { printf "[INFO] $1\n" ; } +log_error() { printf "[ERROR] $1\n" >&2 ; } + function backup_hook_example { - bup -d $CUR_BACK_DIR ls -l $BACKUP_NAME/latest/var/minecraft + bup -d $CUR_BACK_DIR ls -l "$BACKUP_NAME/latest/var/minecraft" } function send_cmd () { tmux -S $TMUX_SOCKET send -t $TMUX_WINDOW "$1" enter } -function assert_running() { +function assert_not_running() { if server_running; then - echo "It seems a server is already running. If this is not the case,\ + log_info "It seems a server is already running. If this is not the case,\ manually attach to the running screen and close it." exit 1 fi } -function assert_not_running() { +function assert_running() { if ! server_running; then - echo "Server not running" + log_info "Server not running" exit 1 fi } function server_start() { - assert_running + assert_not_running if [ ! -f "eula.txt" ] then - echo "eula.txt not found. Creating and accepting EULA." + log_info "eula.txt not found. Creating and accepting EULA." echo "eula=true" > "eula.txt" fi @@ -44,7 +56,7 @@ function server_start() { $JRE_JAVA $JVM_ARGS -jar $JAR $JAR_ARGS pid=`tmux -S $TMUX_SOCKET list-panes -t $TMUX_WINDOW -F "#{pane_pid}"` echo $pid > $PIDFILE - echo Started with PID $pid + log_info "Started with PID $pid" exit } @@ -52,7 +64,7 @@ function server_stop() { # Allow success even if server is not running #trap "exit 0" EXIT - assert_not_running + assert_running send_cmd "stop" local RET=1 @@ -63,7 +75,7 @@ function server_stop() { RET=$? done - echo "stopped the server" + log_info "stopped the server" rm -f $PIDFILE @@ -71,7 +83,7 @@ function server_stop() { } function server_attach() { - assert_not_running + assert_running tmux -S $TMUX_SOCKET attach -t $TMUX_WINDOW exit } @@ -81,16 +93,16 @@ function server_running() { ps -p $(cat $PIDFILE) > /dev/null return fi - + false } function server_status() { if server_running then - echo "Server is running" + log_info "Server is running" else - echo "Server is not running" + log_info "Server is not running" fi exit } @@ -106,132 +118,282 @@ function players_online() { [ `tail -n 3 "$LOGFILE" | grep -c "There are 0"` -lt 1 ] } +function backup_backend_run() { + local cmd + # could do ${BACKUP_BACKEND}_$1 though + if [ $BACKUP_BACKEND = "bup" ]; then + cmd="bup" + elif [ $BACKUP_BACKEND = "borg" ]; then + cmd="borg" + else + cmd="tar" + fi + cmd="${cmd}_$1" + log_debug "Backup backend command: \"$cmd\"" + eval "$cmd" +} + +function init_backup() { + # even though bup and borg are smart, they will not create a path for a repo + for backup_dir in ${BACKUP_DIRS[*]} + do + if [[ $backup_dir == *:* ]]; then + local remote="$(echo "$backup_dir" | cut -d: -f1)" + local remote_dir="$(echo "$backup_dir" | cut -d: -f2)" + ssh "$remote" "mkdir -p \"$remote_dir\"" + else + mkdir -p "$backup_dir" + fi + done + + backup_backend_run "init" +} + +function same_world() { + delta=$(diff -r "$1" "$2") + if [ $? -ne 0 ]; then + return 1 + fi + if [ -z "$delta" ] ; then + return 0 + fi + return 1 +} + +# checking if latest snapshots are the same as the current world +function test_backup_integrity() { + if [ $BACKUP_CHECK_MODE = 0 ]; then + log_info "Backup integrity check: skipped" + return 0 + fi + + local retcode=0 + for backup_dir in ${BACKUP_DIRS[*]} + do + if [[ "$backup_dir" == *:* ]] && [ $BACKUP_CHECK_MODE = 1 ]; then + log_info "Skipping check of remote backup at $backup_dir" + continue + fi + + local tmpdir=$(mktemp -d); + + # restore most recent backup to a temporary dir + if ! server_restore "$backup_dir" 0 "$tmpdir" ; then + log_error "Failed to get latest snapshot from \"$backup_dir\"" + retcode=1 + elif ! same_world "$WORLD_NAME" "$tmpdir/$WORLD_NAME" ; then + log_error "Latest backup from \"$backup_dir\" differs from current world!" + retcode=1 + else + log_info "Backup at \"$backup_dir\" is OK" + fi + + rm -r "$tmpdir" + done + + if [ $retcode -ne 0 ] ; then + log_error "Backup integrity check: FAILED" + else + log_info "Backup integrity check: OK" + fi + return $retcode +} + +function create_backup() { + init_backup + backup_backend_run "create_backup" + test_backup_integrity +} function server_backup_safe() { - force=$1 - echo "Detected running server. Checking if players online..." + local force=$1 + log_info "Detected running server. Checking if players online..." if [ "$force" != "true" ] && ! players_online; then - echo "Players are not online. Not backing up." + log_info "Players are not online. Not backing up." return fi - echo "Disabling autosave" + log_info "Disabling autosave" send_cmd "save-off" send_cmd "save-all flush" - echo "Waiting for save... If froze, run /save-on to re-enable autosave!!" - + log_info "Waiting for save... If froze, run /save-on to re-enable autosave!!" + sleep 1 while [ $(tail -n 3 "$LOGFILE" | grep -c "Saved the game") -lt 1 ] do sleep 1 done sleep 2 - echo "Done! starting backup..." + log_info "Done! starting backup..." + + create_backup - if [ $USE_BUP = "YES" ]; then - create_bup_backup - else - create_backup_archive - fi - local RET=$? - echo "Re-enabling auto-save" + log_info "Re-enabling auto-save" send_cmd "save-on" - if [ $RET -eq 0 ] - then - echo Running backup hook + if [ $RET -eq 0 ]; then + log_info "Running backup hook" $BACKUP_HOOK fi } function server_backup_unsafe() { - echo "No running server detected. Running Backup" + log_info "No running server detected. Running Backup" + + create_backup + local status=$? + + if [ $status -eq 0 ]; then + log_info "Running backup hook" + $BACKUP_HOOK + fi +} + +function backup_running() { + systemctl is-active --quiet mc-backup.service +} + +function fbackup_running() { + systemctl is-active --quiet mc-fbackup.service +} + +function server_backup() { + local force=$1 - if [ $USE_BUP = "YES" ]; then - create_bup_backup + if [ "$force" = "true" ]; then + if backup_running; then + log_info "A backup is running. Aborting..." + return + fi else - create_backup_archive + if fbackup_running; then + log_info "A force backup is running. Aborting..." + return + fi fi - if [ $? -eq 0 ] - then - echo Running backup hook - $BACKUP_HOOK + if server_running; then + server_backup_safe "$force" + else + server_backup_unsafe fi } -function create_bup_backup() { - BACKUP_DIR="mc-backups" - CUR_BACK_DIR="mc-backups/$CUR_YEAR" +function server_ls_backups() { + for backup_dir in ${BACKUP_DIRS[*]} + do + log_info "backups in ${backup_dir}:" + backup_backend_run "ls \"$backup_dir\"" + done +} + +# creates a selection dialog +function choose_from() { + local items=("$@") + select item in "${items[@]}"; do + echo "$item" + return + done +} + +# checks if an item is in the array +function is_in() { + local item="$1" + shift + local array=("$@") - if [ ! -d "$CUR_BACK_DIR" ]; then - mkdir -p "$CUR_BACK_DIR" + # these :space: things allow checking that *exactly* this item is in array + if [[ ${array[*]} =~ (^|[[:space:]])"$item"($|[[:space:]]) ]]; then + return fi + false +} +function server_restore() { + local backup_dir + local snapshot_index + local dest="$PWD" - bup -d "$CUR_BACK_DIR" index "$WORLD_NAME" - status=$? - if [ $status -eq 1 ]; then - bup -d "$CUR_BACK_DIR" init - bup -d "$CUR_BACK_DIR" index "$WORLD_NAME" + # parameters are only used for testing backups, so thorough checks are not needed + if [ $# -ge 2 ]; then + backup_dir="$1" + snapshot_index=$2 + fi + if [ $# -eq 3 ]; then + dest="$3" fi - bup -d "$CUR_BACK_DIR" save -n "$BACKUP_NAME" "$WORLD_NAME" + if [ "$dest" = "$PWD" ]; then + assert_not_running + fi - echo "Backup using bup to $CUR_BACK_DIR is complete" -} + if [ ${#BACKUP_DIRS[@]} -eq 0 ]; then + log_error "No backup directories found, abort" + return 1 + fi -# TODO: Make default .tar with optional bup -function create_backup_archive() { - ARCHNAME="backup/$WORLD_NAME-backup_`date +%d-%m-%y-%T`.tar.gz" - tar -czf "$ARCHNAME" "./$WORLD_NAME" - if [ ! $? -eq 0 ] - then - echo "TAR failed. No Backup created." - rm $ARCHNAME #remove (probably faulty) archive + if [ -z $backup_dir ]; then + log_info "From where get the snapshot?" + backup_dir="$(choose_from "${BACKUP_DIRS[@]}")" + fi + if ! is_in "$backup_dir" "${BACKUP_DIRS[@]}" ; then + log_error "No valid backup directory selected, abort" return 1 - else - echo $ARCHNAME created. fi -} -function backup_running() { - systemctl is-active --quiet mc-backup.service -} -function fbackup_running() { - systemctl is-active --quiet mc-fbackup.service -} + local snapshots="$( backup_backend_run "ls \"$backup_dir\"" )" + log_debug "Snapshots found:" + log_debug "$snapshots" + if [ -z "$snapshots" ]; then + log_error "No snapshots found, abort" + return 1 + fi + # convert multiline string to bash array + snapshots=($(echo "$snapshots")) -function server_backup() { - force=$1 - if [ "$force" = "true" ]; then - if backup_running; then - echo "A backup is running. Aborting..." - return - fi - else - if fbackup_running; then - echo "A force backup is running. Aborting..." - return - fi + local snapshot + if [ -z $snapshot_index ]; then + log_info "Select which snapshot to restore" + snapshot=$(choose_from "${snapshots[@]}") + else + snapshot="${snapshots[snapshot_index]}" + fi + if ! is_in "$snapshot" "${snapshots[@]}" ; then + log_error "No valid snapshot selected, abort" + return 1 fi - if server_running; then - server_backup_safe "$force" - else - server_backup_unsafe + + log_debug "Restoring snapshot \"$snapshot\" from \"$backup_dir\"" + + # if we restore to PWD, we will overwrite the current world, which might be harmful + local oldworld_name + if [ "$dest" = "$PWD" ] && [[ -d "$WORLD_NAME" ]]; then + oldworld_name="${WORLD_NAME}.old.$(date +'%F_%H-%M-%S.%N')" + log_info "Preserving old world: $(mv -n -v "$PWD/$WORLD_NAME" "$PWD/$oldworld_name")" fi - exit -} -function ls_bup() { - bup -d "mc-backups/${CUR_YEAR}" ls "mc-sad-squad/$1" + backup_backend_run "restore \"$backup_dir\" \"$snapshot\" \"$dest\"" + local status=$? + + + # if we preseved the current world, but failed to restore the snapshot + if [ ! -z ${oldworld_name+x} ] && [ $status -ne 0 ]; then + log_error "Failed to restore snapshot, putting old world back where it was:" + rm -rv "$PWD/$WORLD_NAME" + mv -v "$PWD/$oldworld_name" "$PWD/$WORLD_NAME" + return 1 + fi + + log_debug "Snapshot restored" + + return 0 } #cd $(dirname $0) @@ -249,7 +411,9 @@ case $1 in "backup") server_backup ;; - # TODO: Add restore command + "restore") + server_restore + ;; "status") server_status ;; @@ -257,7 +421,7 @@ case $1 in server_backup "true" ;; "ls") - ls_bup $2 + server_ls_backups ;; *) echo "Usage: $0 start|stop|attach|status|backup" diff --git a/serverconf.sh b/serverconf.sh index cb88dc4..5531969 100644 --- a/serverconf.sh +++ b/serverconf.sh @@ -1,10 +1,12 @@ # serverconf.sh -# configuration file for server.sh minecraft server +# configuration file for server.sh minecraft server # management script +VERBOSE=0 + #CONFIG JRE_JAVA="java" -JVM_ARGS="-Xms4096M -Xmx6144M" +JVM_ARGS="-Xms4096M -Xmx6144M" JAR="fabric-server-launch.jar" JAR_ARGS="-nogui" @@ -12,12 +14,31 @@ TMUX_WINDOW="minecraft" TMUX_SOCKET="mc_tmux_socket" WORLD_NAME="lfja" +if [ -f "server.properties" ]; then + WORLD_NAME=$(grep level-name server.properties | cut -d= -f2) + echo "Getting world name from server.properties: $WORLD_NAME" +fi BACKUP_NAME="${WORLD_NAME}_backup" LOGFILE="logs/latest.log" PIDFILE="server-screen.pid" -USE_BUP="NO" +# if not bup or borg, uses tar by default +BACKUP_BACKEND="tar" +#BACKUP_BACKEND="bup" +#BACKUP_BACKEND="borg" #Constants CUR_YEAR=`date +"%Y"` +# IMPORTANT: local paths must be absolute! +BACKUP_DIRS=( "$PWD/.bak/$CUR_YEAR" "user@backupserver:/path/to/backup/$CUR_YEAR" ) + +# borg repository could be pasword-protected +# to avoid having to manually type password, borg can run a command that should echo a password +#BACKUP_PASSCOMMAND="echo superstrongpassword" +#BACKUP_PASSCOMMAND="pass passwordname" + +# 0 - don't check backups after creation +# 1 - check only local backups +# 2 - check local and remote backups (may take a while if world is large and connection is slow) +BACKUP_CHECK_MODE=1 diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000..38ce83b --- /dev/null +++ b/tests.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# these tests will create a "old" and "new/current" worlds, make backups of then and check if they actually correspond to what they supposed to +# if tests succeed, you'll see the last line "All tests passed" + +source server.sh + +VERBOSE=1 + +WORLD_NAME="test_world" + +function test_backend() { + BACKUP_BACKEND="$1" + + # doing tests inside /tmp/ + DIR=$(mktemp -d) + cd "$DIR" + local status=$? + if [ $status -ne 0 ] ; then + echo "Failed to make a temporary directory" + return 1 + fi + function cleanup() { + rm -r "$DIR" + } + # TODO: testing remote directories + BACKUP_DIRS=( "$PWD/.bak" ) + + # make a "world" with some volatile data + function make_world() { + mkdir -p "$DIR/$1/DIM0" + date > "$1/data0" + shuf -e {1..100} | tr '\n' ' ' > "$DIR/$1/DIM0/data1" + } + + # make two versions of a world and back up both + make_world "$WORLD_NAME" + local old_world="${WORLD_NAME}.orig0" + cp -r "$DIR/$WORLD_NAME" "$DIR/$old_world" + if ! server_backup ; then + cleanup + exit + fi + + # backup time in archive's name is specified up to seconds, so subsequent backups without some delay will have the same name and previous backup be overwritten + if [ $BACKUP_BACKEND = "tar" ]; then + sleep 1 + fi + + make_world "$WORLD_NAME" + local new_world="${WORLD_NAME}.orig1" + cp -r "$DIR/$WORLD_NAME" "$DIR/$new_world" + if ! server_backup ; then + cleanup + exit + fi + + local backups="$(server_ls_backups)" + if [ -z "$backups" ]; then + log_error "Found no backups" + cleanup + exit + fi + + # corrupting current (new) world + find "$DIR/$WORLD_NAME" -type f -exec shred {} \; + if same_world "$DIR/$WORLD_NAME" "$DIR/$new_world" ; then + log_error "Failed to corrupt new world" + cleanup + exit + fi + + + # restore new backup + server_restore "${BACKUP_DIRS[0]}" 0 + # must be: new backup == new world + if ! same_world "$DIR/$WORLD_NAME" "$DIR/$new_world" ; then + log_error "${BACKUP_BACKEND}: new backup != new world" + cleanup + exit + fi + # must be: new backup != old world + if same_world "$DIR/$WORLD_NAME" "$DIR/$old_world" ; then + log_error "${BACKUP_BACKEND}: new backup == old world" + cleanup + exit + fi + + + # restore old backup + if [ $BACKUP_BACKEND = "bup" ]; then + # bup's 0th option is "latest", which links to 1st option, this is not present in tar and borg + server_restore "${BACKUP_DIRS[0]}" 2 + else + server_restore "${BACKUP_DIRS[0]}" 1 + fi + # must be: old backup == old world + if ! same_world "$DIR/$WORLD_NAME" "$DIR/$old_world" ; then + log_error "${BACKUP_BACKEND}: old backup != old world" + cleanup + exit + fi + # must be: old backup != new world + if same_world "$DIR/$WORLD_NAME" "$DIR/$new_world" ; then + log_error "${BACKUP_BACKEND}: old backup == new world" + cleanup + exit + fi + + cleanup +} + +printf "\n\n\nTesting tar backend\n" +test_backend "tar" + +printf "\n\n\nTesting bup backend\n" +test_backend "bup" + +printf "\n\n\nTesting borg backend\n" +test_backend "borg" + +printf "\n\n\nAll tests passed\n" |