1 #!/bin/bash
  2 
  3 ########################################################################
  4 # A backup script for the tildeverse minecraft server
  5 ########################################################################
  6 # Backs files up to a remote server. Uses ssh and sftp
  7 # Designed to be run as a cronjob on the local server
  8 #
  9 # Usage: tildecraftBAK.sh [command] [option]
 10 #     -b <hourly/daily/weekly>     Run the specified backup
 11 #     -c <hourly/daily/monthly>    Run the specified cleanup
 12 ########################################################################
 13 # The intent of this script is to create hourly/daily/weekly
 14 # backups of certain files on a server, with the ability
 15 # to clean old backups based on specified timeframes.
 16 #
 17 # As written, it will keep one day's worth of hourly backups.
 18 # After that, the hourly cleanup function removes all but
 19 # the midnight backup.
 20 #
 21 # It will keep two weeks worth of those and other daily backups.
 22 # The daily cleanup function removes all but those
 23 # that fall on the first of the month.
 24 #
 25 # Lastly, it will keep four months worth of those backups.
 26 ########################################################################
 27 # An example crontab that utilizes all functions:
 28 #-----------------------------------------------------------------------
 29 # @hourly tildecraftBAK.sh -b hourly; tildecraftBAK.sh -c hourly
 30 # @daily tildecraftBAK.sh -b daily; tildecraftBAK.sh -c daily
 31 # @weekly tildecraftBAK.sh -b weekly
 32 # @monthly tildecraftBAK.sh -c monthly
 33 ########################################################################
 34 # made with <3 by ~inthreedee ( https://tilde.town/~inthreedee/ )
 35 ########################################################################
 36 
 37 # Local paths
 38 MC_PATH="/example/servers/minecraft_spigot"
 39 WORLDS_DIR="$MC_PATH/worlds"
 40 PLUGINS_DIR="$MC_PATH/plugins"
 41 LOG_FILE="$MC_PATH/backup.log"
 42 LOCK_FILE="$MC_PATH/backup.lockfile"
 43 
 44 # Remote paths
 45 BACKUP_PATH="/example/backups"
 46 
 47 # Cleanup options used when running with -c
 48 KEEP_HOURLY="1"  # _DAYS_ of hourly backups to keep
 49 KEEP_DAILY="15"  # Days of daily backups to keep
 50 KEEP_MONTHLY="4" # Months of monthly backups to keep
 51 
 52 # Timestamp format
 53 DATED_DIRS=$(date +"%Y/%B/%d/%H:%M")  # Dated backup directory structure
 54 TIMESTAMP() { date +"%D %T"; }        # Log timestamps
 55 
 56 # Remote connection arguments
 57 # Note: ssh uses lowercase -p for the port; sftp uses uppercase -P
 58 SFTP_ARGS=(-P 32 -i /path/to/key user@127.0.0.1)
 59 SSH_ARGS=(-p 32 -i /path/to/key user@127.0.0.1)
 60 
 61 
 62 ####################
 63 # Backup functions #
 64 ####################
 65 
 66 backup_hourly()
 67 {
 68     # Hourly backups for the most frequently changed worlds
 69     echo "$(TIMESTAMP) Starting hourly backup..." >> "$LOG_FILE"
 70 
 71     check_lock  # Abort if the last hourly backup is still running
 72 
 73     echo "$(TIMESTAMP) Creating backup directory $BACKUP_PATH/$DATED_DIRS ..." >> "$LOG_FILE"
 74     ssh "${SSH_ARGS[@]}" "mkdir -p $BACKUP_PATH/$DATED_DIRS" > /dev/null 2>> "$LOG_FILE"
 75 
 76     echo "$(TIMESTAMP) Backing up files..." >> "$LOG_FILE"
 77     {
 78         sftp "${SFTP_ARGS[@]}" <<EOF
 79         put -R "$WORLDS_DIR"/world "$BACKUP_PATH/$DATED_DIRS"
 80         put "$PLUGINS_DIR"/TPPets/tppets.db "$BACKUP_PATH/$DATED_DIRS"
 81         put -R "$PLUGINS_DIR"/PerWorldInventory/data "$BACKUP_PATH/$DATED_DIRS"
 82         exit
 83 EOF
 84     } > /dev/null 2>> "$LOG_FILE"
 85     echo "$(TIMESTAMP) Hourly backup complete." >> "$LOG_FILE"
 86 }
 87 
 88 backup_daily()
 89 {
 90     # Daily backups for less frequently changed worlds
 91     echo "$(TIMESTAMP) Starting daily backup..." >> "$LOG_FILE"
 92 
 93     echo "$(TIMESTAMP) Creating backup directory $BACKUP_PATH/$DATED_DIRS ..." >> "$LOG_FILE"
 94     ssh "${SSH_ARGS[@]}" "mkdir -p $BACKUP_PATH/$DATED_DIRS" > /dev/null 2>> "$LOG_FILE"
 95 
 96     echo "$(TIMESTAMP) Backing up files..." >> "$LOG_FILE"
 97     {
 98         sftp "${SFTP_ARGS[@]}" <<EOF
 99         put -R "$WORLDS_DIR"/world_nether "$BACKUP_PATH/$DATED_DIRS"
100         put -R "$WORLDS_DIR"/world_the_end "$BACKUP_PATH/$DATED_DIRS"
101         exit
102 EOF
103     } > /dev/null 2>> "$LOG_FILE"
104     echo "$(TIMESTAMP) Daily backup complete." >> "$LOG_FILE"
105 }
106 
107 backup_weekly()
108 {
109     # Weekly backups for worlds that rarely change
110     echo "$(TIMESTAMP) Starting weekly backup..." >> "$LOG_FILE"
111 
112     echo "$(TIMESTAMP) Creating backup directory $BACKUP_PATH/$DATED_DIRS ..." >> "$LOG_FILE"
113     ssh "${SSH_ARGS[@]}" "mkdir -p $BACKUP_PATH/$DATED_DIRS" > /dev/null 2>> "$LOG_FILE"
114 
115     echo "$(TIMESTAMP) Backing up files..." >> "$LOG_FILE"
116     {
117         sftp "${SFTP_ARGS[@]}" <<EOF
118         put -R "$WORLDS_DIR"/world_creative "$BACKUP_PATH/$DATED_DIRS"
119         put -R "$WORLDS_DIR"/world_superflat "$BACKUP_PATH/$DATED_DIRS"
120         exit
121 EOF
122     } > /dev/null 2>> "$LOG_FILE"
123     echo "$(TIMESTAMP) weekly backup complete." >> "$LOG_FILE"
124 }
125 
126 #####################
127 # Cleanup functions #
128 #####################
129 
130 clean_hourly()
131 {
132     # Clean all hourly backups for the date range specified
133     # except for the one taken at midnight.
134     # This leaves behind one backup for the cleaned up day
135     # which can be cleaned by the daily cleanup script.
136 
137     CLEANUP_FOLDER=$(date --date="$KEEP_HOURLY days ago" +%Y/%B/%d)
138 
139     echo "$(TIMESTAMP) Beginning cleanup of old hourly backups..." >> "$LOG_FILE"
140     ssh "${SSH_ARGS[@]}" "rm -rfv $BACKUP_PATH/$CLEANUP_FOLDER/{01..23}:00" > /dev/null 2>> "$LOG_FILE"
141     echo "$(TIMESTAMP) Hourly cleanup complete." >> "$LOG_FILE"
142 }
143 
144 clean_daily()
145 {
146     # Clean daily backups except the 1st of the month.
147     # This leaves behind one backup for each month cleaned,
148     # which can be cleaned by the monthly cleanup script.
149 
150     CLEANUP_FOLDER=$(date --date="$KEEP_DAILY days ago" +%Y/%B)
151     CLEANUP_DAY=$(date --date="$KEEP_DAILY days ago" +%d)
152 
153     echo "$(TIMESTAMP) Beginning cleanup of old daily backups..." >> "$LOG_FILE"
154     if [ "$CLEANUP_DAY" != "01" ]; then
155         echo "$(TIMESTAMP) Cleaning $CLEANUP_FOLDER/$CLEANUP_DAY ..." >> "$LOG_FILE"
156         ssh "${SSH_ARGS[@]}" "rm -rfv $BACKUP_PATH/$CLEANUP_FOLDER/$CLEANUP_DAY" > /dev/null 2>> "$LOG_FILE"
157     else
158         echo "$(TIMESTAMP) Leaving backup from the first of the month." >> "$LOG_FILE"
159     fi
160     echo "$(TIMESTAMP) Daily cleanup complete." >> "$LOG_FILE"
161 
162 }
163 
164 clean_monthly()
165 {
166     # Clean all remaining backups for the month specified below.
167     # Note that this ONLY cleans the month specified.  If the number of
168     # months being kept is reduced, the script can't detect that and a
169     # one-time manual cleanup may be required.
170 
171     CLEANUP_FOLDER=$(date --date="$KEEP_MONTHLY months ago" +%Y/%B)
172 
173     echo "$(TIMESTAMP) Beginning cleanup of old monthly backups..." >> "$LOG_FILE"
174     echo "$(TIMESTAMP) Cleaning $CLEANUP_FOLDER ..." >> "$LOG_FILE"
175     ssh "${SSH_ARGS[@]}" "rm -rfv $BACKUP_PATH/$CLEANUP_FOLDER" > /dev/null 2>> "$LOG_FILE"
176     echo "$(TIMESTAMP) Monthly cleanup complete." >> "$LOG_FILE"
177 }
178 
179 
180 #######################
181 # Secondary functions #
182 #######################
183 
184 show_help()
185 {
186     echo "A backup script for the tilde Minecraft server"
187     echo ""
188     echo "Backs files up to a remote server.  Uses ssh and sftp"
189     echo "Designed to be run as a cronjob on the local server"
190     echo ""
191     echo "Usage: tildecraftBAK.sh [command] [option]"
192     echo ""
193     echo "  -b <hourly/daily/weekly>     Run the specified backup"
194     echo "  -c <hourly/daily/monthly>    Run the specified cleanup"
195     echo ""
196     echo "An example crontab that utilizes all functions:"
197     echo ""
198     echo "@hourly tildecraftBAK.sh -b hourly; tildecraftBAK.sh -c hourly"
199     echo "@daily tildecraftBAK.sh -b daily; tildecraftBAK.sh -c daily"
200     echo "@weekly tildecraftBAK.sh -b weekly"
201     echo "@monthly tildecraftBAK.sh -c monthly"
202     exit 0
203 }
204 
205 # Which backup function needs to be called?
206 which_backup()
207 {
208     case "$1" in
209         hourly)
210             backup_hourly
211             ;;
212         daily)
213             backup_daily
214             ;;
215         weekly)
216             backup_weekly
217             ;;
218         *)
219             show_help
220             ;;
221     esac
222 }
223 
224 # Which cleanup function needs to be called?
225 which_cleanup()
226 {
227     case "$1" in
228         hourly)
229             clean_hourly
230             ;;
231         daily)
232             clean_daily
233             ;;
234         monthly)
235             clean_monthly
236             ;;
237         *)
238             show_help
239             ;;
240     esac
241 }
242 
243 check_lock()
244 {
245     # Only run once instance of this script at a time.
246     # Create a lock file if it doesn't already exist then
247     # attempt to acquire the lock. If we can't acquire a lock, abort.
248     exec 9>"$LOCK_FILE"
249     if ! flock -n 9; then
250         echo "$(TIMESTAMP) Another instance of the backup is running. Aborting." >> "$LOG_FILE"
251         exit 1
252     fi
253 }
254 
255 check_sanity()
256 {
257     # Sanity checks
258     if [ ! -x "$(command -v ssh)" ] || [ ! -x "$(command -v sftp)" ]; then
259         echo "This script requires ssh and sftp!"
260         exit 1
261     elif [ ! -d "$MC_PATH" ]; then
262         echo "Directory not found: $MC_PATH"
263         exit 1
264     elif [ ! -d "$WORLDS_DIR" ]; then
265         echo "Directory not found: $WORLDS_DIR"
266         exit 1
267     elif [ ! -d "$PLUGINS_DIR" ]; then
268         echo "Directory not found: $PLUGINS_DIR"
269         exit 1
270     fi
271 }
272 
273 ########
274 # Main #
275 ########
276 
277 if [ "$#" -eq 0 ]; then
278     # No agruments, so print usage instructions
279     show_help
280     exit 0
281 fi
282 
283 # Handle the arguments passed to the script
284 while getopts :b:c: option
285 do
286     case "$option" in
287         b)
288             check_sanity
289             which_backup "$OPTARG"
290             ;;
291         c)
292             check_sanity
293             which_cleanup "$OPTARG"
294             ;;
295         *|\?)
296             echo "Invalid option"
297             echo ""
298             show_help
299             ;;
300     esac
301 done