Mac Server Backup Script

If you run a Mac server, you are responsible for keeping backups of the world so that you can restore your world from backup if something goes wrong.

While it’s possible to do this manually, I find it to be a bit of a pain… so of course I wrote a script :slight_smile:

Note: This won’t work on worlds created after 1.6, though it should keep working for worlds created before the release of 1.7.

Setup:

  1. Place the script at the end of this post in a plain text file somewhere. (For example backup.sh on your desktop)
  2. Execute the following commands in Terminal (change if you put the file somewhere else, or named it something else.)
cd Desktop
chmod +x backup.sh

Usage

Execute in terminal with ./backup.sh <command> [options]

Commands

--list, -l 🢂 list worlds, with their world IDs

--backup, -b 🢂 backup a world, prompts for a world ID unless provided with --world

--restore, -r 🢂 restore a world backup, replacing the current save with the world ID

Options

--path <path>, '-p 🢂 Sets the path for backup worlds to be stored at (Default:~/Documents/BH`)

--world <id>, '-w 🢂 Sets the world ID. For–list, this has no affect. For–backupit prevents the prompt asking for a world ID. For–restore` it hides all world backups that are not for this ID.

--yes 🢂 Hide reminder / confirmation to stop a world when restoring

--debug, -d 🢂 Show debugging info

Video Tutorial

I apologize for the audio. I recorded this several times, and this is the best I could do. Not sure what’s wrong with my mic. The video tutorial also shows how to back up worlds manually, and how to set up an Automator script to automatically back up a world daily.

Script

Should work on all Macs with an up to date version of bash. No programs required.

The script (long)
#!/bin/bash

displayHelp () {
    echo 'USAGE:'
    echo
    echo "--list or -l : Display worlds that can be backed up"
    echo "--backup or -b: Backup a world, if no id is specified, you will be prompted to choose from a list of worlds"
    echo "--restore or -r: Reads the backups in the backup dir provided by --path, or in the default backup dir and allows choosing a backup to restore. If --world is used, backups shown will be limited to those with this world ID"
    echo "--path <path> or -p <path> : Sets the backup/restore path to use. By default, backups are stored in ~/Documents/BH. Do not include a trailing slash!"
    echo "--world <id> or -w <id> : Sets the world ID to use to avoid prompts when using --backup and --restore"
    echo "--yes : Prevents any 'are you sure' prompts"
    echo "--debug or -d : Show debugging info"
    echo
    exit
}

debug () {
    echo -e "DEBUG: $1\n"
}


ROOT="/Users/$(whoami)/Library/Containers/com.majicjungle.BlockheadsServer/Data/Library/Application Support/TheBlockheads/saves"
BACKUP_DIR="/Users/$(whoami)/Documents/BH"
COMMAND=""
WORLD_ID=""

# Parse command line switches
while [[ $# -gt 0 ]]
do
    key="$1"

    case $key in
        -h|--help)
        COMMAND='HELP'
        ;;

        -l|--list)
        COMMAND='LIST'
        ;;

        -b|--backup)
        COMMAND='BACKUP'
        ;;

        -r|--restore)
        COMMAND='RESTORE'
        ;;

        -p|--path)
        BACKUP_DIR="$2"
        shift
        ;;

        -w|--world)
        WORLD_ID="$2"
        shift
        ;;

        -d|--debug)
        DEBUG=1
        ;;

        --yes)
        YES=1
        ;;

        *)
        echo "WARNING: Unknown flag: $1"
        ;;
    esac
    shift
done

# Exit if command unknown
test "$COMMAND" == 'HELP' && displayHelp
test "$COMMAND" == '' && test "$DEBUG" && debug 'Command not found.'
test "$COMMAND" == '' && displayHelp

if [[ "$DEBUG" == 1 ]]; then
    echo
    echo 'VARIABLES:'
    echo "COMMAND = $COMMAND"
    echo "BACKUP_DIR = $BACKUP_DIR"
    echo "WORLD_ID = $WORLD_ID"
    echo
fi

# -l, --list
if [[ "$COMMAND" == 'LIST' ]]; then
    mkdir /tmp/bh

    test "$DEBUG" && debug "Looking for worlds in $ROOT"

    echo -e 'WORLD NAME\t(world ID)'

    find "$ROOT" -type d -maxdepth 1 -mindepth 1 | while read save
    do
        id=$(basename "${save}")

        # For whatever reason, defaults refuses to read the plist files in the saves directory.
        # Defaults requires a file with .plist as the extension
        cp "$save/worldv2" "/tmp/bh/$id-worldv2.plist"

        name=$(defaults read "/tmp/bh/$id-worldv2" worldName)

        # Format this more nicely, if char length is less than 8, use two tabs
        if [[ "${#name}" -lt 8 ]]; then
            echo -e "$name\t\t($id)"
        else
            echo -e "$name\t($id)"
        fi
    done

    echo

    # Remove temp directory where we stored worldv2 files
    rm -r /tmp/bh
fi

# -b, --backup
if [[ "$COMMAND" == 'BACKUP' ]]; then
    # Do we have the required world ID? If not, prompt for input
    if [[ "$WORLD_ID" == '' ]]; then
        echo 'Enter a world ID'
        read -p '>' WORLD_ID
    fi

    test "$DEBUG" && debug "Using world ID: $WORLD_ID"

    # Make sure the world exists
    if [[ ! -d "$ROOT/$WORLD_ID" ]]; then
        echo 'Invalid world ID! Exiting.'
        exit 1
    fi

    # World exists, make a backup

    # Make sure the backup dir exists
    mkdir -p "$BACKUP_DIR"

    # Get world name
    mkdir /tmp/bh
    cp "$ROOT/$WORLD_ID/worldv2" "/tmp/bh/worldv2.plist"
    name=$(defaults read '/tmp/bh/worldv2' worldName)
    rm -r /tmp/bh

    # Don't use colons in file names, messes with tar, scp, and other tools
    saveName="${name} $(date "+%Y-%m-%d %H_%M_%S") - ${WORLD_ID}"
    # Redirect stderr to /dev/null to avoid message about striping leading slashes
    tar -zcf "$BACKUP_DIR/$saveName.tar.gz" -C "$ROOT" "$WORLD_ID" 2&> /dev/null

    echo "Backup saved at $BACKUP_DIR/$saveName.tar.gz"
fi

if [[ "$COMMAND" == 'RESTORE' ]]; then
    if [ ! -d "$BACKUP_DIR" ]; then
        echo 'Backup directory does not exist.'
        exit 1
    fi

    # Read backup files

    test "$DEBUG" && test "$WORLD_ID" != '' && debug "Limiting worlds to $WORLD_ID"

    echo -e '\nIndex\tBackup'
    index=0
    files=$(ls "$BACKUP_DIR")

    shownBackups=()

    while read backup
    do
        if [[ "$WORLD_ID" != '' ]]; then
            id=${backup/* - /}
            id=${id/\.tar\.gz/}
            if [[ "$WORLD_ID" == "$id" ]]; then
                shownBackups+=("$backup")
                ((index++))
                echo -e "$index\t$backup"
            fi
        else
            shownBackups+=("$backup")
            ((index++))
            echo -e "$index\t$backup"
        fi
    done <<< "$files"

    echo -e '\nChoose a backup index to restore'
    read -p '> ' BACKUP_INDEX
    if [[ "$BACKUP_INDEX" -gt "$index" ]] || [[ "$BACKUP_INDEX" -lt 1 ]]; then
        echo 'Unable to find backup file.'
        exit 1
    fi

    if [[ "$YES" != 1 ]]; then
        echo
        echo 'Are you sure you want to do this? This will overwrite your current world.'
        echo 'WARNING: If you fail to stop your world in the Blockheads Server App first, this may result in unexpected glitches or world corruption.'
        echo
        echo 'Enter y to continue, anything else to exit.'
        read -p '> ' check
        if [[ "$check" != 'y' ]]; then
            exit
        fi
    fi

    file=${shownBackups[(($BACKUP_INDEX - 1))]}
    id=${file/* - /}
    id=${id/\.tar\.gz/}

    echo
    if [[ -d "$ROOT/$id" ]]; then
        echo 'Removing world'
        rm -r "$ROOT/$id"
    fi

    echo 'Restoring backup'
    tar -xzf "$BACKUP_DIR/$file" -C "$ROOT"

    echo 'Backup restored!'
fi
11 Likes

Nice! ^o^

Can there be a way to stop the server before backing up with this script? I don’t want to back up a server when it’s active.

Stopping individual worlds is out of scope for this script, but you could do something like this:

killall BlockheadsServer && ./backup.sh -w asdfasdfsdfasdf -b && open /Applications/BlockheadsServer.app

I suppose you could use the scripts here to start/stop an individual server, but they are limited in the supported OS X versions (and it seems they are giving you some trouble)