Files
minecraft_server/scripts/restore.sh
2026-01-26 19:07:27 +00:00

550 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
SCRIPT_NAME=$(basename "$0")
log_fatal() {
echo "$SCRIPT_NAME: $*" >&2
exit 1
}
usage() {
cat<<EOF
Usage:
$SCRIPT_NAME list
$SCRIPT_NAME restore [options] <BACKUP_ID>
$SCRIPT_NAME help
Commands:
list List available backups
restore <backup-id> Restore a backup
help Show this usage message
Restore options:
--all
--bluemap
--fabric
--fabric-essentials
--fabric-g4mespeed
--fabric-world
--luckperms
--paper
--paper-multiverse-inventories
--paper-essentials
--paper-plot-squared
--paper-worlds
--paper-creative
--paper-creative-overworld
--paper-creative-nether
--paper-survival
--paper-survival-overworld
--paper-survival-nether
--paper-survival-the-end
--schematics
--velocity
--velocity-dclink
Examples:
$SCRIPT_NAME list
$SCRIPT_NAME restore --fabric 2026-01-20T07-00-49Z
$SCRIPT_NAME restore --all latest
EOF
}
cmd_list() {
docker compose run --rm init ls -1 /backups || log_fatal "failed to list backups"
}
cmd_restore() {
# Cleanup Flags
CLEANUP_VOLUMES=0
# Default flags
## Bluemap
RESTORE_BLUEMAP=0
## Fabric server
RESTORE_FABRIC_ESSENTIALS=0
RESTORE_FABRIC_G4MESPEED=0
RESTORE_FABRIC_WORLD=0
## LuckPerms
RESTORE_LUCKPERMS=0
## Paper server
RESTORE_PAPER_MULTIVERSE_INVENTORIES=0
RESTORE_PAPER_ESSENTIALS=0
RESTORE_PAPER_PLOT_SQUARED=0
RESTORE_PAPER_CREATIVE_OVERWORLD=0
RESTORE_PAPER_CREATIVE_NETHER=0
RESTORE_PAPER_SURVIVAL_OVERWORLD=0
RESTORE_PAPER_SURVIVAL_NETHER=0
RESTORE_PAPER_SURVIVAL_THE_END=0
## Schematics
RESTORE_SCHEMATICS=0
## Velocity proxy
RESTORE_VELOCITY_DCLINK=0
# Argument parsing
POS_ARGS=""
while [ $# -gt 0 ]; do
case "$1" in
--all)
RESTORE_BLUEMAP=1
RESTORE_FABRIC_ESSENTIALS=1
RESTORE_FABRIC_G4MESPEED=1
RESTORE_FABRIC_WORLD=1
RESTORE_LUCKPERMS=1
RESTORE_PAPER_MULTIVERSE_INVENTORIES=1
RESTORE_PAPER_ESSENTIALS=1
RESTORE_PAPER_PLOT_SQUARED=1
RESTORE_PAPER_CREATIVE_OVERWORLD=1
RESTORE_PAPER_CREATIVE_NETHER=1
RESTORE_PAPER_SURVIVAL_OVERWORLD=1
RESTORE_PAPER_SURVIVAL_NETHER=1
RESTORE_PAPER_SURVIVAL_THE_END=1
RESTORE_SCHEMATICS=1
RESTORE_VELOCITY_DCLINK=1
;;
--bluemap)
RESTORE_BLUEMAP=1
;;
--fabric)
RESTORE_FABRIC_ESSENTIALS=1
RESTORE_FABRIC_G4MESPEED=1
RESTORE_FABRIC_WORLD=1
;;
--fabric-essentials)
RESTORE_FABRIC_ESSENTIALS=1
;;
--fabric-g4mespeed)
RESTORE_FABRIC_G4MESPEED=1
;;
--fabric-world)
RESTORE_FABRIC_WORLD=1
;;
--luckperms)
RESTORE_LUCKPERMS=1
;;
--paper)
RESTORE_PAPER_MULTIVERSE_INVENTORIES=1
RESTORE_PAPER_ESSENTIALS=1
RESTORE_PAPER_PLOT_SQUARED=1
RESTORE_PAPER_CREATIVE_OVERWORLD=1
RESTORE_PAPER_CREATIVE_NETHER=1
RESTORE_PAPER_SURVIVAL_OVERWORLD=1
RESTORE_PAPER_SURVIVAL_NETHER=1
RESTORE_PAPER_SURVIVAL_THE_END=1
;;
--paper-multiverse-inventories)
RESTORE_PAPER_MULTIVERSE_INVENTORIES=1
;;
--paper-essentials)
RESTORE_PAPER_ESSENTIALS=1
;;
--paper-plot-squared)
RESTORE_PAPER_PLOT_SQUARED=1
;;
--paper-worlds)
RESTORE_PAPER_CREATIVE_OVERWORLD=1
RESTORE_PAPER_CREATIVE_NETHER=1
RESTORE_PAPER_SURVIVAL_OVERWORLD=1
RESTORE_PAPER_SURVIVAL_NETHER=1
RESTORE_PAPER_SURVIVAL_THE_END=1
;;
--paper-creative)
RESTORE_PAPER_CREATIVE_OVERWORLD=1
RESTORE_PAPER_CREATIVE_NETHER=1
;;
--paper-creative-overworld)
RESTORE_PAPER_CREATIVE_OVERWORLD=1
;;
--paper-creative-nether)
RESTORE_PAPER_CREATIVE_NETHER=1
;;
--paper-survival)
RESTORE_PAPER_SURVIVAL_OVERWORLD=1
RESTORE_PAPER_SURVIVAL_NETHER=1
RESTORE_PAPER_SURVIVAL_THE_END=1
;;
--paper-survival-overworld)
RESTORE_PAPER_SURVIVAL_OVERWORLD=1
;;
--paper-survival-nether)
RESTORE_PAPER_SURVIVAL_NETHER=1
;;
--paper-survival-the-end)
RESTORE_PAPER_SURVIVAL_THE_END=1
;;
--schematics)
RESTORE_SCHEMATICS=1
;;
--velocity)
RESTORE_VELOCITY_DCLINK=1
;;
--velocity-dclink)
RESTORE_VELOCITY_DCLINK=1
;;
--help)
usage
exit 0
;;
*)
POS_ARGS="$POS_ARGS $1"
;;
esac
shift
done
# Restore positional arguments
set -- $POS_ARGS
# Validate and resolve backup id
[ $# -ge 1 ] || log_fatal "missing backup id"
[ $# -le 1 ] || log_fatal "unexpected argument: $2"
BACKUP_ID="$1"
BACKUP_DIR=$(docker compose run --rm init sh -c '
TARGET=/backups/"$1"
if [ ! -e "$TARGET" ]; then
exit 1
fi
# Resolve symlinks to real paths
REALPATH=$(readlink -f "$TARGET")
echo "$REALPATH"
' -- "$BACKUP_ID") || log_fatal "backup '$BACKUP_ID' not found"
# Update BACKUP_ID to the actual directory name
BACKUP_ID=$(basename "$BACKUP_DIR")
# Validate options
if [ "$RESTORE_BLUEMAP" -eq 0 ] \
&& [ "$RESTORE_FABRIC_ESSENTIALS" -eq 0 ] \
&& [ "$RESTORE_FABRIC_G4MESPEED" -eq 0 ] \
&& [ "$RESTORE_FABRIC_WORLD" -eq 0 ] \
&& [ "$RESTORE_LUCKPERMS" -eq 0 ] \
&& [ "$RESTORE_PAPER_MULTIVERSE_INVENTORIES" -eq 0 ] \
&& [ "$RESTORE_PAPER_ESSENTIALS" -eq 0 ] \
&& [ "$RESTORE_PAPER_PLOT_SQUARED" -eq 0 ] \
&& [ "$RESTORE_PAPER_CREATIVE_OVERWORLD" -eq 0 ] \
&& [ "$RESTORE_PAPER_CREATIVE_NETHER" -eq 0 ] \
&& [ "$RESTORE_PAPER_SURVIVAL_OVERWORLD" -eq 0 ] \
&& [ "$RESTORE_PAPER_SURVIVAL_NETHER" -eq 0 ] \
&& [ "$RESTORE_PAPER_SURVIVAL_THE_END" -eq 0 ] \
&& [ "$RESTORE_SCHEMATICS" -eq 0 ] \
&& [ "$RESTORE_VELOCITY_DCLINK" -eq 0 ]; then
log_fatal "no restore options specified"
fi
echo "=== Restore started at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
echo "Restoring from backup: $BACKUP_DIR"
# Ensure stack is not running
docker compose down > /dev/null 2>&1
# Create snapshot to revert to on failure
trap restore_cleanup_trap HUP INT QUIT ABRT TERM
create_snapshot || restore_cleanup_failure
restore_bluemap || restore_cleanup_failure
restore_fabric || restore_cleanup_failure
restore_luckperms || restore_cleanup_failure
restore_paper || restore_cleanup_failure
restore_schematics || restore_cleanup_failure
restore_velocity || restore_cleanup_failure
restore_cleanup_success
echo "=== Restore completed successfully at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
}
create_snapshot() {
echo "Creating snapshot..."
# Map: source volume name -> cloned volume name
# Source names must match the mount points inside init
declare -Ag VOLUMES=(
[bluemap_data]="bluemap_data_$BACKUP_ID"
[bluemap_maps]="bluemap_maps_$BACKUP_ID"
[bluemap_web]="bluemap_web_$BACKUP_ID"
[fabric_data]="fabric_data_$BACKUP_ID"
[luckperms_data]="luckperms_data_$BACKUP_ID"
[paper_data]="paper_data_$BACKUP_ID"
[schematics]="schematics_$BACKUP_ID"
[velocity_data]="velocity_data_$BACKUP_ID"
)
MOUNTS=()
for VOLUME in "${!VOLUMES[@]}"; do
MOUNTS+=(-v "${VOLUMES[$VOLUME]}:/backup_$VOLUME")
done
# Volumes have started being created
# Cleanup should delete volumes
CLEANUP_VOLUMES=1
for VOLUME in "${VOLUMES[@]}"; do
docker volume create "$VOLUME" > /dev/null || return 1
done
docker compose run --rm \
"${MOUNTS[@]}" \
init sh -ec '
for DIR in \
bluemap_data \
bluemap_maps \
bluemap_web \
fabric_data \
luckperms_data \
paper_data \
schematics \
velocity_data
do
cp -a --reflink=auto "/$DIR/." "/backup_$DIR"
done
' || return 1
# Volumes have been successfully cloned
# Cleanup should restore contents to originals, then delete clones
CLEANUP_VOLUMES=2
echo "Created snapshot successfully"
}
restore_bluemap() {
if [ "$RESTORE_BLUEMAP" -eq "1" ]; then
echo "Restoring BlueMap data..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/bluemap_data/. /bluemap_data &&
cp -a --reflink=auto "$1"/bluemap_maps/. /bluemap_maps &&
cp -a --reflink=auto "$1"/bluemap_web/. /bluemap_web
' -- "$BACKUP_DIR" || return 1
fi
}
restore_fabric() {
if [ "$RESTORE_FABRIC_ESSENTIALS" -eq 1 ]; then
echo "Restoring Fabric server's Essentials mod data..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/fabric_data/fabric-essentials.json /fabric_data
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_FABRIC_G4MESPEED" -eq 1 ]; then
echo "Restoring Fabric server's G4mespeed mod data..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/fabric_data/g4mespeed /fabric_data
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_FABRIC_WORLD" -eq 1 ]; then
echo "Restoring Fabric server's world..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/fabric_data/world /fabric_data
' -- "$BACKUP_DIR" || return 1
fi
}
restore_luckperms() {
if [ "$RESTORE_LUCKPERMS" -eq 1 ]; then
echo "Restoring LuckPerms database..."
docker compose up --wait luckperms > /dev/null 2>&1
docker compose exec -T luckperms sh -c '
psql -U luckperms postgres -c "DROP DATABASE IF EXISTS luckperms;" > /dev/null 2>&1 &&
psql -U luckperms postgres -c "CREATE DATABASE luckperms OWNER luckperms;" > /dev/null 2>&1 &&
psql -U luckperms luckperms < "$1"/luckperms.sql > /dev/null 2>&1
' -- "$BACKUP_DIR" || return 1
fi
}
restore_paper() {
if [ "$RESTORE_PAPER_MULTIVERSE_INVENTORIES" -eq 1 ]; then
echo "Restoring Paper server's Multiverse-Inventories plugin data..."
docker compose run --rm init sh -c '
mkdir -p /paper_data/plugins &&
chown minecraft_server:minecraft_server /paper_data/plugins &&
cp -a --reflink=auto "$1"/paper_data/plugins/Multiverse-Inventories /paper_data/plugins
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_PAPER_ESSENTIALS" -eq 1 ]; then
echo "Restoring Paper server's Essentials plugin data..."
docker compose run --rm init sh -c '
mkdir -p /paper_data/plugins &&
chown minecraft_server:minecraft_server /paper_data/plugins &&
cp -a --reflink=auto "$1"/paper_data/plugins/Essentials /paper_data/plugins
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_PAPER_PLOT_SQUARED" -eq 1 ]; then
echo "Restoring Paper server's PlotSquared plugin data..."
docker compose run --rm init sh -c '
mkdir -p /paper_data/plugins &&
chown minecraft_server:minecraft_server /paper_data/plugins &&
cp -a --reflink=auto "$1"/paper_data/plugins/PlotSquared /paper_data/plugins
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_PAPER_CREATIVE_OVERWORLD" -eq 1 ]; then
echo "Restoring Paper server's creative overworld..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/paper_data/creative /paper_data
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_PAPER_CREATIVE_NETHER" -eq 1 ]; then
echo "Restoring Paper server's creative nether..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/paper_data/creative_nether /paper_data
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_PAPER_SURVIVAL_OVERWORLD" -eq 1 ]; then
echo "Restoring Paper server's survival overworld..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/paper_data/survival /paper_data
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_PAPER_SURVIVAL_NETHER" -eq 1 ]; then
echo "Restoring Paper server's survival nether..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/paper_data/survival_nether /paper_data
' -- "$BACKUP_DIR" || return 1
fi
if [ "$RESTORE_PAPER_SURVIVAL_THE_END" -eq 1 ]; then
echo "Restoring Paper server's survival the end..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/paper_data/survival_the_end /paper_data
' -- "$BACKUP_DIR" || return 1
fi
}
restore_schematics() {
if [ "$RESTORE_SCHEMATICS" -eq 1 ]; then
echo "Restoring WorldEdit schematics..."
docker compose run --rm init sh -c '
cp -a --reflink=auto "$1"/schematics/. /schematics
' -- "$BACKUP_DIR" || return 1
fi
}
restore_velocity() {
if [ "$RESTORE_VELOCITY_DCLINK" -eq 1 ]; then
echo "Restoring Velocity proxy's dclink database..."
docker compose run --rm init sh -c '
mkdir -p /velocity_data/plugins/dclink-velocity &&
chown minecraft_server:minecraft_server /velocity_data/plugins &&
chown minecraft_server:minecraft_server /velocity_data/plugins/dclink-velocity &&
cp -a --reflink=auto "$1"/velocity_data/plugins/dclink-velocity/dclink.db /velocity_data/plugins/dclink-velocity
' -- "$BACKUP_DIR" || return 1
fi
}
restore_cleanup_success() {
CLEANUP_VOLUMES=1
restore_cleanup
}
restore_cleanup_trap() {
restore_cleanup
echo "=== Restore cancelled at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
exit 0
}
restore_cleanup_failure() {
restore_cleanup
echo "=== Restore completed unsuccessfully at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
exit 1
}
restore_cleanup() {
# Ignore specified conditions to ensure cleanup runs to completion uninterrupted
trap '' HUP INT QUIT ABRT TERM
echo "Running cleanup..."
docker compose down > /dev/null 2>&1
if [ "$CLEANUP_VOLUMES" -ge "2" ]; then
echo "Reverting to snapshot..."
docker compose run --rm \
"${MOUNTS[@]}" \
init sh -ec '
for DIR in \
bluemap_data \
bluemap_maps \
bluemap_web \
fabric_data \
luckperms_data \
paper_data \
schematics \
velocity_data
do
find "${DIR:?}" -mindepth 1 -delete &&
cp -a --reflink=auto "/backup_$DIR/." "/$DIR"
done
' || return 1
echo "Reverted to snapshot successfully"
fi
if [ "$CLEANUP_VOLUMES" -ge "1" ]; then
echo "Removing snapshot..."
for VOLUME in "${VOLUMES[@]}"; do
docker volume rm "$VOLUME" > /dev/null 2>&1
done
echo "Removed snapshot successfully"
fi
echo "Finished cleanup successfully"
}
if [ "$EUID" -ne 0 ]; then
log_fatal "This command must be run by the root user"
fi
[ $# -ge 1 ] || {
usage
log_fatal "expected argument"
}
COMMAND=$1
shift
case "$COMMAND" in
list)
cmd_list "$@"
;;
restore)
cmd_restore "$@"
;;
help)
usage
;;
*)
log_fatal "unknown command: $COMMAND"
;;
esac