Multiple worlds backup

This commit is contained in:
semklauke 2018-12-12 17:50:04 +01:00
parent 851c2a9718
commit 70054ecdc2
2 changed files with 132 additions and 69 deletions

View file

@ -4,12 +4,13 @@ Backup script for Linux servers running a Minecraft server in a GNU Screen
### Still in development, so use at your own risk!
## Features
- Create backups of your world folder
- Create backups of your world folders
- Manage deletion of old backups
- "thin" - keep last 24 hourly, last 30 daily, and use remaining space for monthly backups
- "sequential" - delete oldest backup
- Choose your own compression algorithm (tested with: `gzip`, `xz`, `zstd`)
- Able to print backup status and info to the Minecraft chat
- Can back up as many worlds as you like
## Requirements
- Linux computer (tested on Ubuntu)
@ -20,17 +21,21 @@ Backup script for Linux servers running a Minecraft server in a GNU Screen
1. Download the script: `$ wget https://raw.githubusercontent.com/nicolaschan/minecraft-backup/master/backup.sh`
2. Mark as executable: `$ chmod +x backup.sh`
3. Configure the variables (in the top of the script)
or use the commandline options (see ./backup.sh -h for help)
```bash
SCREEN_NAME="PrivateSurvival" # Name of the GNU Screen your Minecraft server is running in
SERVER_DIRECTORY="/home/server/MinecraftServers/PrivateSurvival" # Server directory, NOT the world; world is SERVER_DIRECTORY/world
BACKUP_DIRECTORY="/media/server/ExternalStorage/Backups/PrivateSurvivalBackups" # Directory to save backups in
SERVER_WORLDS=() # Array for input paths (paths of the worlds to back up)
SERVER_WORLDS[0]="/home/server/MinecraftServers/PrivateSurvival/world" # World you want to back up
BACKUP_DIRECTORYS=() # Array for directory to save backups in (invoke in same order as input directorys with -i)
BACKUP_DIRECTORY[0]="/media/server/ExternalStorage/Backups/PrivateSurvivalBackups" # Directory to save backups in
NUMBER_OF_BACKUPS_TO_KEEP=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
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=true # Tell players in Minecraft chat about backup status
ENABLE_JOINED_BACKUP_MESSAGE=false # Print a combined Backup info messsage after all Backups are finished - if multiple Backups were performed
PREFIX="Backup" # Shows in the chat message
DEBUG=true # Enable debug messages
```
@ -45,6 +50,7 @@ DEBUG=true # Enable debug messages
- Make sure your compression algorithm is in the crontab's PATH
- Make sure cron has permissions for all the files involved and access to the Minecraft server's GNU Screen
- It's surprising how much space backups can take--make sure you have enough empty space
- `SERVER_DIRECTORY` should be the server directory, not the `world` directory
- Do not put trailing `/` in the `SERVER_DIRECTORY` or `BACKUP_DIRECTORY`
- Do not put trailing `/` in the `SERVER_WORLDS` or `BACKUP_DIRECTORYs`
- If "thin" delete method is behaving weirdly, try emptying your backup directory or switch to "sequential"
- you can back up multiple worlds into seperate folders or all into one
- the backup for first specified world folder goes into the first specified backup folder and so on...

159
backup.sh
View file

@ -9,14 +9,15 @@
# Default Configuration
SCREEN_NAME="" # Name of the GNU Screen your Minecraft server is running in
SERVER_WORLD="" # Server world directory
BACKUP_DIRECTORY="" # Directory to save backups in
MAX_BACKUPS=128 # -1 indicates unlimited
SERVER_WORLDS=() # Array for input paths (paths of the worlds to back up)
BACKUP_DIRECTORYS=() # Array for directory to save backups in (invoke in same order as input directorys with -i)
MAX_BACKUPS=128 # Max. backups per world. -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="gzip" # Leave empty for no compression
COMPRESSION_FILE_EXTENSION=".gz" # 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
ENABLE_JOINED_BACKUP_MESSAGE=false # Print a combined Backup info messsage after all Backups are finished - if multiple Backups were performed
PREFIX="Backup" # Shows in the chat message
DEBUG=false # Enable debug messages
SUPPRESS_WARNINGS=false # Suppress warnings
@ -25,7 +26,7 @@ SUPPRESS_WARNINGS=false # Suppress warnings
DATE_FORMAT="%F_%H-%M-%S"
TIMESTAMP=$(date +$DATE_FORMAT)
while getopts 'a:cd:e:f:hi:l:m:o:p:qs:v' FLAG; do
while getopts 'a:cd:e:f:hi:jl:m:o:p:qs:v' FLAG; do
case $FLAG in
a) COMPRESSION_ALGORITHM=$OPTARG ;;
c) ENABLE_CHAT_MESSAGES=true ;;
@ -39,7 +40,8 @@ while getopts 'a:cd:e:f:hi:l:m:o:p:qs:v' FLAG; do
echo "-e Compression file extension, exclude leading \".\" (default: gz)"
echo "-f Output file name (default is the timestamp)"
echo "-h Shows this help text"
echo "-i Input directory (path to world folder)"
echo "-i Input directory (path to world folder) - can be used multiple times"
echo "-j if chat messages is enabled, print an info message after all backups are finished"
echo "-l Compression level (default: 3)"
echo "-m Maximum backups to keep, use -1 for unlimited (default: 128)"
echo "-o Output directory"
@ -49,10 +51,11 @@ while getopts 'a:cd:e:f:hi:l:m:o:p:qs:v' FLAG; do
echo "-v Verbose mode"
exit 0
;;
i) SERVER_WORLD=$OPTARG ;;
i) SERVER_WORLDS+=("$OPTARG") ;;
j) ENABLE_JOINED_BACKUP_MESSAGE=true ;;
l) COMPRESSION_LEVEL=$OPTARG ;;
m) MAX_BACKUPS=$OPTARG ;;
o) BACKUP_DIRECTORY=$OPTARG ;;
o) BACKUP_DIRECTORYS+=("$OPTARG") ;;
p) PREFIX=$OPTARG ;;
q) SUPPRESS_WARNINGS=true ;;
s) SCREEN_NAME=$OPTARG ;;
@ -75,22 +78,22 @@ if ! $SUPPRESS_WARNINGS; then
fi
# Check for required arguments
MISSING_CONFIGURATION=false
if [[ $SERVER_WORLD == "" ]]; then
log-fatal "Server world not specified (use -i)"
if [[ ${#SERVER_WORLDS[@]} -eq 0 ]]; then
log-fatal "No Server world specified (use -i)"
MISSING_CONFIGURATION=true
fi
if [[ $BACKUP_DIRECTORY == "" ]]; then
log-fatal "Backup directory not specified (use -o)"
if [[ ${#BACKUP_DIRECTORYS[@]} -eq 0 ]]; then
log-fatal "No Backup directory specified (use -o)"
MISSING_CONFIGURATION=true
fi
if [[ ${#BACKUP_DIRECTORYS[@]} -ne 1 && ${#BACKUP_DIRECTORYS[@]} -ne ${#SERVER_WORLDS[@]} ]]; then
log-fatal "To many or less Backup directory(s) specified (must be either 1 directory or for each input input path one)"
MISSING_CONFIGURATION=true
fi
if $MISSING_CONFIGURATION; then
exit 0
fi
ARCHIVE_FILE_NAME=$TIMESTAMP.tar$COMPRESSION_FILE_EXTENSION
ARCHIVE_PATH=$BACKUP_DIRECTORY/$ARCHIVE_FILE_NAME
# Minecraft server screen interface functions
message-players () {
local MESSAGE=$1
@ -125,9 +128,6 @@ message-players-color () {
fi
}
# Notify players of start
message-players "Starting backup..." "$ARCHIVE_FILE_NAME"
# Parse file timestamp to one readable by "date"
parse-file-timestamp () {
local DATE_STRING=$(echo $1 | awk -F_ '{gsub(/-/,":",$2); print $1" "$2}')
@ -136,17 +136,20 @@ parse-file-timestamp () {
# Delete a backup
delete-backup () {
local BACKUP=$1
local BACKUP=$2
local BACKUP_DIRECTORY=$1
rm $BACKUP_DIRECTORY/$BACKUP
message-players "Deleted old backup" "$BACKUP"
}
# Sequential delete method
delete-sequentially () {
local BACKUPS=($(ls $BACKUP_DIRECTORY))
local BACKUP_DIRECTORY=$1
local WORLD_NAME=$2
local BACKUPS=($(ls $BACKUP_DIRECTORY | grep $WORLD_NAME))
while [[ $MAX_BACKUPS -ge 0 && ${#BACKUPS[@]} -gt $MAX_BACKUPS ]]; do
delete-backup ${BACKUPS[0]}
BACKUPS=($(ls $BACKUP_DIRECTORY))
delete-backup BACKUP_DIRECTORY ${BACKUPS[0]}
BACKUPS=($(ls $BACKUP_DIRECTORY | grep $WORLD_NAME))
done
}
@ -178,6 +181,8 @@ array-sum () {
# Thinning delete method
delete-thinning () {
local BACKUP_DIRECTORY=$1
local WORLD_NAME=$2
# sub-hourly, hourly, daily, weekly is everything else
local BLOCK_SIZES=(16 24 30)
# First block is unconditional
@ -193,7 +198,7 @@ delete-thinning () {
fi
local CURRENT_INDEX=0
local BACKUPS=($(ls -r $BACKUP_DIRECTORY)) # List newest first
local BACKUPS=($(ls -r $BACKUP_DIRECTORY | grep $WORLD_NAME)) # List newest first
for BLOCK_INDEX in ${!BLOCK_SIZES[@]}; do
local BLOCK_SIZE=${BLOCK_SIZES[BLOCK_INDEX]}
@ -215,19 +220,63 @@ delete-thinning () {
fi
else
# Oldest backup in this block does not satisfy the condition for placement in next block
delete-backup $OLDEST_BACKUP_IN_BLOCK
delete-backup $BACKUP_DIRECTORY $OLDEST_BACKUP_IN_BLOCK
break
fi
((CURRENT_INDEX += BLOCK_SIZE))
done
delete-sequentially
delete-sequentially $BACKUP_DIRECTORY $WORLD_NAME
}
# Delete old backups
delete-old-backups () {
local BACKUP_DIRECTORY=$1
case $DELETE_METHOD in
"sequential") delete-sequentially $BACKUP_DIRECTORY $2
;;
"thin") delete-thinning $BACKUP_DIRECTORY $2
;;
esac
}
# Disable world autosaving
execute-command "save-off"
JOINED_START_TIME=$(date +"%s")
JOINED_WORLD_SIZE_BYTES=0
JOINED_ARCHIVE_SIZE_BYTES=0
JOINED_ARCHIVE_SIZE=0
JOINED_BACKUP_DIRECTORY_SIZE=0
CURRENT_INDEX=0
for SERVER_WORLD in "${SERVER_WORLDS[@]}"
do
WORLD_NAME=$(basename $SERVER_WORLD)
BACKUP_DIRECTORY=""
ARCHIVE_FILE_NAME=""
NOTIFY_ADDITION=""
if [[ ${#BACKUP_DIRECTORYS[@]} -eq 1 ]]; then
BACKUP_DIRECTORY=${BACKUP_DIRECTORYS[0]}
else
BACKUP_DIRECTORY=${BACKUP_DIRECTORYS[${CURRENT_INDEX}]}
fi
if [[ ${#SERVER_WORLDS[@]} -gt 1 ]]; then
ARCHIVE_FILE_NAME=$TIMESTAMP"_"$WORLD_NAME.tar$COMPRESSION_FILE_EXTENSION
NOTIFY_ADDITION=" of ${WORLD_NAME}"
else
ARCHIVE_FILE_NAME=$TIMESTAMP.tar$COMPRESSION_FILE_EXTENSION
fi
# Notify players of start
message-players "Starting backup${NOTIFY_ADDITION}..." "$ARCHIVE_FILE_NAME"
ARCHIVE_PATH=$BACKUP_DIRECTORY/$ARCHIVE_FILE_NAME
# Backup world
START_TIME=$(date +"%s")
case $COMPRESSION_ALGORITHM in
@ -241,34 +290,42 @@ esac
sync
END_TIME=$(date +"%s")
# Notify players of completion
WORLD_SIZE_BYTES=$(du -b --max-depth=0 $SERVER_WORLD | awk '{print $1}')
JOINED_WORLD_SIZE_BYTES=$(($JOINED_WORLD_SIZE_BYTES + $WORLD_SIZE_BYTES))
ARCHIVE_SIZE_BYTES=$(du -b $ARCHIVE_PATH | awk '{print $1}')
JOINED_ARCHIVE_SIZE_BYTES=$(($JOINED_ARCHIVE_SIZE_BYTES + $ARCHIVE_SIZE_BYTES))
COMPRESSION_PERCENT=$(($ARCHIVE_SIZE_BYTES * 100 / $WORLD_SIZE_BYTES))
ARCHIVE_SIZE=$(du -h $ARCHIVE_PATH | awk '{print $1}')
BACKUP_DIRECTORY_SIZE=$(du -h --max-depth=0 $BACKUP_DIRECTORY | awk '{print $1}')
BACKUP_DIRECTORY_SIZE_BYTES=$(du -b --max-depth=0 $BACKUP_DIRECTORY | awk '{print $1}')
JOINED_BACKUP_DIRECTORY_SIZE=$(($JOINED_ARCHIVE_SIZE_BYTES + $BACKUP_DIRECTORY_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${NOTIFY_ADDITION} complete!" "$TIME_DELTA s, $ARCHIVE_SIZE/$BACKUP_DIRECTORY_SIZE, $COMPRESSION_PERCENT%"
delete-old-backups $BACKUP_DIRECTORY $WORLD_NAME
else
message-players-error "Backup${NOTIFY_ADDITION} was not saved!" "Please notify an administrator"
fi
CURRENT_INDEX=$((INDEX_COUTER+1))
done
JOINED_END_TIME=$(date +"%s")
JOINED_COMPRESSION_PERCENT=$(($JOINED_ARCHIVE_SIZE_BYTES * 100 / $JOINED_WORLD_SIZE_BYTES))
JOINED_TIME_DELTA=$(($JOINED_END_TIME - $JOINED_START_TIME))
JOINED_ARCHIVE_SIZE=$((JOINED_ARCHIVE_SIZE_BYTES / 1024 / 1024))
JOINED_BACKUP_DIRECTORY_SIZE=$((JOINED_BACKUP_DIRECTORY_SIZE / 1024 / 1024))
if $ENABLE_JOINED_BACKUP_MESSAGE; then
message-players-color "All Backups completed!" "$JOINED_TIME_DELTA s, $JOINED_ARCHIVE_SIZE M/$JOINED_BACKUP_DIRECTORY_SIZE M, $JOINED_COMPRESSION_PERCENT%" "dark_green"
fi
# Enable world autosaving
execute-command "save-on"
# Save the world
execute-command "save-all"
# Delete old backups
delete-old-backups () {
case $DELETE_METHOD in
"sequential") delete-sequentially
;;
"thin") delete-thinning
;;
esac
}
# Notify players of completion
WORLD_SIZE_BYTES=$(du -b --max-depth=0 $SERVER_WORLD | awk '{print $1}')
ARCHIVE_SIZE_BYTES=$(du -b $ARCHIVE_PATH | awk '{print $1}')
COMPRESSION_PERCENT=$(($ARCHIVE_SIZE_BYTES * 100 / $WORLD_SIZE_BYTES))
ARCHIVE_SIZE=$(du -h $ARCHIVE_PATH | awk '{print $1}')
BACKUP_DIRECTORY_SIZE=$(du -h --max-depth=0 $BACKUP_DIRECTORY | awk '{print $1}')
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%"
delete-old-backups
else
message-players-error "Backup was not saved!" "Please notify an administrator"
fi