550 lines
16 KiB
Bash
Executable File
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
|
|
|
|
# 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
|