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! ### Still in development, so use at your own risk!
## Features ## Features
- Create backups of your world folder - Create backups of your world folders
- Manage deletion of old backups - Manage deletion of old backups
- "thin" - keep last 24 hourly, last 30 daily, and use remaining space for monthly backups - "thin" - keep last 24 hourly, last 30 daily, and use remaining space for monthly backups
- "sequential" - delete oldest backup - "sequential" - delete oldest backup
- Choose your own compression algorithm (tested with: `gzip`, `xz`, `zstd`) - Choose your own compression algorithm (tested with: `gzip`, `xz`, `zstd`)
- Able to print backup status and info to the Minecraft chat - Able to print backup status and info to the Minecraft chat
- Can back up as many worlds as you like
## Requirements ## Requirements
- Linux computer (tested on Ubuntu) - 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` 1. Download the script: `$ wget https://raw.githubusercontent.com/nicolaschan/minecraft-backup/master/backup.sh`
2. Mark as executable: `$ chmod +x backup.sh` 2. Mark as executable: `$ chmod +x backup.sh`
3. Configure the variables (in the top of the script) 3. Configure the variables (in the top of the script)
or use the commandline options (see ./backup.sh -h for help)
```bash ```bash
SCREEN_NAME="PrivateSurvival" # Name of the GNU Screen your Minecraft server is running in 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 SERVER_WORLDS=() # Array for input paths (paths of the worlds to back up)
BACKUP_DIRECTORY="/media/server/ExternalStorage/Backups/PrivateSurvivalBackups" # Directory to save backups in 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 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) 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_ALGORITHM="zstd" # Leave empty for no compression
COMPRESSION_FILE_EXTENSION=".zst" # Leave empty for no compression; Precede with a . (for example: ".gz") COMPRESSION_FILE_EXTENSION=".zst" # Leave empty for no compression; Precede with a . (for example: ".gz")
COMPRESSION_LEVEL=3 # Passed to the compression algorithm COMPRESSION_LEVEL=3 # Passed to the compression algorithm
ENABLE_CHAT_MESSAGES=true # Tell players in Minecraft chat about backup status 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 PREFIX="Backup" # Shows in the chat message
DEBUG=true # Enable debug messages 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 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 - 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 - 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_WORLDS` or `BACKUP_DIRECTORYs`
- Do not put trailing `/` in the `SERVER_DIRECTORY` or `BACKUP_DIRECTORY`
- If "thin" delete method is behaving weirdly, try emptying your backup directory or switch to "sequential" - 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...

183
backup.sh
View file

@ -9,14 +9,15 @@
# Default Configuration # Default Configuration
SCREEN_NAME="" # Name of the GNU Screen your Minecraft server is running in SCREEN_NAME="" # Name of the GNU Screen your Minecraft server is running in
SERVER_WORLD="" # Server world directory SERVER_WORLDS=() # Array for input paths (paths of the worlds to back up)
BACKUP_DIRECTORY="" # Directory to save backups in BACKUP_DIRECTORYS=() # Array for directory to save backups in (invoke in same order as input directorys with -i)
MAX_BACKUPS=128 # -1 indicates unlimited 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) 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_ALGORITHM="gzip" # Leave empty for no compression
COMPRESSION_FILE_EXTENSION=".gz" # Leave empty for no compression; Precede with a . (for example: ".gz") COMPRESSION_FILE_EXTENSION=".gz" # Leave empty for no compression; Precede with a . (for example: ".gz")
COMPRESSION_LEVEL=3 # Passed to the compression algorithm COMPRESSION_LEVEL=3 # Passed to the compression algorithm
ENABLE_CHAT_MESSAGES=false # Tell players in Minecraft chat about backup status 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 PREFIX="Backup" # Shows in the chat message
DEBUG=false # Enable debug messages DEBUG=false # Enable debug messages
SUPPRESS_WARNINGS=false # Suppress warnings SUPPRESS_WARNINGS=false # Suppress warnings
@ -25,7 +26,7 @@ SUPPRESS_WARNINGS=false # Suppress warnings
DATE_FORMAT="%F_%H-%M-%S" DATE_FORMAT="%F_%H-%M-%S"
TIMESTAMP=$(date +$DATE_FORMAT) 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 case $FLAG in
a) COMPRESSION_ALGORITHM=$OPTARG ;; a) COMPRESSION_ALGORITHM=$OPTARG ;;
c) ENABLE_CHAT_MESSAGES=true ;; 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 "-e Compression file extension, exclude leading \".\" (default: gz)"
echo "-f Output file name (default is the timestamp)" echo "-f Output file name (default is the timestamp)"
echo "-h Shows this help text" 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 "-l Compression level (default: 3)"
echo "-m Maximum backups to keep, use -1 for unlimited (default: 128)" echo "-m Maximum backups to keep, use -1 for unlimited (default: 128)"
echo "-o Output directory" 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" echo "-v Verbose mode"
exit 0 exit 0
;; ;;
i) SERVER_WORLD=$OPTARG ;; i) SERVER_WORLDS+=("$OPTARG") ;;
j) ENABLE_JOINED_BACKUP_MESSAGE=true ;;
l) COMPRESSION_LEVEL=$OPTARG ;; l) COMPRESSION_LEVEL=$OPTARG ;;
m) MAX_BACKUPS=$OPTARG ;; m) MAX_BACKUPS=$OPTARG ;;
o) BACKUP_DIRECTORY=$OPTARG ;; o) BACKUP_DIRECTORYS+=("$OPTARG") ;;
p) PREFIX=$OPTARG ;; p) PREFIX=$OPTARG ;;
q) SUPPRESS_WARNINGS=true ;; q) SUPPRESS_WARNINGS=true ;;
s) SCREEN_NAME=$OPTARG ;; s) SCREEN_NAME=$OPTARG ;;
@ -75,22 +78,22 @@ if ! $SUPPRESS_WARNINGS; then
fi fi
# Check for required arguments # Check for required arguments
MISSING_CONFIGURATION=false MISSING_CONFIGURATION=false
if [[ $SERVER_WORLD == "" ]]; then if [[ ${#SERVER_WORLDS[@]} -eq 0 ]]; then
log-fatal "Server world not specified (use -i)" log-fatal "No Server world specified (use -i)"
MISSING_CONFIGURATION=true MISSING_CONFIGURATION=true
fi fi
if [[ $BACKUP_DIRECTORY == "" ]]; then if [[ ${#BACKUP_DIRECTORYS[@]} -eq 0 ]]; then
log-fatal "Backup directory not specified (use -o)" 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 MISSING_CONFIGURATION=true
fi fi
if $MISSING_CONFIGURATION; then if $MISSING_CONFIGURATION; then
exit 0 exit 0
fi fi
ARCHIVE_FILE_NAME=$TIMESTAMP.tar$COMPRESSION_FILE_EXTENSION
ARCHIVE_PATH=$BACKUP_DIRECTORY/$ARCHIVE_FILE_NAME
# Minecraft server screen interface functions # Minecraft server screen interface functions
message-players () { message-players () {
local MESSAGE=$1 local MESSAGE=$1
@ -125,9 +128,6 @@ message-players-color () {
fi fi
} }
# Notify players of start
message-players "Starting backup..." "$ARCHIVE_FILE_NAME"
# Parse file timestamp to one readable by "date" # Parse file timestamp to one readable by "date"
parse-file-timestamp () { parse-file-timestamp () {
local DATE_STRING=$(echo $1 | awk -F_ '{gsub(/-/,":",$2); print $1" "$2}') local DATE_STRING=$(echo $1 | awk -F_ '{gsub(/-/,":",$2); print $1" "$2}')
@ -136,17 +136,20 @@ parse-file-timestamp () {
# Delete a backup # Delete a backup
delete-backup () { delete-backup () {
local BACKUP=$1 local BACKUP=$2
local BACKUP_DIRECTORY=$1
rm $BACKUP_DIRECTORY/$BACKUP rm $BACKUP_DIRECTORY/$BACKUP
message-players "Deleted old backup" "$BACKUP" message-players "Deleted old backup" "$BACKUP"
} }
# Sequential delete method # Sequential delete method
delete-sequentially () { 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 while [[ $MAX_BACKUPS -ge 0 && ${#BACKUPS[@]} -gt $MAX_BACKUPS ]]; do
delete-backup ${BACKUPS[0]} delete-backup BACKUP_DIRECTORY ${BACKUPS[0]}
BACKUPS=($(ls $BACKUP_DIRECTORY)) BACKUPS=($(ls $BACKUP_DIRECTORY | grep $WORLD_NAME))
done done
} }
@ -178,6 +181,8 @@ array-sum () {
# Thinning delete method # Thinning delete method
delete-thinning () { delete-thinning () {
local BACKUP_DIRECTORY=$1
local WORLD_NAME=$2
# sub-hourly, hourly, daily, weekly is everything else # sub-hourly, hourly, daily, weekly is everything else
local BLOCK_SIZES=(16 24 30) local BLOCK_SIZES=(16 24 30)
# First block is unconditional # First block is unconditional
@ -193,7 +198,7 @@ delete-thinning () {
fi fi
local CURRENT_INDEX=0 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 for BLOCK_INDEX in ${!BLOCK_SIZES[@]}; do
local BLOCK_SIZE=${BLOCK_SIZES[BLOCK_INDEX]} local BLOCK_SIZE=${BLOCK_SIZES[BLOCK_INDEX]}
@ -215,60 +220,112 @@ delete-thinning () {
fi fi
else else
# Oldest backup in this block does not satisfy the condition for placement in next block # 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 break
fi fi
((CURRENT_INDEX += BLOCK_SIZE)) ((CURRENT_INDEX += BLOCK_SIZE))
done 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 # Disable world autosaving
execute-command "save-off" execute-command "save-off"
# Backup world JOINED_START_TIME=$(date +"%s")
START_TIME=$(date +"%s") JOINED_WORLD_SIZE_BYTES=0
case $COMPRESSION_ALGORITHM in JOINED_ARCHIVE_SIZE_BYTES=0
"") # No compression JOINED_ARCHIVE_SIZE=0
tar -cf $ARCHIVE_PATH -C $SERVER_WORLD . JOINED_BACKUP_DIRECTORY_SIZE=0
;; CURRENT_INDEX=0
*) # With compression
tar -cf - -C $SERVER_WORLD . | $COMPRESSION_ALGORITHM -cv -$COMPRESSION_LEVEL - > $ARCHIVE_PATH 2>> /dev/null for SERVER_WORLD in "${SERVER_WORLDS[@]}"
;; do
esac
sync WORLD_NAME=$(basename $SERVER_WORLD)
END_TIME=$(date +"%s") 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
"") # 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
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 # Enable world autosaving
execute-command "save-on" execute-command "save-on"
# Save the world # Save the world
execute-command "save-all" 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