123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- #!/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
|