|
@@ -0,0 +1,421 @@
|
|
|
+#!/bin/bash
|
|
|
+##############################################
|
|
|
+# The original backup script was found on the internet, but i forgot where i originally got it from, and i no longer have the source,
|
|
|
+# nor was i able to retrace my steps.
|
|
|
+# If the original script looks familiar and you know where it came from please contact me so i can give credits to the original author.
|
|
|
+##############################################
|
|
|
+
|
|
|
+# Minecraft Backup Script v1.1.2
|
|
|
+
|
|
|
+# changelog v1.1.1
|
|
|
+# - adapted for use with tmux instead of screen
|
|
|
+# changelog v1.1.2
|
|
|
+# - changed achive type from tar/gzip to zst to get better compression on the backup-files.
|
|
|
+
|
|
|
+# File and directory configuration
|
|
|
+# Ensure these directories have correct permissions
|
|
|
+# Do not add trailing slashes
|
|
|
+MCDIR="/opt/minecraft"
|
|
|
+BACKUPDIR="${MCDIR}/backups"
|
|
|
+
|
|
|
+# Log location - set to false to disable logging (not recommended)
|
|
|
+LOGFILE="${MCDIR}/backup.log"
|
|
|
+
|
|
|
+# Revision directories
|
|
|
+# These directories MUST all be on the same filesystem as BACKUPDIR
|
|
|
+ONDEMANDDIR=${BACKUPDIR}
|
|
|
+HOURLYDIR=${BACKUPDIR}/hourly
|
|
|
+DAILYDIR=${BACKUPDIR}/daily
|
|
|
+WEEKLYDIR=${BACKUPDIR}/weekly
|
|
|
+MONTHLYDIR=${BACKUPDIR}/monthly
|
|
|
+
|
|
|
+# Increments of time on which to back up
|
|
|
+HOURLY=true
|
|
|
+DAILY=true
|
|
|
+WEEKLY=true
|
|
|
+MONTHLY=false
|
|
|
+
|
|
|
+# How many revisions to retain (0 for infinite)
|
|
|
+RETAINHOURS=24
|
|
|
+RETAINDAYS=7
|
|
|
+RETAINWEEKS=5
|
|
|
+RETAINMONTHS=12
|
|
|
+
|
|
|
+# Name of Minecraft tmux session
|
|
|
+MCSCREENNAME="minecraft"
|
|
|
+
|
|
|
+### Do not modify below this line unless you know what you are doing! ###
|
|
|
+
|
|
|
+mcsend() {
|
|
|
+ # $1 - Command to send
|
|
|
+ if mcrunning; then
|
|
|
+ tmux send-keys -t $MCSCREENNAME "$1" ENTER
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+mcsay() {
|
|
|
+ # $1 - Message to send
|
|
|
+ mcsend "say [§3Backup§r] $1"
|
|
|
+}
|
|
|
+
|
|
|
+logmsg() {
|
|
|
+ # $1 - Message to log
|
|
|
+ if [ "$LOGFILE" = false ]; then
|
|
|
+ return
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Accept argument or stream from STDIN
|
|
|
+ if [ -n "$1" ]; then
|
|
|
+ IN="$1"
|
|
|
+ else
|
|
|
+ read IN
|
|
|
+ fi
|
|
|
+
|
|
|
+ if [ -n "$IN" ]; then
|
|
|
+ echo "`date +"%Y-%m-%d %H:%M:%S"` mcbackup[$$]: $IN" >> $LOGFILE
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+enablesave() {
|
|
|
+ # FIXME: Remove this when save-off works correctly
|
|
|
+ chmod -R u+w $MCDIR/world/playerdata
|
|
|
+ chmod -R u+w $MCDIR/world/stats
|
|
|
+ mcsend "save-on"
|
|
|
+}
|
|
|
+
|
|
|
+err() {
|
|
|
+ # $1 - Message to log
|
|
|
+ logmsg "[ERROR] $1"
|
|
|
+ mcsay "§cBackup §cfailure"
|
|
|
+ exit 1
|
|
|
+}
|
|
|
+
|
|
|
+fileage() {
|
|
|
+ # $1 - Directory to search
|
|
|
+ # $2 - Formatting string
|
|
|
+ find $1 -maxdepth 1 -name $(ls -t $1 | grep -G "World_.*\.zst" | head -1) -printf $2
|
|
|
+}
|
|
|
+
|
|
|
+hasfile() {
|
|
|
+ # $1 - Directory to check
|
|
|
+ if [ $(numfiles $1) != 0 ]; then
|
|
|
+ true
|
|
|
+ else
|
|
|
+ false
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+numfiles() {
|
|
|
+ # $1 - Directory to check
|
|
|
+ ls $1 | grep -G "World_.*\.zst" | wc -l
|
|
|
+}
|
|
|
+
|
|
|
+PURGEFAIL=false
|
|
|
+purgefiles() {
|
|
|
+ # $1 - Directory in which to purge
|
|
|
+ # $2 - Number of files to preserve (0 disables purging)
|
|
|
+
|
|
|
+ # Only purge if retention is 0 or files exceed maximum number
|
|
|
+ FILESINDIR=$(numfiles $1)
|
|
|
+ if [ $2 -gt 0 ] && [ $FILESINDIR -gt $2 ]; then
|
|
|
+ NUMTOPURGE=$(($FILESINDIR - $2))
|
|
|
+ logmsg "Purging ${NUMTOPURGE} backup(s) from ${1}."
|
|
|
+
|
|
|
+ # DANGER ZONE - Delete files matching above script
|
|
|
+ FILENUM=0
|
|
|
+ while [ $FILENUM -lt $NUMTOPURGE ]
|
|
|
+ do
|
|
|
+ FILENUM=$(($FILENUM + 1))
|
|
|
+ FILE= read -rd $'\0' line < <(
|
|
|
+ find $1 -maxdepth 1 -type f -printf '%T@ %p\0' 2>/dev/null |
|
|
|
+ grep -ZzG "World_.*\.zst" |
|
|
|
+ sort -zn
|
|
|
+ )
|
|
|
+ TOPURGE="${line#* }"
|
|
|
+ logmsg "Purging backup file ${TOPURGE}"
|
|
|
+ if ! $(rm ${TOPURGE} 2>&1 | logmsg ; test ${PIPESTATUS[0]} -eq 0); then
|
|
|
+ PURGEFAIL=true
|
|
|
+ logmsg "[WARNING] Failed to purge a backup; stopping purge."
|
|
|
+ break;
|
|
|
+ fi
|
|
|
+ done
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+mcrunning() {
|
|
|
+ $(pidof minecraft &>/dev/null)
|
|
|
+ ismcrunning=$?
|
|
|
+
|
|
|
+ if $(tmux ls | grep -q "$MCSCREENNAME") && [ ismcrunning = 0 ]; then
|
|
|
+ return 1
|
|
|
+ else
|
|
|
+ return 0
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+# Do not send console commands if MC or screen are not running
|
|
|
+# NOTE: This relies on the Minecraft server process being named "minecraft"
|
|
|
+mcrunning
|
|
|
+if mcrunning; then
|
|
|
+ : # noop
|
|
|
+else
|
|
|
+ logmsg "WARN: Minecraft is not running or is inaccessible; not sending commands to console."
|
|
|
+fi
|
|
|
+
|
|
|
+logmsg "Backup started"
|
|
|
+
|
|
|
+# Determine whether to run on a schedule
|
|
|
+if [ "$1" == "-s" ]; then
|
|
|
+ SCHEDULE=true
|
|
|
+else
|
|
|
+ SCHEDULE=false
|
|
|
+fi
|
|
|
+
|
|
|
+# Generate a filename
|
|
|
+STARTDATE=$(date +"%Y-%m-%d %H:%M:%S")
|
|
|
+FILEPREFIX="World_$(date +"%Y-%m-%d_%H.%M.%S" --date="$STARTDATE")"
|
|
|
+
|
|
|
+# If running on a schedule, check if backups are necessary
|
|
|
+if [ "$SCHEDULE" = true ]; then
|
|
|
+ DOBACKUP=false
|
|
|
+
|
|
|
+ # Hourly backups can run if directory is missing, or if the year, day of the year, or hour
|
|
|
+ # of the day do not match the current time.
|
|
|
+ if ([ "$HOURLY" = true ] && (
|
|
|
+ [ ! -d $HOURLYDIR ] ||
|
|
|
+ ((! $(hasfile $HOURLYDIR)) ||
|
|
|
+ [ $(fileage $HOURLYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
|
|
|
+ [ $(fileage $HOURLYDIR "%Tj") != $(date +"%j" --date="$STARTDATE") ] ||
|
|
|
+ [ $(fileage $HOURLYDIR "%TH") != $(date +"%H" --date="$STARTDATE") ])
|
|
|
+ )); then
|
|
|
+ DOBACKUP=true
|
|
|
+ else
|
|
|
+ HOURLY=false
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Daily backups can run if directory is missing, or if the year or day of the year
|
|
|
+ # do not match the current time.
|
|
|
+ if ([ "$DAILY" = true ] && (
|
|
|
+ [ ! -d $DAILYDIR ] ||
|
|
|
+ ((! $(hasfile $DAILYDIR)) ||
|
|
|
+ [ $(fileage $DAILYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
|
|
|
+ [ $(fileage $DAILYDIR "%Tj") != $(date +"%j" --date="$STARTDATE") ])
|
|
|
+ )); then
|
|
|
+ DOBACKUP=true
|
|
|
+ else
|
|
|
+ DAILY=false
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Weekly backups can run if directory is missing, or if the year or week of the year do
|
|
|
+ # not match the current time.
|
|
|
+ if ([ "$WEEKLY" = true ] && (
|
|
|
+ [ ! -d $WEEKLYDIR ] ||
|
|
|
+ ((! $(hasfile $WEEKLYDIR)) ||
|
|
|
+ [ $(fileage $WEEKLYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
|
|
|
+ [ $(fileage $WEEKLYDIR "%TW") != $(date +"%W" --date="$STARTDATE") ])
|
|
|
+ )); then
|
|
|
+ DOBACKUP=true
|
|
|
+ else
|
|
|
+ WEEKLY=false
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Monthly backups can run if directory is missing, of if the year or month do not match
|
|
|
+ # the current time.
|
|
|
+ if ([ "$MONTHLY" = true ] && (
|
|
|
+ [ ! -d $MONTHLYDIR ] ||
|
|
|
+ ((! $(hasfile $MONTHLYDIR)) ||
|
|
|
+ [ $(fileage $MONTHLYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
|
|
|
+ [ $(fileage $MONTHLYDIR "%Tm") != $(date +"%m" --date="$STARTDATE") ])
|
|
|
+ )); then
|
|
|
+ DOBACKUP=true
|
|
|
+ else
|
|
|
+ MONTHLY=false
|
|
|
+ fi
|
|
|
+
|
|
|
+
|
|
|
+ # If no scheduled backups are needed, exit
|
|
|
+ if [ "$DOBACKUP" = false ]; then
|
|
|
+ logmsg "Scheduled backups already up to date; aborting."
|
|
|
+ exit 0
|
|
|
+ fi
|
|
|
+fi
|
|
|
+
|
|
|
+# Make backup directory if needed
|
|
|
+if [ ! -d "$BACKUPDIR" ]; then
|
|
|
+ mkdir "$BACKUPDIR"
|
|
|
+fi
|
|
|
+
|
|
|
+# Send a warning message to the server and disable saving
|
|
|
+mcsay "Backup started."
|
|
|
+mcsend "save-off"
|
|
|
+
|
|
|
+# Workaround to lock playerdata and stats while saving is turned off
|
|
|
+# See https://bugs.mojang.com/browse/MC-3208
|
|
|
+# FIXME: Remove this when save-off works correctly
|
|
|
+chmod -R u-w $MCDIR/world/playerdata
|
|
|
+chmod -R u-w $MCDIR/world/stats
|
|
|
+
|
|
|
+# Back up the world to a temorary location
|
|
|
+# NOTE: This must be on the same filesystem as the backup target directory
|
|
|
+TEMPFILE=$BACKUPDIR/.mcbackup.zst
|
|
|
+# FIXME: Remove permissions override when above mentioned bug is resolved
|
|
|
+if ! $(zstd -zcrf $MCDIR/world -o $TEMPFILE 2>&1 | logmsg ; test ${PIPESTATUS[0]} -eq 0); then
|
|
|
+ enablesave
|
|
|
+ rm $TEMPFILE 2>/dev/null
|
|
|
+ err "Unable to generate zst file. Aborting."
|
|
|
+fi
|
|
|
+
|
|
|
+# Allow server to begin saving again
|
|
|
+enablesave
|
|
|
+
|
|
|
+# Check if anything has changed since the last backup
|
|
|
+SUMFILE=$MCDIR/backup.md5
|
|
|
+if md5sum --status -c $SUMFILE 2>/dev/null; then
|
|
|
+ NOCHANGE=true
|
|
|
+else
|
|
|
+ NOCHANGE=false
|
|
|
+ md5sum $TEMPFILE > $SUMFILE
|
|
|
+# if ! $(gzip -fq $TEMPFILE 2>&1 | logmsg ; test ${PIPESTATUS[0]} -eq 0); then
|
|
|
+# rm $TEMPFILE 2>/dev/null
|
|
|
+# err "Unable to generate gzip file. Aborting."
|
|
|
+# fi
|
|
|
+ TEMPFILE=${TEMPFILE}
|
|
|
+fi
|
|
|
+
|
|
|
+# Only perform backups if something has changed
|
|
|
+BACKUPRUN=false
|
|
|
+BACKUPFAIL=false
|
|
|
+if [ "$NOCHANGE" = false ]; then
|
|
|
+ # Create scheduled files if the schedule is enabled
|
|
|
+ if [ "$SCHEDULE" = true ]; then
|
|
|
+ # Perform the hourly backup if enabled
|
|
|
+ if [ "$HOURLY" = true ]; then
|
|
|
+ # Create the hourly backup directory if it does not already exist
|
|
|
+ if [ ! -d $HOURLYDIR ]; then
|
|
|
+ mkdir -p $HOURLYDIR
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Hard link the hourly backup to the temporary file
|
|
|
+ if $(
|
|
|
+ ln $TEMPFILE $HOURLYDIR/${FILEPREFIX}.zst 2>&1 |
|
|
|
+ logmsg;
|
|
|
+ test ${PIPESTATUS[0]} -eq 0
|
|
|
+ ); then
|
|
|
+ logmsg "Performed hourly backup"
|
|
|
+ BACKUPRUN=true
|
|
|
+
|
|
|
+ # Purge outdated files
|
|
|
+ purgefiles $HOURLYDIR $RETAINHOURS
|
|
|
+ else
|
|
|
+ logmsg "[WARNING] Failed to complete hourly backup"
|
|
|
+ BACKUPFAIL=true
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Perform the daily backup if enabled
|
|
|
+ if [ "$DAILY" = true ]; then
|
|
|
+ # Create the daily backup directory if it does not already exist
|
|
|
+ if [ ! -d $DAILYDIR ]; then
|
|
|
+ mkdir -p $DAILYDIR
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Hard link the daily backup to the temporary file
|
|
|
+ if $(
|
|
|
+ ln $TEMPFILE $DAILYDIR/${FILEPREFIX}.zst 2>&1 |
|
|
|
+ logmsg;
|
|
|
+ test ${PIPESTATUS[0]} -eq 0
|
|
|
+ ); then
|
|
|
+ logmsg "Performed daily backup"
|
|
|
+ BACKUPRUN=true
|
|
|
+
|
|
|
+ # Purge outdated files
|
|
|
+ purgefiles $DAILYDIR $RETAINDAYS
|
|
|
+ else
|
|
|
+ logmsg "[WARNING] Failed to complete daily backup"
|
|
|
+ BACKUPFAIL=true
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Perform the weekly backup if enabled
|
|
|
+ if [ "$WEEKLY" = true ]; then
|
|
|
+ # Create the weekly backup directory if it does not already exist
|
|
|
+ if [ ! -d $WEEKLYDIR ]; then
|
|
|
+ mkdir -p $WEEKLYDIR
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Hard link the weekly backup to the temporary file
|
|
|
+ if $(
|
|
|
+ ln $TEMPFILE $WEEKLYDIR/${FILEPREFIX}.zst 2>&1 |
|
|
|
+ logmsg;
|
|
|
+ test ${PIPESTATUS[0]} -eq 0
|
|
|
+ ); then
|
|
|
+ logmsg "Performed weekly backup"
|
|
|
+ BACKUPRUN=true
|
|
|
+
|
|
|
+ # Purge outdated files
|
|
|
+ purgefiles $WEEKLYDIR $RETAINWEEKS
|
|
|
+ else
|
|
|
+ logmsg "[WARNING] Failed to complete weekly backup"
|
|
|
+ BACKUPFAIL=true
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Perform the monthly backup if enabled
|
|
|
+ if [ "$MONTHLY" = true ]; then
|
|
|
+ # Create the monthly backup directory if it does not already exist
|
|
|
+ if [ ! -d $MONTHLYDIR ]; then
|
|
|
+ mkdir -p $MONTHLYDIR
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Hard link the monthly backup to the temporary file
|
|
|
+ if $(
|
|
|
+ ln $TEMPFILE $MONTHLYDIR/${FILEPREFIX}.zzt 2>&1 |
|
|
|
+ logmsg;
|
|
|
+ test ${PIPESTATUS[0]} -eq 0
|
|
|
+ ); then
|
|
|
+ logmsg "Performed monthly backup"
|
|
|
+ BACKUPRUN=true
|
|
|
+
|
|
|
+ # Purge outdated files
|
|
|
+ purgefiles $MONTHLYDIR $RETAINWEEKS
|
|
|
+ else
|
|
|
+ logmsg "[WARNING] Failed to complete monthly backup"
|
|
|
+ BACKUPFAIL=true
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+
|
|
|
+ else
|
|
|
+ # Create on-demand backup if this is not a schedule run
|
|
|
+ logmsg "Performed backup on demand"
|
|
|
+ ln $TEMPFILE $BACKUPDIR/${FILEPREFIX}.zst
|
|
|
+ BACKUPRUN=true
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Always link the last backup to latest if a backup ran and did not fail
|
|
|
+ if [ "$BACKUPFAIL" = false ]; then
|
|
|
+ if [ "$BACKUPRUN" = true ]; then
|
|
|
+ rm $BACKUPDIR/latest.zst 2>/dev/null
|
|
|
+ ln $TEMPFILE $BACKUPDIR/latest.zst
|
|
|
+ logmsg "Backup completed successfully"
|
|
|
+ else
|
|
|
+ logmsg "Scheduled backups are already up to date"
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+else
|
|
|
+ logmsg "No change was detected in the world file; backup stopped"
|
|
|
+fi
|
|
|
+
|
|
|
+# Remove the temporary file
|
|
|
+rm $TEMPFILE
|
|
|
+
|
|
|
+# If there was a purge failure, notify the server
|
|
|
+if [ "$PURGEFAIL" = true ]; then
|
|
|
+ mcsay "§cPurge §cfailure §c- §ccheck §clog §cfile"
|
|
|
+fi
|
|
|
+
|
|
|
+# Display appropriate message, depending on backup status
|
|
|
+if [ "$BACKUPFAIL" = false ]; then
|
|
|
+ mcsay "Backup complete."
|
|
|
+else
|
|
|
+ err "Unable to complete all backups."
|
|
|
+fi
|