Major refactor to support new backends

This commit is contained in:
Nicolas Chan 2020-03-21 17:33:21 -07:00
parent 5eaae39e73
commit 2ec8f7f4dc
5 changed files with 473 additions and 243 deletions

269
backup.sh
View file

@ -15,8 +15,7 @@ BACKUP_DIRECTORY="" # Directory to save backups in
MAX_BACKUPS=128 # -1 indicates unlimited
DELETE_METHOD="thin" # Choices: thin, sequential, none; sequential: delete oldest; thin: keep last 24 hourly, last 30 daily, and monthly (use with 1 hr cron interval)
COMPRESSION_ALGORITHM="zstd" # Leave empty for no compression
EXIT_IF_NO_SCREEN=false
USE_BORG_BACKUP=false # Use borg backup
EXIT_IF_NO_SCREEN=false # Skip backup if there is no minecraft screen running
COMPRESSION_FILE_EXTENSION=".zst" # Leave empty for no compression; Precede with a . (for example: ".gz")
COMPRESSION_LEVEL=3 # Passed to the compression algorithm
ENABLE_CHAT_MESSAGES=false # Tell players in Minecraft chat about backup status
@ -31,7 +30,6 @@ TIMESTAMP=$(date +$DATE_FORMAT)
while getopts 'a:bcd:e:f:ghi:l:m:o:p:qs:v' FLAG; do
case $FLAG in
a) COMPRESSION_ALGORITHM=$OPTARG ;;
b) USE_BORG_BACKUP=true ;;
c) ENABLE_CHAT_MESSAGES=true ;;
d) DELETE_METHOD=$OPTARG ;;
e) COMPRESSION_FILE_EXTENSION=".$OPTARG" ;;
@ -39,7 +37,6 @@ while getopts 'a:bcd:e:f:ghi:l:m:o:p:qs:v' FLAG; do
g) EXIT_IF_NO_SCREEN=true ;;
h) echo "Minecraft Backup (by Nicolas Chan)"
echo "-a Compression algorithm (default: gzip)"
echo "-b Use borg backup (default: false)"
echo "-c Enable chat messages"
echo "-d Delete method: thin (default), sequential, none"
echo "-e Compression file extension, exclude leading \".\" (default: gz)"
@ -64,17 +61,14 @@ while getopts 'a:bcd:e:f:ghi:l:m:o:p:qs:v' FLAG; do
q) SUPPRESS_WARNINGS=true ;;
s) SCREEN_NAME=$OPTARG ;;
v) DEBUG=true ;;
*) ;;
esac
done
log-fatal () {
echo -e "\033[0;31mFATAL:\033[0m $*"
}
log-warning () {
if ! $SUPPRESS_WARNINGS; then
echo -e "\033[0;33mWARNING:\033[0m $*"
fi
}
BASE_DIR=$(dirname "$(realpath "$0")")
# shellcheck source=src/logging.sh
source "$BASE_DIR/src/logging.sh"
# Check for missing encouraged arguments
if [[ $SCREEN_NAME == "" ]]; then
@ -91,237 +85,26 @@ if [[ $BACKUP_DIRECTORY == "" ]]; then
log-fatal "Backup directory not specified (use -o)"
MISSING_CONFIGURATION=true
fi
if $MISSING_CONFIGURATION; then
exit 0
fi
if $USE_BORG_BACKUP; then
ARCHIVE_FILE_NAME=$BACKUP_DIRECTORY
ARCHIVE_PATH="$BACKUP_DIRECTORY"::'{now}'
else
ARCHIVE_FILE_NAME=$TIMESTAMP.tar$COMPRESSION_FILE_EXTENSION
ARCHIVE_PATH=$BACKUP_DIRECTORY/$ARCHIVE_FILE_NAME
fi
# Minecraft server screen interface functions
message-players () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
message-players-color "$MESSAGE" "$HOVER_MESSAGE" "gray"
}
execute-command () {
local COMMAND=$1
if ! $(screen -S "$SCREEN_NAME" -Q "select" . &> /dev/null); then
return 1
fi
if [[ "$SCREEN_NAME" != "" ]]; then
screen -S "$SCREEN_NAME" -p 0 -X stuff "$COMMAND$(printf \\r)"
fi
}
message-players-error () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
message-players-color "$MESSAGE" "$HOVER_MESSAGE" "red"
}
message-players-success () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
message-players-color "$MESSAGE" "$HOVER_MESSAGE" "green"
}
message-players-color () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
local COLOR=$3
if $DEBUG; then
echo "$MESSAGE ($HOVER_MESSAGE)"
fi
if $ENABLE_CHAT_MESSAGES; then
execute-command "tellraw @a [\"\",{\"text\":\"[$PREFIX] \",\"color\":\"gray\",\"italic\":true},{\"text\":\"$MESSAGE\",\"color\":\"$COLOR\",\"italic\":true,\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[{\"text\":\"$HOVER_MESSAGE\"}]}}}]"
fi
}
# Parse file timestamp to one readable by "date"
parse-file-timestamp () {
local DATE_STRING=$(echo $1 | awk -F_ '{gsub(/-/,":",$2); print $1" "$2}')
echo $DATE_STRING
}
# Delete a backup
delete-backup () {
local BACKUP=$1
rm -f $BACKUP_DIRECTORY/$BACKUP
message-players "Deleted old backup" "$BACKUP"
}
# Sequential delete method
delete-sequentially () {
local BACKUPS=($(ls $BACKUP_DIRECTORY))
while [[ $MAX_BACKUPS -ge 0 && ${#BACKUPS[@]} -gt $MAX_BACKUPS ]]; do
delete-backup ${BACKUPS[0]}
BACKUPS=($(ls $BACKUP_DIRECTORY))
done
}
# Functions to sort backups into correct categories based on timestamps
is-hourly-backup () {
local TIMESTAMP=$*
local MINUTE=$(date -d "$TIMESTAMP" +%M)
return $MINUTE
}
is-daily-backup () {
local TIMESTAMP=$*
local HOUR=$(date -d "$TIMESTAMP" +%H)
return $HOUR
}
is-weekly-backup () {
local TIMESTAMP=$*
local DAY=$(date -d "$TIMESTAMP" +%u)
return $((DAY - 1))
}
# Helper function to sum an array
array-sum () {
SUM=0
for NUMBER in $*; do
(( SUM += NUMBER ))
done
echo $SUM
}
# Thinning delete method
delete-thinning () {
# sub-hourly, hourly, daily, weekly is everything else
local BLOCK_SIZES=(16 24 30)
# First block is unconditional
# The next blocks will only accept files whose names cause these functions to return true (0)
local BLOCK_FUNCTIONS=("is-hourly-backup" "is-daily-backup" "is-weekly-backup")
# Warn if $MAX_BACKUPS does not have enough room for all the blocks
TOTAL_BLOCK_SIZE=$(array-sum ${BLOCK_SIZES[@]})
if [[ $TOTAL_BLOCK_SIZE -gt $MAX_BACKUPS ]]; then
log-warning "MAX_BACKUPS ($MAX_BACKUPS) is smaller than TOTAL_BLOCK_SIZE ($TOTAL_BLOCK_SIZE)"
fi
local CURRENT_INDEX=0
local BACKUPS=($(ls -r $BACKUP_DIRECTORY)) # List newest first
for BLOCK_INDEX in ${!BLOCK_SIZES[@]}; do
local BLOCK_SIZE=${BLOCK_SIZES[BLOCK_INDEX]}
local BLOCK_FUNCTION=${BLOCK_FUNCTIONS[BLOCK_INDEX]}
local OLDEST_BACKUP_IN_BLOCK_INDEX=$((BLOCK_SIZE + CURRENT_INDEX)) # Not an off-by-one error because a new backup was already saved
local OLDEST_BACKUP_IN_BLOCK=${BACKUPS[OLDEST_BACKUP_IN_BLOCK_INDEX]}
if [[ $OLDEST_BACKUP_IN_BLOCK == "" ]]; then
break
fi
local OLDEST_BACKUP_TIMESTAMP=$(parse-file-timestamp ${OLDEST_BACKUP_IN_BLOCK:0:19})
local BLOCK_COMMAND="$BLOCK_FUNCTION $OLDEST_BACKUP_TIMESTAMP"
if $BLOCK_COMMAND; then
# Oldest backup in this block satisfies the condition for placement in the next block
if $DEBUG; then
echo "$OLDEST_BACKUP_IN_BLOCK promoted to next block"
fi
else
# Oldest backup in this block does not satisfy the condition for placement in next block
delete-backup $OLDEST_BACKUP_IN_BLOCK
break
fi
((CURRENT_INDEX += BLOCK_SIZE))
done
delete-sequentially
}
# Delete old backups
delete-old-backups () {
case $DELETE_METHOD in
"sequential") delete-sequentially
;;
"thin") delete-thinning
;;
esac
}
clean-up () {
# Re-enable world autosaving
execute-command "save-on"
# Save the world
execute-command "save-all"
}
trap-ctrl-c () {
log-warning "Backup interrupted. Attempting to re-enable autosaving"
clean-up
exit 2
}
trap "trap-ctrl-c" 2
# Check if screen is running
if ! $(screen -S "$SCREEN_NAME" -Q "select" . &> /dev/null); then
if $EXIT_IF_NO_SCREEN; then
log-warning "Screen \"$SCREEN_NAME\" is not running. Exiting without backing up (because of -g option)."
exit 1
fi
fi
# Notify players of start
message-players "Starting backup..." "$ARCHIVE_FILE_NAME"
# Disable world autosaving
execute-command "save-off"
# Record start time for performance reporting
START_TIME=$(date +"%s")
if $USE_BORG_BACKUP; then
borg create --compression "$COMPRESSION_ALGORITHM,$COMPRESSION_LEVEL" "$ARCHIVE_PATH" "$SERVER_WORLD"
else
case $COMPRESSION_ALGORITHM in
"") # No compression
tar -cf $ARCHIVE_PATH -C $SERVER_WORLD .
;;
*) # With compression
tar -cf - -C $SERVER_WORLD . | $COMPRESSION_ALGORITHM -cv -$COMPRESSION_LEVEL - > $ARCHIVE_PATH 2>> /dev/null
;;
esac
fi
sync
END_TIME=$(date +"%s")
clean-up
# Notify players of completion
if $USE_BORG_BACKUP; then
BORG_LAST_ARCHIVE_INFO=$(borg info "$BACKUP_DIRECTORY" --last 1 --json)
BORG_REPO_INFO=$(borg info "$BACKUP_DIRECTORY" --json)
WORLD_SIZE_BYTES=$(echo "$BORG_LAST_ARCHIVE_INFO" | jq '.["archives"][0]["stats"]["original_size"]')
ARCHIVE_SIZE_BYTES=$(echo "$BORG_LAST_ARCHIVE_INFO" | jq '.["archives"][0]["stats"]["deduplicated_size"]')
BACKUP_DIRECTORY_SIZE=$(echo "$BORG_REPO_INFO" | jq '.["cache"]["stats"]["unique_csize"]' | numfmt --to=iec)
else
WORLD_SIZE_BYTES=$(du -b --max-depth=0 $SERVER_WORLD | awk '{print $1}')
ARCHIVE_SIZE_BYTES=$(du -b $ARCHIVE_PATH | awk '{print $1}')
BACKUP_DIRECTORY_SIZE=$(du -h --max-depth=0 $BACKUP_DIRECTORY | awk '{print $1}')
fi
ARCHIVE_SIZE=$(numfmt --to=iec "$ARCHIVE_SIZE_BYTES")
COMPRESSION_PERCENT=$(($ARCHIVE_SIZE_BYTES * 100 / $WORLD_SIZE_BYTES))
TIME_DELTA=$((END_TIME - START_TIME))
# Check that archive size is not null and at least 1024 KB
if [[ "$ARCHIVE_SIZE" != "" && "$ARCHIVE_SIZE_BYTES" -gt 8 ]]; then
message-players-success "Backup complete!" "$TIME_DELTA s, $ARCHIVE_SIZE/$BACKUP_DIRECTORY_SIZE, $COMPRESSION_PERCENT%"
if ! $USE_BORG_BACKUP; then
delete-old-backups
fi
else
message-players-error "Backup was not saved!" "Please notify an administrator"
exit 1
fi
"$BASE_DIR/src/core.sh" \
"$BASE_DIR/src/exec-methods/screen.sh" \
-s "$SCREEN_NAME" \
-- \
"$BASE_DIR/src/backup-methods/tar.sh" \
-a "$COMPRESSION_ALGORITHM" \
-d "$DELETE_METHOD" \
-e "$COMPRESSION_FILE_EXTENSION" \
-f "$TIMESTAMP" \
-i "$SERVER_WORLD" \
-l "$COMPRESSION_LEVEL" \
-m "$MAX_BACKUPS" \
-o "$BACKUP_DIRECTORY" \
-- \
-c "$ENABLE_CHAT_MESSAGES" \
-g "$EXIT_IF_NO_SCREEN" \
-p "$PREFIX" \
-q "$SUPPRESS_WARNINGS" \
-v "$DEBUG"

195
src/backup-methods/tar.sh Executable file
View file

@ -0,0 +1,195 @@
#!/bin/env bash
# Backup to (compressed) tar archives, and automatically delete
OPTIND=1
while getopts 'a:d:e:f:i:l:m:o:' FLAG; do
case $FLAG in
a) COMPRESSION_ALGORITHM=$OPTARG ;;
d) DELETE_METHOD=$OPTARG ;;
e) COMPRESSION_FILE_EXTENSION=$OPTARG ;;
f) TIMESTAMP=$OPTARG ;;
i) SERVER_WORLD=$OPTARG ;;
l) COMPRESSION_LEVEL=$OPTARG ;;
m) MAX_BACKUPS=$OPTARG ;;
o) BACKUP_DIRECTORY=$OPTARG ;;
*) ;;
esac
done
ARCHIVE_FILE_NAME=$TIMESTAMP.tar$COMPRESSION_FILE_EXTENSION
ARCHIVE_PATH=$BACKUP_DIRECTORY/$ARCHIVE_FILE_NAME
mkdir -p "$BACKUP_DIRECTORY"
# Parse file timestamp to one readable by "date"
parse-file-timestamp () {
local DATE_STRING
DATE_STRING=$(echo "$1" | awk -F_ '{gsub(/-/,":",$2); print $1" "$2}')
echo "$DATE_STRING"
}
# Delete a backup
delete-backup () {
local BACKUP=$1
rm -f "$BACKUP_DIRECTORY/$BACKUP"
message-players "Deleted old backup" "$BACKUP"
}
# Sequential delete method
delete-sequentially () {
local BACKUPS_UNSORTED_RAW=("$BACKUP_DIRECTORY"/*)
local BACKUPS_UNSORTED=()
for BACKUP_NAME in "${BACKUPS_UNSORTED_RAW[@]}"; do
local BASENAME
BASENAME=$(basename "$BACKUP_NAME")
BACKUPS_UNSORTED+=("$BASENAME")
done
local BACKUPS=()
# List oldest first
while IFS='' read -r line; do BACKUPS+=("$line"); done < <(IFS=$'\n' sort <<<"${BACKUPS_UNSORTED[*]}")
while [[ $MAX_BACKUPS -ge 0 && ${#BACKUPS[@]} -gt $MAX_BACKUPS ]]; do
delete-backup "${BACKUPS[0]}"
local BACKUPS_UNSORTED_RAW=("$BACKUP_DIRECTORY"/*)
local BACKUPS_UNSORTED=()
for BACKUP_NAME in "${BACKUPS_UNSORTED_RAW[@]}"; do
local BASENAME
BASENAME=$(basename "$BACKUP_NAME")
BACKUPS_UNSORTED+=("$BASENAME")
done
local BACKUPS=()
# List oldest first
while IFS='' read -r line; do BACKUPS+=("$line"); done < <(IFS=$'\n' sort <<<"${BACKUPS_UNSORTED[*]}")
done
}
# Functions to sort backups into correct categories based on timestamps
is-hourly-backup () {
local TIMESTAMP=$*
local MINUTE
MINUTE=$(date -d "$TIMESTAMP" +%M)
return "$MINUTE"
}
is-daily-backup () {
local TIMESTAMP=$*
local HOUR
HOUR=$(date -d "$TIMESTAMP" +%H)
return "$HOUR"
}
is-weekly-backup () {
local TIMESTAMP=$*
local DAY
DAY=$(date -d "$TIMESTAMP" +%u)
return $((DAY - 1))
}
# Helper function to sum an array
array-sum () {
SUM=0
for NUMBER in "$@"; do
(( SUM += NUMBER ))
done
echo $SUM
}
# Thinning delete method
delete-thinning () {
# sub-hourly, hourly, daily, weekly is everything else
local BLOCK_SIZES=(16 24 30)
# First block is unconditional
# The next blocks will only accept files whose names cause these functions to return true (0)
local BLOCK_FUNCTIONS=("is-hourly-backup" "is-daily-backup" "is-weekly-backup")
# Warn if $MAX_BACKUPS does not have enough room for all the blocks
TOTAL_BLOCK_SIZE=$(array-sum "${BLOCK_SIZES[@]}")
if [[ $TOTAL_BLOCK_SIZE -gt $MAX_BACKUPS ]]; then
log-warning "MAX_BACKUPS ($MAX_BACKUPS) is smaller than TOTAL_BLOCK_SIZE ($TOTAL_BLOCK_SIZE)"
fi
local CURRENT_INDEX=0
local BACKUPS_UNSORTED_RAW=("$BACKUP_DIRECTORY"/*)
local BACKUPS_UNSORTED=()
for BACKUP_NAME in "${BACKUPS_UNSORTED_RAW[@]}"; do
local BASENAME
BASENAME=$(basename "$BACKUP_NAME")
BACKUPS_UNSORTED+=("$BASENAME")
done
local BACKUPS=()
# List newest first
while IFS='' read -r line; do BACKUPS+=("$line"); done < <(IFS=$'\n' sort -r <<<"${BACKUPS_UNSORTED[*]}")
for BLOCK_INDEX in "${!BLOCK_SIZES[@]}"; do
local BLOCK_SIZE=${BLOCK_SIZES[BLOCK_INDEX]}
local BLOCK_FUNCTION=${BLOCK_FUNCTIONS[BLOCK_INDEX]}
local OLDEST_BACKUP_IN_BLOCK_INDEX=$((BLOCK_SIZE + CURRENT_INDEX)) # Not an off-by-one error because a new backup was already saved
local OLDEST_BACKUP_IN_BLOCK=${BACKUPS[OLDEST_BACKUP_IN_BLOCK_INDEX]}
if [[ $OLDEST_BACKUP_IN_BLOCK == "" ]]; then
break
fi
local OLDEST_BACKUP_TIMESTAMP
OLDEST_BACKUP_TIMESTAMP=$(parse-file-timestamp "${OLDEST_BACKUP_IN_BLOCK:0:19}")
local BLOCK_COMMAND="$BLOCK_FUNCTION $OLDEST_BACKUP_TIMESTAMP"
if $BLOCK_COMMAND; then
# Oldest backup in this block satisfies the condition for placement in the next block
if $DEBUG; then
echo "$OLDEST_BACKUP_IN_BLOCK promoted to next block"
fi
else
# Oldest backup in this block does not satisfy the condition for placement in next block
delete-backup "$OLDEST_BACKUP_IN_BLOCK"
break
fi
((CURRENT_INDEX += BLOCK_SIZE))
done
delete-sequentially
}
clean-up () {
# Re-enable world autosaving
execute-command "save-on"
# Save the world
execute-command "save-all"
}
minecraft-backup-backup () {
case $COMPRESSION_ALGORITHM in
"") # No compression
tar -cf "$ARCHIVE_PATH" -C "$SERVER_WORLD" .
;;
*) # With compression
tar -cf - -C "$SERVER_WORLD" . | $COMPRESSION_ALGORITHM -cv -"$COMPRESSION_LEVEL" - > "$ARCHIVE_PATH" 2>> /dev/null
;;
esac
sync
}
minecraft-backup-check () {
WORLD_SIZE_BYTES=$(du -b --max-depth=0 "$SERVER_WORLD" | awk '{print $1}')
ARCHIVE_SIZE_BYTES=$(du -b "$ARCHIVE_PATH" | awk '{print $1}')
BACKUP_DIRECTORY_SIZE=$(du -h --max-depth=0 "$BACKUP_DIRECTORY" | awk '{print $1}')
ARCHIVE_SIZE=$(numfmt --to=iec "$ARCHIVE_SIZE_BYTES")
COMPRESSION_PERCENT=$((ARCHIVE_SIZE_BYTES * 100 / WORLD_SIZE_BYTES))
# Check that archive size is not null and at least 1024 KB
if [[ "$ARCHIVE_SIZE" != "" && "$ARCHIVE_SIZE_BYTES" -gt 8 ]]; then
echo "$ARCHIVE_SIZE/$BACKUP_DIRECTORY_SIZE, $COMPRESSION_PERCENT%"
else
return 1
fi
}
minecraft-backup-epilog () {
case $DELETE_METHOD in
"sequential") delete-sequentially
;;
"thin") delete-thinning
;;
esac
}

185
src/core.sh Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env bash
# Minecraft server automatic backup management script
# by Nicolas Chan
# https://github.com/nicolaschan/minecraft-backup
# MIT License
#
# For most convenience, run automatically with cron.
# This script implements the core functionality, and expects the following
# functions to be defined by the method scripts:
#
# minecraft-backup-execute "$COMMAND"
# where $COMMAND is a command sent to the Minecraft server console
# minecraft-backup-backup
# which performs a backup
# minecraft-backup-check
# minecraft-backup-epilog
# Default Configuration
EXECUTE_METHOD="$1"
shift
EXECUTE_METHOD_OPTIONS=()
while [[ $1 != "--" ]]; do
EXECUTE_METHOD_OPTIONS+=("$1")
shift
done
shift
BACKUP_METHOD="$1"
shift
BACKUP_METHOD_OPTIONS=()
while [[ $1 != "--" ]]; do
BACKUP_METHOD_OPTIONS+=("$1")
shift
done
shift
while getopts 'c:g:p:q:v:' FLAG; do
case $FLAG in
c) ENABLE_CHAT_MESSAGES=$OPTARG ;;
g) EXIT_IF_NO_SCREEN=true ;;
p) PREFIX=$OPTARG ;;
q) SUPPRESS_WARNINGS=$OPTARG ;;
v) DEBUG=$OPTARG ;;
*) ;;
esac
done
BASE_DIR=$(dirname "$(realpath "$0")")
# shellcheck source=logging.sh
source "$BASE_DIR/logging.sh" \
-q "$SUPPRESS_WARNINGS" \
-v "$DEBUG"
EXECUTE_METHOD_PATH=$EXECUTE_METHOD
BACKUP_METHOD_PATH=$BACKUP_METHOD
assert-all () {
local TEST_CMD=$1
local MESSAGE=$2
shift 2
local ITEMS=("$@")
local RESULT=true
for ITEM in "${ITEMS[@]}"; do
if ! $TEST_CMD "$ITEM"; then
log-fatal "$MESSAGE $ITEM"
RESULT=false
fi
done
if ! $RESULT; then
exit 1
fi
}
assert-files-exist () {
assert-all "test -f" "Script not found:" "$@"
}
assert-files-exist \
"$EXECUTE_METHOD_PATH" \
"$BACKUP_METHOD_PATH"
# shellcheck source=exec-methods/screen.sh
source "$EXECUTE_METHOD_PATH" "${EXECUTE_METHOD_OPTIONS[@]}"
# shellcheck source=backup-methods/tar.sh
source "$BACKUP_METHOD_PATH" "${BACKUP_METHOD_OPTIONS[@]}"
# fn_exists based upon https://stackoverflow.com/q/85880
fn_exists () {
LC_ALL=C type "$1" 2>&1 | grep -q 'function'
}
assert-functions-exist () {
assert-all fn_exists "Function not defined:" "$@"
}
assert-functions-exist \
minecraft-backup-execute \
minecraft-backup-backup \
minecraft-backup-check \
minecraft-backup-epilog
# Minecraft server communication interface functions
execute-command () {
minecraft-backup-execute "$@"
}
message-players () {
message-players-color "gray" "$@"
}
message-players-error () {
message-players-color "red" "$@"
}
message-players-warning () {
message-players-color "yellow" "$@"
}
message-players-success () {
message-players-color "green" "$@"
}
message-players-color () {
local COLOR=$1
local MESSAGE=$2
local HOVER_MESSAGE=$3
log-info "$MESSAGE ($HOVER_MESSAGE)"
if $ENABLE_CHAT_MESSAGES; then
execute_command \
"tellraw @a [\"\",{\"text\":\"[$PREFIX] \",\"color\":\"gray\",\"italic\":true},{\"text\":\"$MESSAGE\",\"color\":\"$COLOR\",\"italic\":true,\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[{\"text\":\"$HOVER_MESSAGE\"}]}}}]"
fi
}
clean-up () {
# Re-enable world autosaving
execute-command "save-on"
# Save the world
execute-command "save-all"
}
trap-ctrl-c () {
log-warning "Backup interrupted. Attempting to re-enable autosaving"
clean-up
exit 2
}
trap "trap-ctrl-c" 2
# Notify players of start
message-players "Starting backup..." "$TIMESTAMP"
# Disable world autosaving
execute-command "save-off"
RESULT=$?
if $EXIT_IF_NO_SCREEN && [[ $RESULT != "0" ]]; then
exit $RESULT
fi
# Record start time for performance reporting
START_TIME=$(date +"%s")
minecraft-backup-backup
END_TIME=$(date +"%s")
clean-up
TIME_DELTA=$((END_TIME - START_TIME))
CHECK_MESSAGE=$(minecraft-backup-check)
CHECK_RESULT=$?
if [[ $CHECK_RESULT != "0" ]]; then
message-players-error "Backup failed!" "Please notify an admin."
exit $CHECK_RESULT
fi
message-players-success "Backup complete!" "$TIME_DELTA s, $CHECK_MESSAGE"
EPILOG_MESSAGE=$(minecraft-backup-epilog)
EPILOG_RESULT=$?
if [[ $EPILOG_RESULT != "0" ]]; then
message-players-warning "Backup epilog failed."
fi
if [[ $EPILOG_MESSAGE != "" ]]; then
message-players "$EPILOG_MESSAGE"
fi

21
src/exec-methods/screen.sh Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Execute commands on a Minecraft server running in a GNU screen
OPTIND=1
while getopts 's:' FLAG "$@"; do
case $FLAG in
s) SCREEN_NAME=$OPTARG ;;
*) ;;
esac
done
minecraft-backup-execute () {
local COMMAND=$1
if ! screen -S "$SCREEN_NAME" -Q "select" .; then
return 1
fi
if [[ "$SCREEN_NAME" != "" ]]; then
screen -S "$SCREEN_NAME" -p 0 -X stuff "$COMMAND$(printf \\r)"
fi
}

46
src/logging.sh Normal file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
SUPPRESS_WARNINGS=false
DEBUG=true
while getopts 'q:v:' FLAG; do
case $FLAG in
q) SUPPRESS_WARNINGS=$OPTARG ;;
v) DEBUG=$OPTARG ;;
*) ;;
esac
done
echo-colored () {
local COLOR_CODE="$1"
shift
local MESSAGE="$*"
if test -t 1 && [ "$(tput colors)" -gt 1 ]; then
# This terminal supports color
echo -ne "\033[${COLOR_CODE}m${MESSAGE}\033[0m"
else
# Output does not support color
echo -n "$MESSAGE"
fi
}
log () {
local COLOR="$1"
local TYPE="$2"
local MESSAGE="$3"
echo-colored "$COLOR" "${TYPE}: "
echo-colored "0" "$MESSAGE"
echo
}
log-fatal () {
>&2 log "0;31" "FATAL" "$*"
}
log-warning () {
if ! $SUPPRESS_WARNINGS; then
>&2 log "0;33" "WARNING" "$*"
fi
}
log-info () {
if $DEBUG; then
log "0;36" "INFO" "$*"
fi
}