Add lockfile support
This commit is contained in:
parent
ac2bf6ccb6
commit
ca2e82815a
3 changed files with 93 additions and 52 deletions
|
@ -64,8 +64,10 @@ Command line options:
|
||||||
-q Suppress warnings
|
-q Suppress warnings
|
||||||
-r Restic repo name (if using restic)
|
-r Restic repo name (if using restic)
|
||||||
-s Screen name, tmux session name, or hostname:port:password for RCON
|
-s Screen name, tmux session name, or hostname:port:password for RCON
|
||||||
|
-t Enable lock file (lock file not used by default)
|
||||||
|
-u Lock file timeout seconds (empty = unlimited)
|
||||||
-v Verbose mode
|
-v Verbose mode
|
||||||
-w Window manager: screen (default), tmux, RCON
|
-w Window manager: screen (default), tmux, RCON
|
||||||
```
|
```
|
||||||
|
|
||||||
### Automate backups with cron
|
### Automate backups with cron
|
||||||
|
|
124
backup.sh
124
backup.sh
|
@ -20,6 +20,8 @@ ENABLE_CHAT_MESSAGES=false # Tell players in Minecraft chat about backup status
|
||||||
PREFIX="Backup" # Shows in the chat message
|
PREFIX="Backup" # Shows in the chat message
|
||||||
DEBUG=false # Enable debug messages
|
DEBUG=false # Enable debug messages
|
||||||
SUPPRESS_WARNINGS=false # Suppress warnings
|
SUPPRESS_WARNINGS=false # Suppress warnings
|
||||||
|
LOCK_FILE="" # Optional lock file to acquire to ensure two backups don't run at once
|
||||||
|
LOCK_FILE_TIMEOUT="" # Optional lock file wait timeout (in seconds)
|
||||||
WINDOW_MANAGER="screen" # Choices: screen, tmux, RCON
|
WINDOW_MANAGER="screen" # Choices: screen, tmux, RCON
|
||||||
|
|
||||||
# Other Variables (do not modify)
|
# Other Variables (do not modify)
|
||||||
|
@ -38,7 +40,7 @@ debug-log () {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:vw:' FLAG; do
|
while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:t:u:vw:' FLAG; do
|
||||||
case $FLAG in
|
case $FLAG in
|
||||||
a) COMPRESSION_ALGORITHM=$OPTARG ;;
|
a) COMPRESSION_ALGORITHM=$OPTARG ;;
|
||||||
c) ENABLE_CHAT_MESSAGES=true ;;
|
c) ENABLE_CHAT_MESSAGES=true ;;
|
||||||
|
@ -61,6 +63,8 @@ while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:vw:' FLAG; do
|
||||||
echo "-q Suppress warnings"
|
echo "-q Suppress warnings"
|
||||||
echo "-r Restic repo name (if using restic)"
|
echo "-r Restic repo name (if using restic)"
|
||||||
echo "-s Screen name, tmux session name, or hostname:port:password for RCON"
|
echo "-s Screen name, tmux session name, or hostname:port:password for RCON"
|
||||||
|
echo "-t Enable lock file (lock file not used by default)"
|
||||||
|
echo "-u Lock file timeout seconds (empty = unlimited)"
|
||||||
echo "-v Verbose mode"
|
echo "-v Verbose mode"
|
||||||
echo "-w Window manager: screen (default), tmux, RCON"
|
echo "-w Window manager: screen (default), tmux, RCON"
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -69,10 +73,12 @@ while getopts 'a:cd:e:f:hi:l:m:o:p:qr:s:vw:' FLAG; do
|
||||||
l) COMPRESSION_LEVEL=$OPTARG ;;
|
l) COMPRESSION_LEVEL=$OPTARG ;;
|
||||||
m) MAX_BACKUPS=$OPTARG ;;
|
m) MAX_BACKUPS=$OPTARG ;;
|
||||||
o) BACKUP_DIRECTORY=$OPTARG ;;
|
o) BACKUP_DIRECTORY=$OPTARG ;;
|
||||||
r) RESTIC_REPO=$OPTARG ;;
|
|
||||||
p) PREFIX=$OPTARG ;;
|
p) PREFIX=$OPTARG ;;
|
||||||
q) SUPPRESS_WARNINGS=true ;;
|
q) SUPPRESS_WARNINGS=true ;;
|
||||||
|
r) RESTIC_REPO=$OPTARG ;;
|
||||||
s) SCREEN_NAME=$OPTARG ;;
|
s) SCREEN_NAME=$OPTARG ;;
|
||||||
|
t) LOCK_FILE=$OPTARG ;;
|
||||||
|
u) LOCK_FILE_TIMEOUT=$OPTARG ;;
|
||||||
v) DEBUG=true ;;
|
v) DEBUG=true ;;
|
||||||
w) WINDOW_MANAGER=$OPTARG ;;
|
w) WINDOW_MANAGER=$OPTARG ;;
|
||||||
*) log-fatal "Invalid option -$FLAG"; exit 1 ;;
|
*) log-fatal "Invalid option -$FLAG"; exit 1 ;;
|
||||||
|
@ -467,64 +473,80 @@ clean-up () {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Notify players of start
|
|
||||||
message-players "Starting backup..." "$ARCHIVE_FILE_NAME"
|
|
||||||
|
|
||||||
trap "clean-up" 2
|
trap "clean-up" 2
|
||||||
|
|
||||||
# Disable world autosaving
|
do-backup () {
|
||||||
execute-command "save-off"
|
# Notify players of start
|
||||||
|
message-players "Starting backup..." "$ARCHIVE_FILE_NAME"
|
||||||
|
|
||||||
# Backup world
|
# Disable world autosaving
|
||||||
START_TIME=$(date +"%s")
|
execute-command "save-off"
|
||||||
|
|
||||||
if [[ "$BACKUP_DIRECTORY" != "" ]]; then
|
# Backup world
|
||||||
# Ensure backup directory exists
|
START_TIME=$(date +"%s")
|
||||||
mkdir -p "$(dirname "$ARCHIVE_PATH")"
|
|
||||||
|
|
||||||
case $COMPRESSION_ALGORITHM in
|
if [[ "$BACKUP_DIRECTORY" != "" ]]; then
|
||||||
# No compression
|
# Ensure backup directory exists
|
||||||
"") tar -cf "$ARCHIVE_PATH" -C "$SERVER_WORLD" .
|
mkdir -p "$(dirname "$ARCHIVE_PATH")"
|
||||||
;;
|
|
||||||
# With compression
|
|
||||||
*) tar -cf - -C "$SERVER_WORLD" . | $COMPRESSION_ALGORITHM -cv -"$COMPRESSION_LEVEL" - > "$ARCHIVE_PATH" 2>> /dev/null
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
EXIT_CODES=("${PIPESTATUS[@]}")
|
|
||||||
|
|
||||||
# tar exit codes: http://www.gnu.org/software/tar/manual/html_section/Synopsis.html
|
case $COMPRESSION_ALGORITHM in
|
||||||
# 0 = successful, 1 = some files differ, 2 = fatal
|
# No compression
|
||||||
if [ "${EXIT_CODES[0]}" == "1" ]; then
|
"") tar -cf "$ARCHIVE_PATH" -C "$SERVER_WORLD" .
|
||||||
log-warning "Some files may differ in the backup archive (file changed as read)"
|
;;
|
||||||
TAR_EXIT_CODE="0"
|
# With compression
|
||||||
else
|
*) tar -cf - -C "$SERVER_WORLD" . | $COMPRESSION_ALGORITHM -cv -"$COMPRESSION_LEVEL" - > "$ARCHIVE_PATH" 2>> /dev/null
|
||||||
TAR_EXIT_CODE="${EXIT_CODES[0]}"
|
;;
|
||||||
fi
|
esac
|
||||||
|
EXIT_CODES=("${PIPESTATUS[@]}")
|
||||||
|
|
||||||
ARCHIVE_EXIT_CODE="$(exit-code "$TAR_EXIT_CODE" "${EXIT_CODES[1]}")"
|
# tar exit codes: http://www.gnu.org/software/tar/manual/html_section/Synopsis.html
|
||||||
if [ "$ARCHIVE_EXIT_CODE" -ne 0 ]; then
|
# 0 = successful, 1 = some files differ, 2 = fatal
|
||||||
log-fatal "Archive command exited with nonzero exit code $ARCHIVE_EXIT_CODE"
|
if [ "${EXIT_CODES[0]}" == "1" ]; then
|
||||||
fi
|
log-warning "Some files may differ in the backup archive (file changed as read)"
|
||||||
fi
|
TAR_EXIT_CODE="0"
|
||||||
|
else
|
||||||
|
TAR_EXIT_CODE="${EXIT_CODES[0]}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$RESTIC_REPO" != "" ]]; then
|
ARCHIVE_EXIT_CODE="$(exit-code "$TAR_EXIT_CODE" "${EXIT_CODES[1]}")"
|
||||||
RESTIC_TIMESTAMP="${TIMESTAMP:0:10} ${TIMESTAMP:11:2}:${TIMESTAMP:14:2}:${TIMESTAMP:17:2}"
|
|
||||||
restic backup -r "$RESTIC_REPO" "$SERVER_WORLD" --time "$RESTIC_TIMESTAMP" "$QUIET"
|
|
||||||
ARCHIVE_EXIT_CODE=$?
|
|
||||||
if [ "$ARCHIVE_EXIT_CODE" -eq 3 ]; then
|
|
||||||
log-warning "Incomplete snapshot taken (some files could not be read)"
|
|
||||||
ARCHIVE_EXIT_CODE="0"
|
|
||||||
else
|
|
||||||
if [ "$ARCHIVE_EXIT_CODE" -ne 0 ]; then
|
if [ "$ARCHIVE_EXIT_CODE" -ne 0 ]; then
|
||||||
# According to the restic docs, exit code is either 0, 1, or 3
|
log-fatal "Archive command exited with nonzero exit code $ARCHIVE_EXIT_CODE"
|
||||||
# Exit code 1 means fatal
|
|
||||||
# See: https://restic.readthedocs.io/en/latest/040_backup.html
|
|
||||||
log-fatal "No restic snapshot created (exit code $ARCHIVE_EXIT_CODE)"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$RESTIC_REPO" != "" ]]; then
|
||||||
|
RESTIC_TIMESTAMP="${TIMESTAMP:0:10} ${TIMESTAMP:11:2}:${TIMESTAMP:14:2}:${TIMESTAMP:17:2}"
|
||||||
|
restic backup -r "$RESTIC_REPO" "$SERVER_WORLD" --time "$RESTIC_TIMESTAMP" "$QUIET"
|
||||||
|
ARCHIVE_EXIT_CODE=$?
|
||||||
|
if [ "$ARCHIVE_EXIT_CODE" -eq 3 ]; then
|
||||||
|
log-warning "Incomplete snapshot taken (some files could not be read)"
|
||||||
|
ARCHIVE_EXIT_CODE="0"
|
||||||
|
else
|
||||||
|
if [ "$ARCHIVE_EXIT_CODE" -ne 0 ]; then
|
||||||
|
# According to the restic docs, exit code is either 0, 1, or 3
|
||||||
|
# Exit code 1 means fatal
|
||||||
|
# See: https://restic.readthedocs.io/en/latest/040_backup.html
|
||||||
|
log-fatal "No restic snapshot created (exit code $ARCHIVE_EXIT_CODE)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sync
|
||||||
|
END_TIME=$(date +"%s")
|
||||||
|
|
||||||
|
clean-up
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$LOCK_FILE" != "" ]]; then
|
||||||
|
TIMEOUT_OPTION=""
|
||||||
|
if [[ "$LOCK_FILE_TIMEOUT" != "" ]]; then
|
||||||
|
TIMEOUT_OPTION="-w $LOCK_FILE_TIMEOUT"
|
||||||
|
fi
|
||||||
|
(if ! flock $TIMEOUT_OPTION --no-fork 200; then
|
||||||
|
log-fatal "Could not acquire lock on lock file: $LOCK_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
do-backup) 200>"$LOCK_FILE"
|
||||||
|
else
|
||||||
|
do-backup
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sync
|
|
||||||
END_TIME=$(date +"%s")
|
|
||||||
|
|
||||||
clean-up
|
|
||||||
|
|
17
test/test.sh
17
test/test.sh
|
@ -80,6 +80,23 @@ check-latest-backup-restic () {
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
|
|
||||||
|
test-lock-defaults () {
|
||||||
|
TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")"
|
||||||
|
./backup.sh -t "$TEST_TMP/lockfile" -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP"
|
||||||
|
check-backup "$TIMESTAMP.tar.gz"
|
||||||
|
TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01 +1 hour")"
|
||||||
|
./backup.sh -t "$TEST_TMP/lockfile" -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP"
|
||||||
|
check-backup "$TIMESTAMP.tar.gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
test-lock-timeout () {
|
||||||
|
TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")"
|
||||||
|
flock "$TEST_TMP/lockfile" sleep 10 &
|
||||||
|
OUTPUT=$(./backup.sh -t "$TEST_TMP/lockfile" -u 0 -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP")
|
||||||
|
assertNotEquals 0 "$?"
|
||||||
|
assertContains "$OUTPUT" "Could not acquire lock on lock file: $TEST_TMP/lockfile"
|
||||||
|
}
|
||||||
|
|
||||||
test-restic-incomplete-snapshot () {
|
test-restic-incomplete-snapshot () {
|
||||||
chmod 000 "$TEST_TMP/server/world/file1.txt"
|
chmod 000 "$TEST_TMP/server/world/file1.txt"
|
||||||
TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")"
|
TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")"
|
||||||
|
|
Loading…
Add table
Reference in a new issue