forked from bingobongo/server-backup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup.sh
More file actions
executable file
·418 lines (343 loc) · 11.9 KB
/
Copy pathbackup.sh
File metadata and controls
executable file
·418 lines (343 loc) · 11.9 KB
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
#!/usr/bin/bash
#
# Automated, rotating server backup to Strongspace
#
# @version 0.2
# @author Markus Mayer
# @license MIT License
# @copyright 2012- Markus Mayer
# @link http://doogvaard.net/server-backup
# CONFIGURATION SECTION
# ---------------------
# The email address to which error messages are sent.
# Leave it blank to turn it off.
RECIPIENT="name@domain.tld"
# The pathname of the directory where the exclude-text-files
# are stored and where to store backups, log, etc.
# For example "/root/backup" or "/home/username/backup"
# or similar, whereat on a TextDrive Shared it probably
# would be "/users/home/username/backup". If its name
# is different from "backup", align the patterns inside
# "exclude-tar.txt" and, if not omitted, "exclude-rsync.txt"
# accordingly.
DESTINATION="/pathname/backup"
# A space-separated list of directory/file pathnames
# to backup. On a TextDrive Shared this might be
# "/users/home/username" or a subdirectory/file thereof.
PATHNAMES="/pathname/directory /pathname/file.ext"
# How many days to keep database dumps and tarballs for;
# e.g. for a backup once a week and keeping eight backups
# on the disk, set this to 56.
MTIME=56
# The MySQL user name, password and a space-separated
# list of MySQL database names to backup; optionally.
MYSQL_USER="db-username"
MYSQL_PASSWD="db-password"
MYSQL_NAMES="db-name-1 db-name-2"
# The SSH alias name used for the connection to Strongspace,
# i.e. the before defined Host in ~/.ssh/config file.
SS_ALIAS="ss"
# The pathname of the base directory on Strongspace,
# i.e. the pathname of the Strongspace Space.
SS_BASE="/strongspace/username/space-name"
# A space-separated list of subdirectory names where to
# store and rotate the backup in the Strongspace Space.
# Note that the subdirectories must have been created
# manually before running the backup the first time.
SS_DIRS="subdir-name-1 subdir-name-2"
# The prefix of the backup destination mark for superb
# readability and the basename used for naming database
# dumps, tarballs. Can be left as is.
SS_PREFIX="_recently_synced_with_"
TIMESTAMP=$(date "+%Y%m%d_%H%M%S")
# -----------------
# CONFIGURATION END
# DO NOT CHANGE TOO MUCH BELOW HERE,
# UNLESS ONE IS DOING SOMETHING CUSTOM.
# -------------------------------------
# Note that the pathes to some utilities might
# differ depending on the respective server.
# Make sure that the correct pathes are noted.
# This is particularly true for rsync, tar, mysqldump.
# Sets user name and log path for safty's sake.
[[ -n ${USERNAME} ]] || USERNAME="$(id -un)"
[[ -d ${DESTINATION} ]] && LOGFILE="${DESTINATION}/backup.log" || LOGFILE=/dev/null
usage()
{
local bold=$(tput bold)
local normal=$(tput sgr0)
local NAMED=${bold}${0##*/}${normal}
echo "\
Usage: ${NAMED} [-h]
${NAMED} [-vlLU]"
exit
}
log()
{
echo -e "${1}"
if [[ -n ${2} ]]; then
[[ ${2} -gt 0 && ! $VERBOSE ]] && notify
cat << EOF
$(date "+%F %T")
* -------------------
EOF
exit "${2}"
fi
return 0
}
notify()
{
# Verifies postfix is running and recipient email specified.
# Note that svcs is available on Illumos/SmartOS/Solaris only.
# Instead, try e.g. service and adjust the pipe accordingly.
if [[ $(svcs -v | grep -cEe "postfix") -ne 0 && \
-n ${RECIPIENT} && ${RECIPIENT} != "name@domain.tld" ]]
then
[[ -n ${HOSTNAME} ]] || HOSTNAME="$(hostname)"
local SERVERNAME=${HOSTNAME%%.*}
rmail ${RECIPIENT} << EOF
from: ${USERNAME^} <noreply@${HOSTNAME}>
subject: ${SERVERNAME^} backup failed.
Read the log file '${LOGFILE}' for details.
EOF
[[ $? -eq 0 ]] && \
log " Notified '${RECIPIENT%.*}####'." || \
log " Failed to notify '${RECIPIENT%.*}####'."
fi
}
backup_mysql()
{
[[ -n ${1} ]] || return 1
if mkdir -m 700 -p ${DESTINATION}/mysql/${1}; then
local ERROR=$({ /opt/local/bin/mysqldump --user=${MYSQL_USER} --password=${MYSQL_PASSWD} \
-Q --database ${1} | gzip > ${DESTINATION}/mysql/${1}/${TIMESTAMP}.sql.gz; } 2>&1 )
if [[ -z ${ERROR} ]]; then
log "* Dumped MySQL database '${1}' to 'mysql/${1}/${TIMESTAMP}.sql.gz'."
remove_outdated ${DESTINATION}/mysql/${1}/*.sql.gz
return 0
else
rm -f ${DESTINATION}/mysql/${1}/${TIMESTAMP}.sql.gz
ERROR="\n ${ERROR}"
fi
fi
log "* ERROR - Failed to backup MySQL database '${1}'.${ERROR}"; return 2
}
backup_pathname()
{
[[ -n ${1} ]] || return 1
local FLAT=${1//\//_}; FLAT=${FLAT#_}
if mkdir -m 700 -p ${DESTINATION}/tar/${FLAT} && \
/opt/local/bin/gtar -pszcf ${DESTINATION}/tar/${FLAT}/${TIMESTAMP}.tar.gz \
--exclude-from ${DESTINATION}/exclude-tar.txt --warning none -C / ${1#/}
then
log "* Dumped '${1}' to 'tar/${FLAT}/${TIMESTAMP}.tar.gz'."
else
log "* ERROR - Failed to backup '${1}'."; return 2
fi
if [[ ! $LOCALONLY && -d ${1} && ! -d ${DESTINATION}/data/${FLAT} ]]; then
mkdir -m 700 -p ${DESTINATION}/data/${FLAT} && \
log " Created placeholder 'data/${FLAT}'." || \
log " WARNING - Failed to create placeholder for directory '${1}'."
fi
remove_outdated ${DESTINATION}/tar/${FLAT}/*.tar.gz
}
remove_outdated()
{
[[ -n ${1} ]] || return 1
local OUTDATED=$(find ${1} -mtime +${MTIME})
if [[ -n ${OUTDATED} ]]; then
rm -f ${OUTDATED} && log " Removed outdated dumps." || \
log " WARNING - Failed to remove outdated dumps."
fi
}
remove_unlisted()
{
[[ -n ${1} && -d ${DESTINATION}/${1} && -n ${2} ]] || return 1
local LISTPATH="${DESTINATION}/${1}"
local LIST="${2}"
# Flattens pathnames for comparison.
local FLAT=; local NAME=
for PATHNAME in ${LIST}; do
FLAT=${PATHNAME//\//_}; FLAT=${FLAT#_}
NAME="${NAME} ${FLAT}"
done
LIST=${NAME# }
for PATHNAME in ${LISTPATH}/*/; do
PATHNAME=${PATHNAME%*/}
NAME="${PATHNAME##*/}"
[[ ${NAME} == "*" ]] && continue
is_listed ${NAME} "${LIST}"
if [[ $? -gt 1 ]]; then
rm -rf ${PATHNAME} && log "* Removed unlisted '${1}/${NAME}'." || \
log "* WARNING - Failed to remove unlisted '${1}/${NAME}'."
fi
done
}
is_listed()
{
[[ -n ${1} && -n ${2} ]] || return 1
for LISTED in ${2}; do
[[ ${LISTED} == ${1} ]] && return
done
return 2
}
sync_pathname()
{
[[ -n ${1} && -d ${1} ]] || return 1
local FLAT=${1//\//_}; FLAT=${FLAT#_}
[[ -d ${DESTINATION}/data/${FLAT} ]] || \
{ log "* ERROR - No placeholder found for '${1}', synchronization abandoned."; return 2; }
/opt/local/bin/rsync ${RSYNC_OPTION} --delete --delete-after \
${EXCLUDE_FROM_RSYNC} ${1}/ ${SS_ALIAS}:${SS_PATH}/data/${FLAT} && \
log "* Synced directory '${1}'." || \
{ log "* ERROR - Failed to synchronize directory '${1}'."; return 2; }
}
run_backup()
{
local TEMP=
# Empties the log file.
[[ $LOGRESET && -f "${LOGFILE}" ]] && $(> "${LOGFILE}") && log "* Emptied log file '${LOGFILE}'."
# Sets rsync options corresponding to user name.
RSYNC_OPTION="-azq"
[[ ${USERNAME} == "root" ]] || { echo "
* WARNING - Not running as root, but as ${USERNAME}.
rsync executes with options '-rlptzq' instead of '-azq'."; RSYNC_OPTION="-rlptzq"; }
# Verifies destination path.
[[ -e ${DESTINATION} ]] || \
log "* ERROR - DESTINATION '${DESTINATION}' doesn't exist." 2
[[ -d ${DESTINATION} ]] || \
log "* ERROR - DESTINATION '${DESTINATION}' is not a directory." 2
# Prevents from configuration bubkis.
if [[ ! $LOCALONLY ]]; then
# Seeks for recently used subdirectory name.
for NAME in ${SS_DIRS}; do
[[ -z ${FUTURE} && ! -f ${DESTINATION}/${SS_PREFIX}${NAME} ]] && FUTURE=${NAME}
if [[ -z ${RECENT} ]]; then
[[ -f ${DESTINATION}/${SS_PREFIX}${NAME} ]] && RECENT=${NAME}
continue
else
FUTURE=${NAME}; break
fi
done
[[ -z ${SS_ALIAS} ]] && \
log "* ERROR - Undefined SS_ALIAS." 2
[[ -z ${SS_BASE} || ${SS_BASE} == "/strongspace/username/space-name" ]] && \
log "* ERROR - SS_BASE '${SS_BASE}' is not advisable." 2
[[ -z ${SS_DIRS} || "${SS_DIRS}" == "subdir-name-1 subdir-name-2" ]] && \
log "* ERROR - SS_DIRS '${SS_DIRS}' is not advisable." 2
[[ -z ${FUTURE} ]] && \
log "* ERROR - Undefined FUTURE, failed to set SS_PATH." 2
[[ -f ${DESTINATION}/exclude-rsync.txt ]] && \
EXCLUDE_FROM_RSYNC="--exclude-from ${DESTINATION}/exclude-rsync.txt"
SS_PATH="${SS_BASE}/${FUTURE}" # Sets full Strongspace pathname.
fi
# Verifies existence of pathnames.
for PATHNAME in ${PATHNAMES}; do
[[ -e ${PATHNAME} ]] && TEMP="${TEMP} ${PATHNAME}" || \
log "* WARNING - '${PATHNAME}' doesn't exist."
done
PATHNAMES=${TEMP# }; TEMP=
# Makes backup of directory, file.
if [[ -n ${PATHNAMES} ]]; then
# Verifies existence of exclude-tar.txt.
[[ -f ${DESTINATION}/exclude-tar.txt ]] || \
log "* ERROR - File '${DESTINATION}/exclude-tar.txt' doesn't exist." 2
for PATHNAME in ${PATHNAMES}; do
backup_pathname ${PATHNAME}
done
else
log "* WARNING - No PATHNAMES found, its backup abandoned."
fi
# Makes backup of database.
if [[ -z "${MYSQL_USER}" || "${MYSQL_USER}" == "db-username" || \
-z "${MYSQL_PASSWD}" || "${MYSQL_PASSWD}" == "db-password" || \
-z "${MYSQL_NAMES}" || "${MYSQL_NAMES}" == "db-name-1 db-name-2" \
]]; then
log "* WARNING - MySQL setup is not advisable, its backup abandoned."
else
for NAME in ${MYSQL_NAMES}; do
backup_mysql ${NAME}
done
fi
# Removes backup of unlisted database, directory, file.
if [[ $DELETE_UNLISTED ]]; then
remove_unlisted "tar" "${PATHNAMES}"
remove_unlisted "data" "${PATHNAMES}"
remove_unlisted "mysql" "${MYSQL_NAMES}"
fi
if [[ ! $LOCALONLY ]]; then
# Synchronizes the backup.
# To test rsync manually with progress and stats (note to replace
# options "-azq" with "-rlptzq" when not running as root), e.g.
# rsync -azv --delete --delete-after --stats --progress /DESTINATION/backup/ ss:/strongspace/USERNAME/SPACE-NAME/SUBDIRECTORY-NAME
/opt/local/bin/rsync ${RSYNC_OPTION} --delete --delete-after ${DESTINATION}/ ${SS_ALIAS}:${SS_PATH} && \
log "* Synced directory '${DESTINATION}'." || \
log "* ERROR - Failed to synchronize directory '${DESTINATION}'." 2
# Remembers the last rotated backup destination.
if [[ -n ${RECENT} && -n ${FUTURE} ]]; then
mv ${DESTINATION}/${SS_PREFIX}${RECENT} ${DESTINATION}/${SS_PREFIX}${FUTURE} && \
log "* Updated destination mark '${SS_PREFIX}${RECENT}' to '${FUTURE}'." || \
log "* ERROR - Failed to update destination mark '${SS_PREFIX}${RECENT}'." 2
else
touch ${DESTINATION}/${SS_PREFIX}${FUTURE} && \
log "* Created destination mark '${SS_PREFIX}${FUTURE}'." || \
log "* ERROR - Failed to create destination mark '${SS_PREFIX}${FUTURE}'." 2
fi
# Pathname synchronization comes last.
if [[ -n ${PATHNAMES} ]]; then
for PATHNAME in ${PATHNAMES}; do
sync_pathname ${PATHNAME}
done
fi
fi
log "* Finished, m'Lord." 0
}
# Checks if verbose mode (writes messages to bash instead of log file,
# also turns off email notification), local-only (without rsync) and
# if the log file shall be emptied and unlisted backup be removed.
while getopts ":vhlLU-:" OPTION; do
case $OPTION in
-)
case "${OPTARG}" in
verbose)
VERBOSE="verbose"
;;
help)
usage
;;
localonly)
LOCALONLY="local-only"
;;
resetlog)
LOGRESET="reset-log"
;;
deleteunlisted)
DELETE_UNLISTED="delete-unlisted"
;;
*)
log "* ERROR - Invalid option '--${OPTARG}'." 2
;;
esac
;;
v)
VERBOSE="verbose"
;;
h)
usage
;;
l)
LOCALONLY="local-only"
;;
L)
LOGRESET="reset-log"
;;
U)
DELETE_UNLISTED="delete-unlisted"
;;
\?)
log "* ERROR - Invalid option '-${OPTARG}'." 2
;;
esac
done
# Runs the backup.
[[ ! $VERBOSE ]] && run_backup 2>&1 >> ${LOGFILE} || run_backup