aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jonas Gunz <himself@jonasgunz.de> 2022-01-25 11:01:32 +0100
committerGravatar Jonas Gunz <himself@jonasgunz.de> 2022-01-25 11:01:32 +0100
commit9b35f4f8f9253ffb2e7aa8d7ee7526472ab191f2 (patch)
tree7bbe74ae3924f479ede973e0a9603b5c0646b2f4
parentb0e2841205ed1f5ce8223c05e90258b30ea88879 (diff)
parent77a9baea740a49c2204893a022fd4bf25d109c07 (diff)
downloadminecraft-server-tools-master.tar.gz
Merge branch 'TheMightyV-dev-tmv'HEADmaster
-rw-r--r--LICENSE21
-rw-r--r--Readme.md65
-rw-r--r--backends/borg.sh84
-rw-r--r--backends/bup.sh71
-rw-r--r--backends/tar.sh73
-rwxr-xr-xserver.sh348
-rw-r--r--serverconf.sh27
-rwxr-xr-xtests.sh122
8 files changed, 706 insertions, 105 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a03b259
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Readme.md b/Readme.md
index 757e394..304521d 100644
--- a/Readme.md
+++ b/Readme.md
@@ -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"
+}
diff --git a/server.sh b/server.sh
index 7e453a7..d8caa16 100755
--- a/server.sh
+++ b/server.sh
@@ -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"