#!/usr/bin/env sh # 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