backup.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. #!/bin/bash
  2. ##############################################
  3. # 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,
  4. # nor was i able to retrace my steps.
  5. # 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.
  6. ###############################################
  7. # changelog v1.1.1
  8. # - adapted for use with tmux instead of screen
  9. # Minecraft Backup Script v1.1.1 - adapted for use with tmux instead of screen
  10. # The script assumes tmux was launched with a named session called 'minecraft'
  11. # File and directory configuration
  12. # Ensure these directories have correct permissions
  13. # Do not add trailing slashes
  14. MCDIR="/opt/minecraft"
  15. BACKUPDIR="${MCDIR}/backups"
  16. # Log location - set to false to disable logging (not recommended)
  17. LOGFILE="${MCDIR}/backup.log"
  18. # Revision directories
  19. # These directories MUST all be on the same filesystem as BACKUPDIR
  20. ONDEMANDDIR=${BACKUPDIR}
  21. HOURLYDIR=${BACKUPDIR}/hourly
  22. DAILYDIR=${BACKUPDIR}/daily
  23. WEEKLYDIR=${BACKUPDIR}/weekly
  24. MONTHLYDIR=${BACKUPDIR}/monthly
  25. # Increments of time on which to back up
  26. HOURLY=true
  27. DAILY=true
  28. WEEKLY=true
  29. MONTHLY=false
  30. # How many revisions to retain (0 for infinite)
  31. RETAINHOURS=24
  32. RETAINDAYS=7
  33. RETAINWEEKS=5
  34. RETAINMONTHS=12
  35. # Name of Minecraft tmux session
  36. MCSCREENNAME="minecraft"
  37. ### Do not modify below this line unless you know what you are doing! ###
  38. mcsend() {
  39. # $1 - Command to send
  40. if mcrunning; then
  41. tmux send-keys -t $MCSCREENNAME "$1" ENTER
  42. fi
  43. }
  44. mcsay() {
  45. # $1 - Message to send
  46. mcsend "say [§3Backup§r] $1"
  47. }
  48. logmsg() {
  49. # $1 - Message to log
  50. if [ "$LOGFILE" = false ]; then
  51. return
  52. fi
  53. # Accept argument or stream from STDIN
  54. if [ -n "$1" ]; then
  55. IN="$1"
  56. else
  57. read IN
  58. fi
  59. if [ -n "$IN" ]; then
  60. echo "`date +"%Y-%m-%d %H:%M:%S"` mcbackup[$$]: $IN" >> $LOGFILE
  61. fi
  62. }
  63. enablesave() {
  64. # FIXME: Remove this when save-off works correctly
  65. chmod -R u+w $MCDIR/world/playerdata
  66. chmod -R u+w $MCDIR/world/stats
  67. mcsend "save-on"
  68. }
  69. err() {
  70. # $1 - Message to log
  71. logmsg "[ERROR] $1"
  72. mcsay "§cBackup §cfailure"
  73. exit 1
  74. }
  75. fileage() {
  76. # $1 - Directory to search
  77. # $2 - Formatting string
  78. find $1 -maxdepth 1 -name $(ls -t $1 | grep -G "World_.*\.tar\.gz" | head -1) -printf $2
  79. }
  80. hasfile() {
  81. # $1 - Directory to check
  82. if [ $(numfiles $1) != 0 ]; then
  83. true
  84. else
  85. false
  86. fi
  87. }
  88. numfiles() {
  89. # $1 - Directory to check
  90. ls $1 | grep -G "World_.*\.tar\.gz" | wc -l
  91. }
  92. PURGEFAIL=false
  93. purgefiles() {
  94. # $1 - Directory in which to purge
  95. # $2 - Number of files to preserve (0 disables purging)
  96. # Only purge if retention is 0 or files exceed maximum number
  97. FILESINDIR=$(numfiles $1)
  98. if [ $2 -gt 0 ] && [ $FILESINDIR -gt $2 ]; then
  99. NUMTOPURGE=$(($FILESINDIR - $2))
  100. logmsg "Purging ${NUMTOPURGE} backup(s) from ${1}."
  101. # DANGER ZONE - Delete files matching above script
  102. FILENUM=0
  103. while [ $FILENUM -lt $NUMTOPURGE ]
  104. do
  105. FILENUM=$(($FILENUM + 1))
  106. FILE= read -rd $'\0' line < <(
  107. find $1 -maxdepth 1 -type f -printf '%T@ %p\0' 2>/dev/null |
  108. grep -ZzG "World_.*\.tar\.gz" |
  109. sort -zn
  110. )
  111. TOPURGE="${line#* }"
  112. logmsg "Purging backup file ${TOPURGE}"
  113. if ! $(rm ${TOPURGE} 2>&1 | logmsg ; test ${PIPESTATUS[0]} -eq 0); then
  114. PURGEFAIL=true
  115. logmsg "[WARNING] Failed to purge a backup; stopping purge."
  116. break;
  117. fi
  118. done
  119. fi
  120. }
  121. mcrunning() {
  122. $(pidof minecraft &>/dev/null)
  123. ismcrunning=$?
  124. if $(tmux ls | grep -q "$MCSCREENNAME") && [ ismcrunning = 0 ]; then
  125. return 1
  126. else
  127. return 0
  128. fi
  129. }
  130. # Do not send console commands if MC or screen are not running
  131. # NOTE: This relies on the Minecraft server process being named "minecraft"
  132. mcrunning
  133. if mcrunning; then
  134. : # noop
  135. else
  136. logmsg "WARN: Minecraft is not running or is inaccessible; not sending commands to console."
  137. fi
  138. logmsg "Backup started"
  139. # Determine whether to run on a schedule
  140. if [ "$1" == "-s" ]; then
  141. SCHEDULE=true
  142. else
  143. SCHEDULE=false
  144. fi
  145. # Generate a filename
  146. STARTDATE=$(date +"%Y-%m-%d %H:%M:%S")
  147. FILEPREFIX="World_$(date +"%Y-%m-%d_%H.%M.%S" --date="$STARTDATE")"
  148. # If running on a schedule, check if backups are necessary
  149. if [ "$SCHEDULE" = true ]; then
  150. DOBACKUP=false
  151. # Hourly backups can run if directory is missing, or if the year, day of the year, or hour
  152. # of the day do not match the current time.
  153. if ([ "$HOURLY" = true ] && (
  154. [ ! -d $HOURLYDIR ] ||
  155. ((! $(hasfile $HOURLYDIR)) ||
  156. [ $(fileage $HOURLYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
  157. [ $(fileage $HOURLYDIR "%Tj") != $(date +"%j" --date="$STARTDATE") ] ||
  158. [ $(fileage $HOURLYDIR "%TH") != $(date +"%H" --date="$STARTDATE") ])
  159. )); then
  160. DOBACKUP=true
  161. else
  162. HOURLY=false
  163. fi
  164. # Daily backups can run if directory is missing, or if the year or day of the year
  165. # do not match the current time.
  166. if ([ "$DAILY" = true ] && (
  167. [ ! -d $DAILYDIR ] ||
  168. ((! $(hasfile $DAILYDIR)) ||
  169. [ $(fileage $DAILYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
  170. [ $(fileage $DAILYDIR "%Tj") != $(date +"%j" --date="$STARTDATE") ])
  171. )); then
  172. DOBACKUP=true
  173. else
  174. DAILY=false
  175. fi
  176. # Weekly backups can run if directory is missing, or if the year or week of the year do
  177. # not match the current time.
  178. if ([ "$WEEKLY" = true ] && (
  179. [ ! -d $WEEKLYDIR ] ||
  180. ((! $(hasfile $WEEKLYDIR)) ||
  181. [ $(fileage $WEEKLYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
  182. [ $(fileage $WEEKLYDIR "%TW") != $(date +"%W" --date="$STARTDATE") ])
  183. )); then
  184. DOBACKUP=true
  185. else
  186. WEEKLY=false
  187. fi
  188. # Monthly backups can run if directory is missing, of if the year or month do not match
  189. # the current time.
  190. if ([ "$MONTHLY" = true ] && (
  191. [ ! -d $MONTHLYDIR ] ||
  192. ((! $(hasfile $MONTHLYDIR)) ||
  193. [ $(fileage $MONTHLYDIR "%TY") != $(date +"%Y" --date="$STARTDATE") ] ||
  194. [ $(fileage $MONTHLYDIR "%Tm") != $(date +"%m" --date="$STARTDATE") ])
  195. )); then
  196. DOBACKUP=true
  197. else
  198. MONTHLY=false
  199. fi
  200. # If no scheduled backups are needed, exit
  201. if [ "$DOBACKUP" = false ]; then
  202. logmsg "Scheduled backups already up to date; aborting."
  203. exit 0
  204. fi
  205. fi
  206. # Make backup directory if needed
  207. if [ ! -d "$BACKUPDIR" ]; then
  208. mkdir "$BACKUPDIR"
  209. fi
  210. # Send a warning message to the server and disable saving
  211. mcsay "Backup started."
  212. mcsend "save-off"
  213. # Workaround to lock playerdata and stats while saving is turned off
  214. # See https://bugs.mojang.com/browse/MC-3208
  215. # FIXME: Remove this when save-off works correctly
  216. chmod -R u-w $MCDIR/world/playerdata
  217. chmod -R u-w $MCDIR/world/stats
  218. # Back up the world to a temorary location
  219. # NOTE: This must be on the same filesystem as the backup target directory
  220. TEMPFILE=$BACKUPDIR/.mcbackup.tar
  221. # FIXME: Remove permissions override when above mentioned bug is resolved
  222. if ! $(tar --mode="a+rw" -cf $TEMPFILE -C $MCDIR world 2>&1 | logmsg ; test ${PIPESTATUS[0]} -eq 0); then
  223. enablesave
  224. rm $TEMPFILE 2>/dev/null
  225. err "Unable to generate tar file. Aborting."
  226. fi
  227. # Allow server to begin saving again
  228. enablesave
  229. # Check if anything has changed since the last backup
  230. SUMFILE=$MCDIR/backup.md5
  231. if md5sum --status -c $SUMFILE 2>/dev/null; then
  232. NOCHANGE=true
  233. else
  234. NOCHANGE=false
  235. md5sum $TEMPFILE > $SUMFILE
  236. if ! $(gzip -fq $TEMPFILE 2>&1 | logmsg ; test ${PIPESTATUS[0]} -eq 0); then
  237. rm $TEMPFILE 2>/dev/null
  238. err "Unable to generate gzip file. Aborting."
  239. fi
  240. TEMPFILE=${TEMPFILE}.gz
  241. fi
  242. # Only perform backups if something has changed
  243. BACKUPRUN=false
  244. BACKUPFAIL=false
  245. if [ "$NOCHANGE" = false ]; then
  246. # Create scheduled files if the schedule is enabled
  247. if [ "$SCHEDULE" = true ]; then
  248. # Perform the hourly backup if enabled
  249. if [ "$HOURLY" = true ]; then
  250. # Create the hourly backup directory if it does not already exist
  251. if [ ! -d $HOURLYDIR ]; then
  252. mkdir -p $HOURLYDIR
  253. fi
  254. # Hard link the hourly backup to the temporary file
  255. if $(
  256. ln $TEMPFILE $HOURLYDIR/${FILEPREFIX}.tar.gz 2>&1 |
  257. logmsg;
  258. test ${PIPESTATUS[0]} -eq 0
  259. ); then
  260. logmsg "Performed hourly backup"
  261. BACKUPRUN=true
  262. # Purge outdated files
  263. purgefiles $HOURLYDIR $RETAINHOURS
  264. else
  265. logmsg "[WARNING] Failed to complete hourly backup"
  266. BACKUPFAIL=true
  267. fi
  268. fi
  269. # Perform the daily backup if enabled
  270. if [ "$DAILY" = true ]; then
  271. # Create the daily backup directory if it does not already exist
  272. if [ ! -d $DAILYDIR ]; then
  273. mkdir -p $DAILYDIR
  274. fi
  275. # Hard link the daily backup to the temporary file
  276. if $(
  277. ln $TEMPFILE $DAILYDIR/${FILEPREFIX}.tar.gz 2>&1 |
  278. logmsg;
  279. test ${PIPESTATUS[0]} -eq 0
  280. ); then
  281. logmsg "Performed daily backup"
  282. BACKUPRUN=true
  283. # Purge outdated files
  284. purgefiles $DAILYDIR $RETAINDAYS
  285. else
  286. logmsg "[WARNING] Failed to complete daily backup"
  287. BACKUPFAIL=true
  288. fi
  289. fi
  290. # Perform the weekly backup if enabled
  291. if [ "$WEEKLY" = true ]; then
  292. # Create the weekly backup directory if it does not already exist
  293. if [ ! -d $WEEKLYDIR ]; then
  294. mkdir -p $WEEKLYDIR
  295. fi
  296. # Hard link the weekly backup to the temporary file
  297. if $(
  298. ln $TEMPFILE $WEEKLYDIR/${FILEPREFIX}.tar.gz 2>&1 |
  299. logmsg;
  300. test ${PIPESTATUS[0]} -eq 0
  301. ); then
  302. logmsg "Performed weekly backup"
  303. BACKUPRUN=true
  304. # Purge outdated files
  305. purgefiles $WEEKLYDIR $RETAINWEEKS
  306. else
  307. logmsg "[WARNING] Failed to complete weekly backup"
  308. BACKUPFAIL=true
  309. fi
  310. fi
  311. # Perform the monthly backup if enabled
  312. if [ "$MONTHLY" = true ]; then
  313. # Create the monthly backup directory if it does not already exist
  314. if [ ! -d $MONTHLYDIR ]; then
  315. mkdir -p $MONTHLYDIR
  316. fi
  317. # Hard link the monthly backup to the temporary file
  318. if $(
  319. ln $TEMPFILE $MONTHLYDIR/${FILEPREFIX}.tar.gz 2>&1 |
  320. logmsg;
  321. test ${PIPESTATUS[0]} -eq 0
  322. ); then
  323. logmsg "Performed monthly backup"
  324. BACKUPRUN=true
  325. # Purge outdated files
  326. purgefiles $MONTHLYDIR $RETAINWEEKS
  327. else
  328. logmsg "[WARNING] Failed to complete monthly backup"
  329. BACKUPFAIL=true
  330. fi
  331. fi
  332. else
  333. # Create on-demand backup if this is not a schedule run
  334. logmsg "Performed backup on demand"
  335. ln $TEMPFILE $BACKUPDIR/${FILEPREFIX}.tar.gz
  336. BACKUPRUN=true
  337. fi
  338. # Always link the last backup to latest if a backup ran and did not fail
  339. if [ "$BACKUPFAIL" = false ]; then
  340. if [ "$BACKUPRUN" = true ]; then
  341. rm $BACKUPDIR/latest.tar.gz 2>/dev/null
  342. ln $TEMPFILE $BACKUPDIR/latest.tar.gz
  343. logmsg "Backup completed successfully"
  344. else
  345. logmsg "Scheduled backups are already up to date"
  346. fi
  347. fi
  348. else
  349. logmsg "No change was detected in the world file; backup stopped"
  350. fi
  351. # Remove the temporary file
  352. rm $TEMPFILE
  353. # If there was a purge failure, notify the server
  354. if [ "$PURGEFAIL" = true ]; then
  355. mcsay "§cPurge §cfailure §c- §ccheck §clog §cfile"
  356. fi
  357. # Display appropriate message, depending on backup status
  358. if [ "$BACKUPFAIL" = false ]; then
  359. mcsay "Backup complete."
  360. else
  361. err "Unable to complete all backups."
  362. fi