#!/bin/bash # # Minecraft Server Backup Script # Deployed by Ansible - {{ ansible_date_time.iso8601 if ansible_date_time is defined else 'managed' }} # # Creates rolling backups of the Minecraft world and data directories. # Prunes old backups based on retention count. # set -euo pipefail # Configuration DATA_DIR="{{ paths.data_dir }}" BACKUP_DIR="{{ paths.backup_dir }}" RETENTION_COUNT="{{ backup.retention_count | default(10) }}" COMPRESS="{{ backup.compress | default('true') }}" SERVER_NAME="{{ minecraft.server_name }}" # Timestamp for this backup TIMESTAMP="$(date +%Y%m%d_%H%M%S)" # Log function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" } # Safety checks if [ ! -d "$DATA_DIR" ]; then log "ERROR: Data directory $DATA_DIR does not exist" exit 1 fi if [ ! -d "$BACKUP_DIR" ]; then log "Creating backup directory: $BACKUP_DIR" mkdir -p "$BACKUP_DIR" fi # Create backup using tar BACKUP_FILE="${BACKUP_DIR}/${SERVER_NAME}_${TIMESTAMP}" log "Starting backup of $DATA_DIR..." # Use atomic save if RCON is enabled USE_RCON="{{ minecraft.rcon_enabled | default('false') }}" if [ "$USE_RCON" = "true" ]; then log "Using RCON for world save..." # Send save-all command via rcon-cli or telnet # This is a best-effort operation - fall back to tar if it fails if command -v rcon-cli >/dev/null 2>&1; then rcon-cli -a localhost -p {{ minecraft.rcon_port }} -P {{ minecraft.rcon_password }} save-all || true sleep 5 elif command -v mcrcon >/dev/null 2>&1; then mcrcon -H localhost -P {{ minecraft.rcon_port }} -p {{ minecraft.rcon_password }} "save-all" || true sleep 5 else log "RCON client not found, skipping save-all command (backup may be inconsistent)" fi fi # Deterministic backup using tar if [ "$COMPRESS" = "true" ]; then BACKUP_FILE="${BACKUP_FILE}.tar.gz" log "Creating compressed backup: $BACKUP_FILE" tar --warning=no-file-changed -czf "$BACKUP_FILE" \ --exclude="$DATA_DIR/logs/*.gz" \ -C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)" else BACKUP_FILE="${BACKUP_FILE}.tar" log "Creating uncompressed backup: $BACKUP_FILE" tar --warning=no-file-changed -cf "$BACKUP_FILE" \ --exclude="$DATA_DIR/logs/*.gz" \ -C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)" fi log "Backup created successfully: $BACKUP_FILE" # Prune old backups, retaining the most recent N log "Pruning old backups (keeping $RETENTION_COUNT)..." # Count backups (whether .tar or .tar.gz) BACKUP_COUNT=$(ls "${BACKUP_DIR}/"${SERVER_NAME}_*.tar* 2>/dev/null | wc -l) OVERFLOW=$((BACKUP_COUNT - RETENTION_COUNT)) if [ "$OVERFLOW" -gt 0 ]; then # Remove oldest backups ls -t "${BACKUP_DIR}/"${SERVER_NAME}_*.tar* 2>/dev/null | tail -n "$OVERFLOW" | while read -r old_backup; do log "Removing old backup: $old_backup" rm -f "$old_backup" done else log "Backup count ($BACKUP_COUNT) is within retention limit ($RETENTION_COUNT)" fi log "Backup completed at $(date '+%Y-%m-%d %H:%M:%S')" log "Current backups:" ls -lh "${BACKUP_DIR}/" 2>/dev/null || true