-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
vbackup.sh
executable file
·267 lines (223 loc) · 7.98 KB
/
vbackup.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
#!/usr/bin/env bash
# Bash strict mode
set -euo pipefail
IFS=$'\n\t'
# DEFAULTS
# The path to the source files
export DUPLICACY_REPOSITORY_PATH="${DUPLICACY_REPOSITORY_PATH:-$(pwd -P)}"
# The path to the dotenv file for this script
export DUPLICACY_ENV_FILE="${DUPLICACY_ENV_FILE:-${DUPLICACY_REPOSITORY_PATH}/.duplicacy/.env}"
# Set to 'true' to enable the Volume Shadow Copy service (Windows and macOS using APFS only)
export DUPLICACY_VSS="${DUPLICACY_VSS:-false}"
# Execute the script only when connected to the specified Wi-Fi SSID
export DUPLICACY_SSID="${DUPLICACY_SSID:-}"
# Number of seconds to wait
export DUPLICACY_TIMEOUT="${DUPLICACY_TIMEOUT:-300}"
# Number of uploading threads
export DUPLICACY_THREADS="${DUPLICACY_THREADS:-4}"
# Extra storage (ex: B2)
export DUPLICACY_EXTRA_STORAGE="${DUPLICACY_EXTRA_STORAGE:-}"
# The Slack webhook used for posting messages
export SLACK_ALERTS_WEBHOOK="${SLACK_ALERTS_WEBHOOK:-}"
# The webhook used for healthchecks (HealthChecks.io)
export HEALTHCHECKS_URL="${HEALTHCHECKS_URL:-}"
# Export PATH if needed by scripts that do not load the entire environment
export PATH="${PATH}:/bin:/sbin:/usr/bin:/usr/local/bin:/usr/local/opt/coreutils/libexec/gnubin"
# Log format (ex: log INFO Message)
# Levels: DEBUG, INFO, WARN, ERROR
log(){
local type=${1:?Must specify the type first}; shift
echo "$(date '+%Y-%m-%d %H:%M:%S.%3N') ${type} DUPLICACY_SCRIPT ${*:-}"
}
# Check if command exists
is_cmd(){
command -v "$@" >/dev/null 2>&1
}
# Load .env file
load_dotenv(){
if [[ -s "$DUPLICACY_ENV_FILE" ]]; then
# shellcheck disable=1090
. "$DUPLICACY_ENV_FILE"
fi
}
# Post message to Slack webhook
notify(){
if [[ -n "$SLACK_ALERTS_WEBHOOK" ]]; then
log INFO 'Notify Slack'
/usr/bin/curl --silent --output /dev/null --show-error --fail --request POST \
--header 'Content-type: application/json' \
--data "{\"text\":\"${1:?Must specify the message}\"}" \
"$SLACK_ALERTS_WEBHOOK"
fi
}
# Ensure that the repository is initialized
check_repository_initialized(){
if [[ ! -s "${DUPLICACY_REPOSITORY_PATH}/.duplicacy/preferences" ]]; then
log ERROR 'The repository is not initialized'; exit 1
fi
}
# Check if the process is already running
check_process_running(){
if [ -f "$DUPLICACY_PID_FILE" ]; then
PID=$(cat "$DUPLICACY_PID_FILE")
if ps -p "$PID" >/dev/null 2>&1; then
log WARN 'Process already running'; exit 3
else
if ! echo $$ > "$DUPLICACY_PID_FILE"; then
log ERROR 'Could not create PID file'; exit 1
fi
fi
else
if ! echo $$ > "$DUPLICACY_PID_FILE"; then
log ERROR 'Could not create PID file'; exit 1
fi
fi
}
# Execute the script only when connected to the specified Wi-Fi SSID
check_ssid(){
if [[ -z "$DUPLICACY_SSID" ]]; then return; fi
if [[ ! -x /System/Library/PrivateFrameworks/Apple80211.framework/Resources/airport ]]; then return; fi
if [[ "$(/System/Library/PrivateFrameworks/Apple80211.framework/Resources/airport -I | awk -F': ' '/AirPort/{print $2}')" == 'Off' ]]; then
log INFO 'The backup will be skipped because Wi-Fi is Off (probably sleeping)'; exit 2
elif [[ "$(/System/Library/PrivateFrameworks/Apple80211.framework/Resources/airport -I | awk -F': ' '/ SSID/{print $2}')" != "$DUPLICACY_SSID" ]]; then
log INFO 'The backup will be skipped for the current Wi-Fi SSID'; exit 2
fi
}
# This is here because of tmutil timeout errors (`tmutil localsnapshot` is used for VSS - Shadow Copy)
wait_for_tmutil(){
if ! is_cmd tmutil; then return; fi
until tmutil listlocalsnapshots / >/dev/null 2>&1 || [[ $((DUPLICACY_TIMEOUT--)) == 0 ]]; do
log INFO 'Waiting for tmutil...'; sleep 5
done
if ! tmutil listlocalsnapshots / >/dev/null 2>&1; then
log ERROR 'Tmutil did not respond in a timely manner'; exit 1
fi
}
# Initialize script
do_initialize(){
# Initialize trap
trap 'clean_up $?' EXIT HUP INT QUIT TERM
# Load settings
load_dotenv
# Log everything to file
mkdir -p "$(dirname "$DUPLICACY_LOG_FILE")"
exec > >(tee -a "$DUPLICACY_LOG_FILE") 2>&1
# Sanity checks
check_repository_initialized
check_process_running
check_ssid
# Wait for other processes
wait_for_tmutil
}
# Concatenate command
concatenate_duplicacy_cmd(){
duplicacy_cmd='duplicacy -log backup -stats'
# Use `caffeinate` command if available
if is_cmd caffeinate; then
duplicacy_cmd="caffeinate -s ${duplicacy_cmd}"
fi
# Enable the Volume Shadow Copy service
if [[ "$DUPLICACY_VSS" == 'true' ]]; then
duplicacy_cmd="${duplicacy_cmd} -vss"
fi
# Use the specified extra storage
if [[ -n "$DUPLICACY_EXTRA_STORAGE" ]]; then
duplicacy_cmd="${duplicacy_cmd} -storage ${DUPLICACY_EXTRA_STORAGE}"
fi
}
# Run backup (use caffeinate command if it exists to prevent sleeping on MacOS)
do_backup(){
# The path to the log file for this script
export DUPLICACY_LOG_FILE="${DUPLICACY_REPOSITORY_PATH}/.duplicacy/logs/backup.log"
# The path to the file containing the pid of the running process
export DUPLICACY_PID_FILE="${DUPLICACY_REPOSITORY_PATH}/.duplicacy/running.pid"
# Initialize script
do_initialize
# Concatenate command
concatenate_duplicacy_cmd
# Run
log INFO 'Start backup'
eval "${duplicacy_cmd:-}"
}
# Prune local storage
prune_local_snapshots(){
# -keep <n:m> [+] keep 1 snapshot every n days for snapshots older than m days
# Keep no snapshots older than 1825 days
# Keep 1 snapshot every 30 days if older than 180 days
# Keep 1 snapshot every 7 days if older than 30 days
# Keep 1 snapshot every 1 day if older than 7 days
log INFO 'Prune local snapshots'
duplicacy -log prune -all -keep 0:1825 -keep 30:180 -keep 7:30 -keep 1:7 || notify "Prune local snapshots failed with exit code '$?'. Skipping..."
}
# Prune remote storage
prune_remote_snapshots(){
if [[ -n "$DUPLICACY_EXTRA_STORAGE" ]]; then
log INFO 'Prune remote snapshots'
duplicacy -log prune -all -keep 0:1825 -keep 30:180 -keep 7:30 -keep 1:7 -storage "$DUPLICACY_EXTRA_STORAGE" || notify "Prune remote snapshots failed with exit code '$?'. Skipping..."
fi
}
# Copy to external storage
copy_snapshots(){
if [[ -n "$DUPLICACY_EXTRA_STORAGE" ]]; then
log INFO 'Copy snapshots'
duplicacy -log copy -to "$DUPLICACY_EXTRA_STORAGE" -threads "$DUPLICACY_THREADS" || notify "Copy snapshots failed with exit code '$?'. Skipping..."
fi
}
# Run maintenance
do_maintenance(){
# The path to the log file for this script
export DUPLICACY_LOG_FILE="${DUPLICACY_REPOSITORY_PATH}/.duplicacy/logs/maintenance.log"
# The path to the file containing the pid of the running process
export DUPLICACY_PID_FILE="${DUPLICACY_REPOSITORY_PATH}/.duplicacy/running-maintenance.pid"
# Initialize script
do_initialize
# Copy to external storage
copy_snapshots
# Prune local storage
prune_local_snapshots
# Prune remote storage
prune_remote_snapshots
# Notify HealthChecks.io
if [[ -n "$HEALTHCHECKS_URL" ]]; then
curl --silent --output /dev/null --show-error --fail --retry 3 "$HEALTHCHECKS_URL"
fi
log INFO 'MAINTENANCE_END'
}
# Clean-up and notify
clean_up(){
# Exit codes: 1 - Errors; 2 - Skipped; 3 - Already running
# If it's already running, just clean exit directly
if [[ ${1:-0} == 3 ]]; then return; fi
# Remove the PID file
if [[ -s "$DUPLICACY_PID_FILE" ]]; then rm -f "$DUPLICACY_PID_FILE"; fi
# If it's skipped, just clean exit (after cleaning the PID file)
if [[ ${1:-0} == 2 ]]; then return; fi
# For all other cases notify failure (after cleaning the PID file)
if [[ ${1:-0} != 0 ]]; then
log ERROR "Backup Failed with exit code '${1}'"
notify "Backup Failed on $(hostname) ($(date))"
fi
}
# Script
main(){
# Process command line arguments
local -r cmd="${1:-backup}"; shift
# Go to the repository path
cd "$DUPLICACY_REPOSITORY_PATH" || exit 1
case "$cmd" in
backup)
# Backup
do_backup
;;
maintenance)
# Maintenance
do_maintenance
;;
*)
# Default
log ERROR "'${cmd}' is not recognized as a valid command"; exit 1
;;
esac
}
# Run
main "${@:-}"