Files
minecraft_server/scripts/backup.sh

282 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Adjustable parameters
## The number of backups to keep
BACKUPS_KEEP=24
# Global variables (Do NOT change)
CLEANUP_FABRIC=false
CLEANUP_LUCKPERMS=false
CLEANUP_PAPER=false
main() {
log_info "=== Backup run started at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
log_info "Keeping last $BACKUPS_KEEP backups"
check_root_permissions
init_backup
broadcast_status started
backup_bluemap || cleanup_failure
backup_fabric || cleanup_failure
backup_luckperms || cleanup_failure
backup_paper || cleanup_failure
backup_schematics || cleanup_failure
backup_velocity || cleanup_failure
finalize_backup || cleanup_failure
prune_backups
cleanup_success
broadcast_status finished
log_info "=== Backup run completed successfully at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
}
check_root_permissions() {
if [ "$EUID" -ne 0 ]; then
log_error "This script must be run by the root user"
exit 1
fi
log_info "Running as root (EUID=$EUID)"
}
init_backup() {
BACKUP_ID="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
BACKUP_DIR="/backups/$BACKUP_ID"
log_info "Creating backup directory: $BACKUP_DIR"
docker compose run --rm init sh -c '
mkdir -p "$1" &&
chown minecraft_server:minecraft_server "$1"
' -- "$BACKUP_DIR" || {
log_error "Failed to create backup directory $BACKUP_DIR"
exit 1
}
log_info "Backup directory created and ownership set"
}
broadcast_status() {
STATUS="$1"
log_info "Broadcasting backup status: $STATUS"
if docker compose exec -T fabric true > /dev/null 2>&1; then
docker compose exec -T fabric rcon-cli "tellraw @a [{\"text\":\"Server\",\"color\":\"light_purple\"},{\"text\":\": Backup $STATUS.\",\"color\":\"white\"}]" > /dev/null 2>&1
fi
if docker compose exec -T paper true > /dev/null 2>&1; then
docker compose exec -T paper rcon-cli "tellraw @a [{\"text\":\"Server\",\"color\":\"light_purple\"},{\"text\":\": Backup $STATUS.\",\"color\":\"white\"}]" > /dev/null 2>&1
fi
}
backup_bluemap() {
log_info "Starting Bluemap backup..."
docker compose run --rm init sh -c '
cp -a --reflink=auto /bluemap_data "$1" &&
cp -a --reflink=auto /bluemap_maps "$1" &&
cp -a --reflink=auto /bluemap_web "$1"
' -- "$BACKUP_DIR" || return 1
log_info "Finished Bluemap backup"
}
backup_fabric() {
log_info "Starting Fabric backup..."
CLEANUP_FABRIC=false
if docker compose exec -T fabric true > /dev/null 2>&1; then
CLEANUP_FABRIC=true
log_info "Fabric server detected, disabling saves"
docker compose exec -T fabric rcon-cli save-off > /dev/null 2>&1
docker compose exec -T fabric rcon-cli save-all > /dev/null 2>&1
fi
docker compose run --rm init sh -c '
mkdir -p "$1"/fabric_data &&
cp -a --reflink=auto /fabric_data/g4mespeed "$1"/fabric_data &&
cp -a --reflink=auto /fabric_data/fabric-essentials.json "$1"/fabric_data &&
cp -a --reflink=auto /fabric_data/world "$1"/fabric_data
' -- "$BACKUP_DIR" || return 1
log_info "Finished Fabric backup"
}
backup_luckperms() {
log_info "Starting LuckPerms backup..."
CLEANUP_LUCKPERMS=false
if ! docker compose exec -T luckperms true > /dev/null 2>&1; then
CLEANUP_LUCKPERMS=true
log_info "LuckPerms not running, starting temporary container"
docker compose up --wait luckperms > /dev/null 2>&1
fi
docker compose exec luckperms sh -c '
pg_dump -U luckperms luckperms > "$1"/luckperms.sql
' -- "$BACKUP_DIR" || return 1
log_info "Finished LuckPerms backup"
}
backup_paper() {
log_info "Starting Paper backup..."
CLEANUP_PAPER=false
if docker compose exec -T paper true > /dev/null 2>&1; then
CLEANUP_PAPER=true
log_info "Paper server detected, disabling saves"
docker compose exec -T paper rcon-cli save-off > /dev/null 2>&1
docker compose exec -T paper rcon-cli save-all > /dev/null 2>&1
fi
docker compose run --rm init sh -c '
mkdir -p "$1"/paper_data/plugins/PlotSquared &&
cp -a --reflink=auto /paper_data/plugins/Multiverse-Inventories "$1"/paper_data/plugins &&
cp -a --reflink=auto /paper_data/plugins/Essentials "$1"/paper_data/plugins &&
cp -a --reflink=auto /paper_data/plugins/PlotSquared/backups "$1"/paper_data/plugins/PlotSquared &&
cp -a --reflink=auto /paper_data/creative "$1"/paper_data &&
cp -a --reflink=auto /paper_data/creative_nether "$1"/paper_data &&
cp -a --reflink=auto /paper_data/survival "$1"/paper_data &&
cp -a --reflink=auto /paper_data/survival_nether "$1"/paper_data &&
cp -a --reflink=auto /paper_data/survival_the_end "$1"/paper_data
' -- "$BACKUP_DIR" || return 1
docker compose run --rm sqlite_helper \
/paper_data/plugins/PlotSquared/storage.db \
".backup $BACKUP_DIR/paper_data/plugins/PlotSquared/storage.db" || return 1
docker compose run --rm sqlite_helper \
/paper_data/plugins/PlotSquared/user_cache.db \
".backup $BACKUP_DIR/paper_data/plugins/PlotSquared/user_cache.db" || return 1
log_info "Finished Paper backup"
}
backup_schematics() {
log_info "Starting schematics backup..."
docker compose run --rm init sh -c '
cp -a --reflink=auto /schematics "$1"
' -- "$BACKUP_DIR" || return 1
log_info "Finished schematics backup"
}
backup_velocity() {
log_info "Starting Velocity backup..."
docker compose run --rm init sh -c 'mkdir -p "$1"/velocity_data/plugins/dclink-velocity' -- "$BACKUP_DIR" || return 1
docker compose run --rm sqlite_helper \
/velocity_data/plugins/dclink-velocity/dclink.db \
".backup $BACKUP_DIR/velocity_data/plugins/dclink-velocity/dclink.db" || return 1
log_info "Finished Velocity backup"
}
finalize_backup() {
log_info "Finalizing backup $BACKUP_ID"
docker compose run --rm init chown -R minecraft_server:minecraft_server "$BACKUP_DIR" || {
log_error "Failed to update ownership of backup files"
return 1
}
log_info "Backup ownership updated"
docker compose run --rm init sh -c '
ln -sfn "$1" /backups/.latest_tmp &&
mv -Tf /backups/.latest_tmp /backups/latest
' -- "$BACKUP_DIR" || {
log_error "Failed to update /backups/latest symlink"
return 1
}
log_info "Updated /backups/latest symlink"
}
prune_backups() {
BACKUPS_ALL=$(docker compose run --rm init sh -c '
find /backups -mindepth 1 -maxdepth 1 -type d \
-name "????-??-??T??-??-??Z" | sort
')
BACKUPS_TOTAL=$(echo "$BACKUPS_ALL" | wc -l)
BACKUPS_PRUNE=$((BACKUPS_TOTAL - BACKUPS_KEEP))
[ "$BACKUPS_PRUNE" -le 0 ] && BACKUPS_PRUNE=0
log_info "Total backups found: $BACKUPS_TOTAL"
log_info "Backups to prune: $BACKUPS_PRUNE"
echo "$BACKUPS_ALL" | head -n "$BACKUPS_PRUNE" | while read -r OLD_BACKUP_DIR; do
log_info "Removing old backup: $OLD_BACKUP_DIR"
docker compose run --rm init rm -rf "$OLD_BACKUP_DIR"
done
}
trap cleanup_trap HUP INT QUIT ABRT TERM
cleanup_trap() {
log_info "Backup cancelled by signal"
broadcast_status cancelled
cleanup
delete_backup
exit 0
}
cleanup_failure() {
log_info "Running cleanup due to failure"
broadcast_status failed
cleanup
delete_backup
exit 1
}
cleanup_success() {
log_info "Running cleanup"
cleanup
}
cleanup() {
# Ignore specified conditions to ensure cleanup runs to completion uninterrupted
trap '' HUP INT QUIT ABRT TERM
if [ "$CLEANUP_FABRIC" = true ]; then
log_info "Re-enabling Fabric saves"
docker compose exec -T fabric rcon-cli save-on > /dev/null 2>&1
fi
if [ "$CLEANUP_LUCKPERMS" = true ]; then
log_info "Bringing down temporary LuckPerms container"
docker compose stop luckperms > /dev/null 2>&1
fi
if [ "$CLEANUP_PAPER" = true ]; then
log_info "Re-enabling Paper saves"
docker compose exec -T paper rcon-cli save-on > /dev/null 2>&1
fi
}
delete_backup() {
if ! docker compose run --rm init rm -rf "$BACKUP_DIR"; then
log_error "Failed to remove $BACKUP_DIR"
fi
}
log_info() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] INFO: $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] ERROR: $*" >&2
}
main