forked from nicolaschan/minecraft-backup
-
Notifications
You must be signed in to change notification settings - Fork 0
/
backup.sh
executable file
·565 lines (496 loc) · 17.2 KB
/
backup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
#!/usr/bin/env bash
# Minecraft server automatic backup management script
# https://github.com/nicolaschan/minecraft-backup
# MIT License
#
# For Minecraft servers running in a GNU screen, tmux, or RCON.
# For most convenience, run automatically with cron.
# Default Configuration
SCREEN_NAME="" # Name of the GNU Screen, tmux session, or hostname:port:password for RCON
SERVER_WORLDS=() # Server world directory
BACKUP_DIRECTORY="" # Directory to save backups in
MAX_BACKUPS=128 # -1 indicates unlimited
DELETE_METHOD="thin" # Choices: thin, sequential, none; sequential: delete oldest; thin: keep last 24 hourly, last 30 daily, and monthly (use with 1 hr cron interval)
COMPRESSION_ALGORITHM="gzip" # Set to "none" for no compression
COMPRESSION_FILE_EXTENSION=".gz" # Leave empty for no compression; Precede with a . (for example: ".gz")
COMPRESSION_LEVEL=3 # Passed to the compression algorithm
ENABLE_CHAT_MESSAGES=false # Tell players in Minecraft chat about backup status
PREFIX="Backup" # Shows in the chat message
DEBUG=false # Enable debug messages
SUPPRESS_WARNINGS=false # Suppress warnings
RESTIC_HOSTNAME="" # Leave empty to use system hostname
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
# Other Variables (do not modify)
DATE_FORMAT="%F_%H-%M-%S"
TIMESTAMP=$(date +$DATE_FORMAT)
log-fatal () {
echo -e "\033[0;31mFATAL:\033[0m $*"
}
log-warning () {
echo -e "\033[0;33mWARNING:\033[0m $*"
}
debug-log () {
if "$DEBUG"; then
echo "$1"
fi
}
while getopts 'a:cd:e:f:hH:i:l:m:o:p:qr:s:t:u:vw:x' FLAG; do
case $FLAG in
a) COMPRESSION_ALGORITHM=$OPTARG ;;
c) ENABLE_CHAT_MESSAGES=true ;;
d) DELETE_METHOD=$OPTARG ;;
e) COMPRESSION_FILE_EXTENSION=".$OPTARG" ;;
f) TIMESTAMP=$OPTARG ;;
h) echo "Minecraft Backup"
echo "Repository: https://github.com/nicolaschan/minecraft-backup"
echo "-a Compression algorithm (default: gzip). Set to \"none\" for no compression"
echo "-c Enable chat messages"
echo "-d Delete method: thin (default), sequential, none"
echo "-e Compression file extension, exclude leading \".\" (default: gz)"
echo "-f Output file name (default is the timestamp)"
echo "-h Shows this help text"
echo "-H Set hostname for restic backup (restic only)"
echo "-i Input directory (path to world folder, use -i once for each world)"
echo "-l Compression level (default: 3)"
echo "-m Maximum backups to keep, use -1 for unlimited (default: 128)"
echo "-o Output directory"
echo "-p Prefix that shows in Minecraft chat (default: Backup)"
echo "-q Suppress warnings"
echo "-r Restic repo name (if using restic)"
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 "-w Window manager: screen (default), tmux, RCON"
exit 0
;;
H) RESTIC_HOSTNAME=$OPTARG ;;
i) SERVER_WORLDS+=("$OPTARG") ;;
l) COMPRESSION_LEVEL=$OPTARG ;;
m) MAX_BACKUPS=$OPTARG ;;
o) BACKUP_DIRECTORY=$OPTARG ;;
p) PREFIX=$OPTARG ;;
q) SUPPRESS_WARNINGS=true ;;
r) RESTIC_REPO=$OPTARG ;;
s) SCREEN_NAME=$OPTARG ;;
t) LOCK_FILE=$OPTARG ;;
u) LOCK_FILE_TIMEOUT=$OPTARG ;;
v) DEBUG=true ;;
w) WINDOW_MANAGER=$OPTARG ;;
*) log-fatal "Invalid option -$FLAG"; exit 1 ;;
esac
done
rcon-command () {
HOST="$(echo "$1" | cut -d: -f1)"
PORT="$(echo "$1" | cut -d: -f2)"
PASSWORD="$(echo "$1" | cut -d: -f3-)"
COMMAND="$2"
reverse-hex-endian () {
# Given a 4-byte hex integer, reverse endianness
while read -r -d '' -N 8 INTEGER; do
echo "$INTEGER" | sed -E 's/(..)(..)(..)(..)/\4\3\2\1/'
done
}
decode-hex-int () {
# decode little-endian hex integer
while read -r -d '' -N 8 INTEGER; do
BIG_ENDIAN_HEX=$(echo "$INTEGER" | reverse-hex-endian)
echo "$((16#$BIG_ENDIAN_HEX))"
done
}
stream-to-hex () {
xxd -ps
}
hex-to-stream () {
xxd -ps -r
}
encode-int () {
# Encode an integer as 4 bytes in little endian and return as hex
INT="$1"
# Source: https://stackoverflow.com/a/9955198
printf "%08x" "$INT" | sed -E 's/(..)(..)(..)(..)/\4\3\2\1/'
}
encode () {
# Encode a packet type and payload for the rcon protocol
TYPE="$1"
PAYLOAD="$2"
REQUEST_ID="$3"
PAYLOAD_LENGTH="${#PAYLOAD}"
TOTAL_LENGTH="$((4 + 4 + PAYLOAD_LENGTH + 1 + 1))"
OUTPUT=""
OUTPUT+=$(encode-int "$TOTAL_LENGTH")
OUTPUT+=$(encode-int "$REQUEST_ID")
OUTPUT+=$(encode-int "$TYPE")
OUTPUT+=$(echo -n "$PAYLOAD" | stream-to-hex)
OUTPUT+="0000"
echo -n "$OUTPUT" | hex-to-stream
}
read-response () {
# read next response packet and return the payload text
HEX_LENGTH=$(head -c4 <&3 | stream-to-hex | reverse-hex-endian)
LENGTH=$((16#$HEX_LENGTH))
RESPONSE_PAYLOAD=$(head -c $LENGTH <&3 | stream-to-hex)
echo -n "$RESPONSE_PAYLOAD"
}
response-request-id () {
echo -n "${1:0:8}" | decode-hex-int
}
response-type () {
echo -n "${1:8:8}" | decode-hex-int
}
response-payload () {
echo -n "${1:16:-4}" | hex-to-stream
}
login () {
PASSWORD="$1"
encode 3 "$PASSWORD" 12 >&3
RESPONSE=$(read-response "$IN_PIPE")
RESPONSE_REQUEST_ID=$(response-request-id "$RESPONSE")
if [[ "$RESPONSE_REQUEST_ID" == "-1" ]] || [[ "$RESPONSE_REQUEST_ID" == "4294967295" ]]; then
log-warning "RCON connection failed: Wrong RCON password" 1>&2
return 1
fi
}
run-command () {
COMMAND="$1"
# encode 2 "$COMMAND" 13 >> "$OUT_PIPE"
encode 2 "$COMMAND" 13 >&3
RESPONSE=$(read-response "$IN_PIPE")
response-payload "$RESPONSE"
}
# Open a TCP socket
# Source: https://www.xmodulo.com/tcp-udp-socket-bash-shell.html
if ! exec 3<>/dev/tcp/"$HOST"/"$PORT"; then
log-warning "RCON connection failed: Could not connect to $HOST:$PORT"
return 1
fi
login "$PASSWORD" || return 1
debug-log "$(run-command "$COMMAND")"
# Close the socket
exec 3<&-
exec 3>&-
}
if ! "$DEBUG"; then
QUIET="-q"
else
QUIET=""
fi
if [[ "$COMPRESSION_FILE_EXTENSION" == "." ]] || [[ "$COMPRESSION_ALGORITHM" == "none" ]]; then
COMPRESSION_FILE_EXTENSION=""
fi
# Check for missing encouraged arguments
if ! $SUPPRESS_WARNINGS; then
if [[ "$SCREEN_NAME" == "" ]]; then
log-warning "Minecraft screen/tmux/rcon location not specified (use -s)"
fi
fi
# Check for required arguments
MISSING_CONFIGURATION=false
if [[ "${#SERVER_WORLDS[@]}" == "0" ]]; then
log-fatal "Server world not specified (use -i)"
MISSING_CONFIGURATION=true
fi
if [[ "$BACKUP_DIRECTORY" == "" ]] && [[ "$RESTIC_REPO" == "" ]]; then
log-fatal "Backup location not specified (use -o or -r)"
MISSING_CONFIGURATION=true
fi
if [[ "$RESTIC_REPO" != "" ]]; then
if [[ "$BACKUP_DIRECTORY" != "" ]]; then
log-fatal "Both output directory (-o) and restic repo (-r) specified but only one may be used at a time"
MISSING_CONFIGURATION=true
fi
if [[ $MAX_BACKUPS -ge 0 ]] && [[ $MAX_BACKUPS -lt 70 ]] && [[ $DELETE_METHOD == "thin" ]]; then
log-fatal "Thinning delete with restic requires at least 70 snapshots to be kept. If you need to keep fewer than 70, use sequential delete."
MISSING_CONFIGURATION=true
fi
fi
if $MISSING_CONFIGURATION; then
exit 1
fi
if [[ "$BACKUP_DIRECTORY" != "" ]]; then
ARCHIVE_FILE_NAME="$TIMESTAMP.tar$COMPRESSION_FILE_EXTENSION"
ARCHIVE_PATH="$BACKUP_DIRECTORY/$ARCHIVE_FILE_NAME"
fi
if [[ "$RESTIC_REPO" != "" ]]; then
ARCHIVE_PATH="$RESTIC_REPO $TIMESTAMP"
fi
# Minecraft server screen interface functions
message-players () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
message-players-color "$MESSAGE" "$HOVER_MESSAGE" "gray"
}
execute-command () {
local COMMAND=$1
if [[ $SCREEN_NAME != "" ]]; then
case $WINDOW_MANAGER in
"screen") screen -S "$SCREEN_NAME" -p 0 -X stuff "$COMMAND$(printf \\r)"
;;
"tmux") tmux send-keys -t "$SCREEN_NAME" "$COMMAND" ENTER
;;
"RCON"|"rcon") rcon-command "$SCREEN_NAME" "$COMMAND"
;;
esac
fi
}
message-players-error () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
message-players-color "$MESSAGE" "$HOVER_MESSAGE" "red"
}
message-players-success () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
message-players-color "$MESSAGE" "$HOVER_MESSAGE" "green"
}
message-players-color () {
local MESSAGE=$1
local HOVER_MESSAGE=$2
local COLOR=$3
debug-log "$MESSAGE ($HOVER_MESSAGE)"
if $ENABLE_CHAT_MESSAGES; then
execute-command "tellraw @a [\"\",{\"text\":\"[$PREFIX] \",\"color\":\"gray\",\"italic\":true},{\"text\":\"$MESSAGE\",\"color\":\"$COLOR\",\"italic\":true,\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[{\"text\":\"$HOVER_MESSAGE\"}]}}}]"
fi
}
# Parse file timestamp to one readable by "date"
parse-file-timestamp () {
local DATE_STRING
DATE_STRING="$(echo "$1" | awk -F_ '{gsub(/-/,":",$2); print $1" "$2}')"
echo "$DATE_STRING"
}
# Delete a backup
delete-backup () {
local BACKUP=$1
rm "$BACKUP_DIRECTORY"/"$BACKUP"
message-players "Deleted old backup" "$BACKUP"
}
# Sequential delete method
delete-sequentially () {
local BACKUPS=("$BACKUP_DIRECTORY"/*) # List oldest first
while [[ $MAX_BACKUPS -ge 0 && ${#BACKUPS[@]} -gt $MAX_BACKUPS ]]; do
delete-backup "$(basename "${BACKUPS[0]}")"
BACKUPS=("$BACKUP_DIRECTORY"/*)
done
}
# Functions to sort backups into correct categories based on timestamps
is-hourly-backup () {
local TIMESTAMP=$*
local MINUTE
MINUTE=$(date -d "$TIMESTAMP" +%M)
return "$MINUTE"
}
is-daily-backup () {
local TIMESTAMP=$*
local HOUR
HOUR=$(date -d "$TIMESTAMP" +%H)
return "$HOUR"
}
is-weekly-backup () {
local TIMESTAMP=$*
local DAY
DAY=$(date -d "$TIMESTAMP" +%u)
return "$((DAY - 1))"
}
# Helper function to sum an array
array-sum () {
SUM=0
for NUMBER in "$@"; do
(( SUM += NUMBER ))
done
echo "$SUM"
}
# Given two exit codes, print a nonzero one if there is one
exit-code () {
if [[ "$1" != "0" ]]; then
echo "$1"
else
if [[ "$2" == "" ]]; then
echo 0
else
echo "$2"
fi
fi
}
# Thinning delete method
delete-thinning () {
# sub-hourly, hourly, daily, weekly is everything else
local BLOCK_SIZES=(16 24 30)
# First block is unconditional
# The next blocks will only accept files whose names cause these functions to return true (0)
local BLOCK_FUNCTIONS=("is-hourly-backup" "is-daily-backup" "is-weekly-backup")
# Warn if $MAX_BACKUPS does not have enough room for all the blocks
TOTAL_BLOCK_SIZE=$(array-sum "${BLOCK_SIZES[@]}")
if [[ $MAX_BACKUPS != -1 ]] && [[ $TOTAL_BLOCK_SIZE -gt $MAX_BACKUPS ]]; then
if ! $SUPPRESS_WARNINGS; then
log-warning "MAX_BACKUPS ($MAX_BACKUPS) is smaller than TOTAL_BLOCK_SIZE ($TOTAL_BLOCK_SIZE)"
fi
fi
local CURRENT_INDEX=0
local BACKUPS=("$BACKUP_DIRECTORY"/*) # Oldest first
local NUM_BACKUPS="${#BACKUPS[@]}"
for BLOCK_INDEX in "${!BLOCK_SIZES[@]}"; do
local BLOCK_SIZE=${BLOCK_SIZES[BLOCK_INDEX]}
local BLOCK_FUNCTION=${BLOCK_FUNCTIONS[BLOCK_INDEX]}
local OLDEST_BACKUP_IN_BLOCK_INDEX=$((NUM_BACKUPS - 1 - (BLOCK_SIZE + CURRENT_INDEX))) # Not an off-by-one error because a new backup was already saved
if [ "$OLDEST_BACKUP_IN_BLOCK_INDEX" -lt 0 ]; then
break;
fi
local OLDEST_BACKUP_IN_BLOCK
OLDEST_BACKUP_IN_BLOCK="$(basename "${BACKUPS[OLDEST_BACKUP_IN_BLOCK_INDEX]}")"
local OLDEST_BACKUP_TIMESTAMP
OLDEST_BACKUP_TIMESTAMP=$(parse-file-timestamp "${OLDEST_BACKUP_IN_BLOCK:0:19}")
local BLOCK_COMMAND="$BLOCK_FUNCTION $OLDEST_BACKUP_TIMESTAMP"
if $BLOCK_COMMAND; then
# Oldest backup in this block satisfies the condition for placement in the next block
debug-log "$OLDEST_BACKUP_IN_BLOCK promoted to next block"
else
# Oldest backup in this block does not satisfy the condition for placement in next block
delete-backup "$OLDEST_BACKUP_IN_BLOCK"
break
fi
((CURRENT_INDEX += BLOCK_SIZE))
done
delete-sequentially
}
delete-restic-sequential () {
if [ "$MAX_BACKUPS" -ge 0 ]; then
restic forget -r "$RESTIC_REPO" --keep-last "$MAX_BACKUPS" "$QUIET"
fi
}
delete-restic-thinning () {
if [ "$MAX_BACKUPS" -ge 70 ]; then
# MAX_BACKUPS >= 70
restic forget -r "$RESTIC_REPO" --keep-last 16 --keep-hourly 24 --keep-daily 30 --keep-weekly $((MAX_BACKUPS - 70)) "$QUIET"
else
# We have a check that MAX_BACKUPS is not 70 > MAX_BACKUPS >= 0, so we can assume here it is negative
# Negative means don't delete old snapshots
restic forget -r "$RESTIC_REPO" --keep-last 16 --keep-hourly 24 --keep-daily 30 --keep-weekly 9999999 "$QUIET"
fi
}
# Delete old backups
delete-old-backups () {
if [[ "$BACKUP_DIRECTORY" != "" ]]; then
case $DELETE_METHOD in
"sequential") delete-sequentially
;;
"thin") delete-thinning
;;
esac
fi
if [[ "$RESTIC_REPO" != "" ]]; then
case $DELETE_METHOD in
"sequential") delete-restic-sequential
;;
"thin") delete-restic-thinning
;;
esac
fi
}
clean-up () {
# Re-enable world autosaving
execute-command "save-on"
# Save the world
execute-command "save-all"
TIME_DELTA=$((END_TIME - START_TIME))
if [[ "$BACKUP_DIRECTORY" != "" ]]; then
WORLD_SIZE_BYTES=$(du --bytes --total --max-depth=0 "${SERVER_WORLDS[@]}" | tail -n 1 | awk '{print $1}')
ARCHIVE_SIZE_BYTES=$(du -b "$ARCHIVE_PATH" | awk '{print $1}')
ARCHIVE_SIZE=$(du -h "$ARCHIVE_PATH" | awk '{print $1}')
BACKUP_DIRECTORY_SIZE=$(du -h --max-depth=0 "$BACKUP_DIRECTORY" | awk '{print $1}')
# Check that archive size is not null and at least 200 Bytes
if [[ "$ARCHIVE_EXIT_CODE" == "0" && "$WORLD_SIZE_BYTES" -gt 0 && "$ARCHIVE_SIZE" != "" && "$ARCHIVE_SIZE_BYTES" -gt 200 ]]; then
# Notify players of completion
COMPRESSION_PERCENT=$((ARCHIVE_SIZE_BYTES * 100 / WORLD_SIZE_BYTES))
message-players-success "Backup complete!" "$TIME_DELTA s, $ARCHIVE_SIZE/$BACKUP_DIRECTORY_SIZE, $COMPRESSION_PERCENT%"
delete-old-backups
exit 0
else
rm "$ARCHIVE_PATH" # Delete bad archive so we can't fill up with bad archives
message-players-error "Backup was not saved!" "Please notify an administrator"
exit 1
fi
fi
if [[ "$RESTIC_REPO" != "" ]]; then
if [[ "$ARCHIVE_EXIT_CODE" == "0" ]]; then
message-players-success "Backup complete!" "$TIME_DELTA s"
delete-old-backups
exit 0
else
message-players-error "Backup was not saved!" "Please notify an administrator"
exit 1
fi
fi
}
trap "clean-up" 2
do-backup () {
# Notify players of start
message-players "Starting backup..." "$ARCHIVE_PATH"
# Disable world autosaving
execute-command "save-off"
# Backup world
START_TIME=$(date +"%s")
if [[ "$BACKUP_DIRECTORY" != "" ]]; then
# Ensure backup directory exists
mkdir -p "$(dirname "$ARCHIVE_PATH")"
case $COMPRESSION_ALGORITHM in
# No compression
"none") tar -cf "$ARCHIVE_PATH" "${SERVER_WORLDS[@]}"
;;
# With compression
*) tar -cf - "${SERVER_WORLDS[@]}" | $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
# 0 = successful, 1 = some files differ, 2 = fatal
if [ "${EXIT_CODES[0]}" == "1" ]; then
log-warning "Some files may differ in the backup archive (file changed as read)"
TAR_EXIT_CODE="0"
else
TAR_EXIT_CODE="${EXIT_CODES[0]}"
fi
ARCHIVE_EXIT_CODE="$(exit-code "$TAR_EXIT_CODE" "${EXIT_CODES[1]}")"
if [ "$ARCHIVE_EXIT_CODE" -ne 0 ]; then
log-fatal "Archive command exited with nonzero exit code $ARCHIVE_EXIT_CODE"
fi
fi
if [[ "$RESTIC_REPO" != "" ]]; then
RESTIC_TIMESTAMP="${TIMESTAMP:0:10} ${TIMESTAMP:11:2}:${TIMESTAMP:14:2}:${TIMESTAMP:17:2}"
if [[ "$RESTIC_HOSTNAME" == "" ]]; then
RESTIC_HOSTNAME_OPTION=()
else
RESTIC_HOSTNAME_OPTION=("--host" "$RESTIC_HOSTNAME")
fi
restic backup -r "$RESTIC_REPO" "${SERVER_WORLDS[@]}" --time "$RESTIC_TIMESTAMP" "$QUIET" "${RESTIC_HOSTNAME_OPTION[@]}"
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