diff --git a/README.md b/README.md index f08f143..6bfe54a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Command line options: -e Compression file extension, exclude leading "." (default: gz) -f Output file name (default is the timestamp) -h Shows this help text --i Input directory (path to world folder) +-i Input directory (path to world folder, use -i once for each world) -l Compression level (default: 3) -m Maximum backups to keep, use -1 for unlimited (default: 128) -o Output directory @@ -87,7 +87,7 @@ cd restored-world tar -xzvf /path/to/backups/2019-04-09_02-15-01.tar.gz ``` -Then you can move your restored world (`restored-world` in this case) to your Minecraft server folder and rename it (usually called `world`) so the Minecraft server uses it. +The restored worlds should be inside the `restored-world` directory, possibly nested under the parent directories. Then you can move your restored world to your Minecraft server folder under the proper name and path so the Minecraft server uses it. ### With `restic` Use [`restic restore`](https://restic.readthedocs.io/en/latest/050_restore.html) to restore from backup. diff --git a/backup.sh b/backup.sh index 33c77ba..5778e51 100755 --- a/backup.sh +++ b/backup.sh @@ -9,7 +9,7 @@ # Default Configuration SCREEN_NAME="" # Name of the GNU Screen, tmux session, or hostname:port:password for RCON -SERVER_WORLD="" # Server world directory +SERVER_WORLDS=() # Server world directory BACKUP_DIRECTORY="" # Directory to save backups in MAX_BACKUPS=128 # -1 indicates unlimited DELETE_METHOD="thin" # Choices: thin, sequential, none; sequential: delete oldest; thin: keep last 24 hourly, last 30 daily, and monthly (use with 1 hr cron interval) @@ -40,7 +40,7 @@ debug-log () { fi } -while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:t:u:vw:' FLAG; do +while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:t:u:vw:x' FLAG; do case $FLAG in a) COMPRESSION_ALGORITHM=$OPTARG ;; c) ENABLE_CHAT_MESSAGES=true ;; @@ -55,7 +55,7 @@ while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:t:u:vw:' FLAG; do echo "-e Compression file extension, exclude leading \".\" (default: gz)" echo "-f Output file name (default is the timestamp)" echo "-h Shows this help text" - echo "-i Input directory (path to world folder)" + echo "-i Input directory (path to world folder, use -i once for each world)" echo "-l Compression level (default: 3)" echo "-m Maximum backups to keep, use -1 for unlimited (default: 128)" echo "-o Output directory" @@ -67,9 +67,10 @@ while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:t:u:vw:' FLAG; do echo "-u Lock file timeout seconds (empty = unlimited)" echo "-v Verbose mode" echo "-w Window manager: screen (default), tmux, RCON" + echo "-x Bukkit-style server backup mode (world files are split by dimension)" exit 0 ;; - i) SERVER_WORLD=$OPTARG ;; + i) SERVER_WORLDS+=("$OPTARG") ;; l) COMPRESSION_LEVEL=$OPTARG ;; m) MAX_BACKUPS=$OPTARG ;; o) BACKUP_DIRECTORY=$OPTARG ;; @@ -216,7 +217,7 @@ if ! $SUPPRESS_WARNINGS; then fi # Check for required arguments MISSING_CONFIGURATION=false -if [[ "$SERVER_WORLD" == "" ]]; then +if [[ "${#SERVER_WORLDS[@]}" == "0" ]]; then log-fatal "Server world not specified (use -i)" MISSING_CONFIGURATION=true fi @@ -442,7 +443,7 @@ clean-up () { TIME_DELTA=$((END_TIME - START_TIME)) if [[ "$BACKUP_DIRECTORY" != "" ]]; then - WORLD_SIZE_BYTES=$(du -b --max-depth=0 "$SERVER_WORLD" | awk '{print $1}') + WORLD_SIZE_BYTES=$(du --bytes --total --max-depth=0 "${SERVER_WORLDS[@]}" | tail -n 1 | awk '{print $1}') ARCHIVE_SIZE_BYTES=$(du -b "$ARCHIVE_PATH" | awk '{print $1}') ARCHIVE_SIZE=$(du -h "$ARCHIVE_PATH" | awk '{print $1}') BACKUP_DIRECTORY_SIZE=$(du -h --max-depth=0 "$BACKUP_DIRECTORY" | awk '{print $1}') @@ -491,10 +492,10 @@ do-backup () { case $COMPRESSION_ALGORITHM in # No compression - "") tar -cf "$ARCHIVE_PATH" -C "$SERVER_WORLD" . + "") tar -cf "$ARCHIVE_PATH" "${SERVER_WORLDS[@]}" ;; # With compression - *) tar -cf - -C "$SERVER_WORLD" . | $COMPRESSION_ALGORITHM -cv -"$COMPRESSION_LEVEL" - > "$ARCHIVE_PATH" 2>> /dev/null + *) tar -cf - "${SERVER_WORLDS[@]}" | $COMPRESSION_ALGORITHM -cv -"$COMPRESSION_LEVEL" - > "$ARCHIVE_PATH" 2>> /dev/null ;; esac EXIT_CODES=("${PIPESTATUS[@]}") @@ -516,7 +517,7 @@ do-backup () { if [[ "$RESTIC_REPO" != "" ]]; then RESTIC_TIMESTAMP="${TIMESTAMP:0:10} ${TIMESTAMP:11:2}:${TIMESTAMP:14:2}:${TIMESTAMP:17:2}" - restic backup -r "$RESTIC_REPO" "$SERVER_WORLD" --time "$RESTIC_TIMESTAMP" "$QUIET" + restic backup -r "$RESTIC_REPO" "${SERVER_WORLDS[@]}" --time "$RESTIC_TIMESTAMP" "$QUIET" ARCHIVE_EXIT_CODE=$? if [ "$ARCHIVE_EXIT_CODE" -eq 3 ]; then log-warning "Incomplete snapshot taken (some files could not be read)" diff --git a/test/test.sh b/test/test.sh index 6bbaf20..9fb69dc 100755 --- a/test/test.sh +++ b/test/test.sh @@ -62,7 +62,7 @@ check-backup-full-paths () { WORLD_DIR="$2" mkdir -p "$TEST_TMP/restored" tar --extract --file "$BACKUP_ARCHIVE" --directory "$TEST_TMP/restored" - assert-equals-directory "$WORLD_DIR" "$TEST_TMP/restored" + assert-equals-directory "$WORLD_DIR" "$TEST_TMP/restored/$WORLD_DIR" rm -rf "$TEST_TMP/restored" } @@ -80,6 +80,46 @@ check-latest-backup-restic () { # Tests +test-backup-defaults () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + check-backup "$TIMESTAMP.tar.gz" +} + +test-backup-multiple-worlds () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + cp -r "$TEST_TMP/server/world" "$TEST_TMP/server/world_nether" + cp -r "$TEST_TMP/server/world" "$TEST_TMP/server/world_the_end" + ./backup.sh -i "$TEST_TMP/server/world" -i "$TEST_TMP/server/world_nether" -i "$TEST_TMP/server/world_the_end" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + mkdir -p "$TEST_TMP/restored" + tar --extract --file "$TEST_TMP/backups/$TIMESTAMP.tar.gz" --directory "$TEST_TMP/restored" + assert-equals-directory "$TEST_TMP/server/world" "$TEST_TMP/restored/$TEST_TMP/server/world" + assert-equals-directory "$TEST_TMP/server/world_nether" "$TEST_TMP/restored/$TEST_TMP/server/world_nether" + assert-equals-directory "$TEST_TMP/server/world_the_end" "$TEST_TMP/restored/$TEST_TMP/server/world_the_end" +} + +test-file-changed-as-read-warning () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + dd if=/dev/urandom of="$TEST_TMP/server/world/random" & + DD_PID="$!" + OUTPUT="$(./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" 2>&1)" + EXIT_CODE="$?" + kill "$DD_PID" + assertEquals 0 "$EXIT_CODE" + assertContains "$OUTPUT" "Some files may differ in the backup archive" + + # Check that the backup actually resulted in a valid tar + assertTrue '[ -f '"$TEST_TMP/backups/$TIMESTAMP.tar.gz"' ]' + + mkdir -p "$TEST_TMP/restored" + tar --extract --file "$TEST_TMP/backups/$TIMESTAMP.tar.gz" --directory "$TEST_TMP/restored" + assert-equals-directory "$WORLD_DIR/file1.txt" "$TEST_TMP/restored/$WORLD_DIR/file1.txt" + assert-equals-directory "$WORLD_DIR/file2.txt" "$TEST_TMP/restored/$WORLD_DIR/file2.txt" + assert-equals-directory "$WORLD_DIR/file3.txt" "$TEST_TMP/restored/$WORLD_DIR/file3.txt" +} + + + test-lock-defaults () { TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" ./backup.sh -t "$TEST_TMP/lockfile" -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" @@ -183,11 +223,6 @@ test-restic-defaults () { check-latest-backup-restic } -test-backup-defaults () { - TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" - ./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" - check-backup "$TIMESTAMP.tar.gz" -} test-backup-spaces-in-directory () { TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" @@ -293,26 +328,6 @@ test-nonzero-exit-warning () { assertFalse '[ -f '"$TEST_TMP/backups/$TIMESTAMP.tar.gz"' ]' } -test-file-changed-as-read-warning () { - TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" - dd if=/dev/urandom of="$TEST_TMP/server/world/random" & - DD_PID="$!" - OUTPUT="$(./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" 2>&1)" - EXIT_CODE="$?" - kill "$DD_PID" - assertEquals 0 "$EXIT_CODE" - assertContains "$OUTPUT" "Some files may differ in the backup archive" - - # Check that the backup actually resulted in a valid tar - assertTrue '[ -f '"$TEST_TMP/backups/$TIMESTAMP.tar.gz"' ]' - - mkdir -p "$TEST_TMP/restored" - tar --extract --file "$TEST_TMP/backups/$TIMESTAMP.tar.gz" --directory "$TEST_TMP/restored" - assert-equals-directory "$WORLD_DIR/file1.txt" "$TEST_TMP/restored/file1.txt" - assert-equals-directory "$WORLD_DIR/file2.txt" "$TEST_TMP/restored/file2.txt" - assert-equals-directory "$WORLD_DIR/file3.txt" "$TEST_TMP/restored/file3.txt" -} - test-screen-interface () { TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" ./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP"