backup-zst.sh 11 KB

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