-
Notifications
You must be signed in to change notification settings - Fork 1
/
rsyncshot
executable file
·188 lines (149 loc) · 5.64 KB
/
rsyncshot
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
#!/usr/bin/env bash
# rsyncshot
# convenient backups using rsync and hard links
# Craig Jennings <[email protected]>
# Inspired by Mike Rubel: http://www.mikerubel.org/computers/rsync_snapshots/
# requirements: bash, rsync, flock, cron, grep
# - unix filesystem capable of hard links at destination
# - core unix utilities: rm, mv, cp, touch
# debugging: uncomment next 4 lines for debugging output
# exec 5> >(logger -t $0)
# BASH_XTRACEFD="5"
# PS4='$LINENO: '
# set -x
# default locations for setup
# modify MOUNTDIR to point to the mount point of your backup
# ---------------------------- Constants ----------------------------
MOUNTDIR=/media/backup;
SCRIPTLOC=/usr/local/bin/rsyncshot;
DESTINATION=$MOUNTDIR/$HOSTNAME
INSTALLHOME=/etc/rsyncshot
LOGFILE=/var/log/rsyncshot.log;
INCLUDES="$INSTALLHOME/include.txt";
EXCLUDES="$INSTALLHOME/exclude.txt";
# copy, move, and rm commands have been aliased by the user or distro
# creator to require confirmation on certain actions.
# using variables allows us to sidestep any alias
CP="/usr/bin/cp"
MV="/usr/bin/mv"
RM="/usr/bin/rm"
# prevent overlapping runs with flock
FLOCKCHECK="flock -x /tmp/rsyncshot.lock -c"
# default cron job entries
CRON_H="0 1-23 * * * "; # hourly on minute 0 from 1am to 11pm
CRON_D="0 12 * * 1-6 "; # daily at noon, monday - saturday
CRON_W="0 12 * * 7 "; # weekly at noon on sundays
# ------------------------ Utility Functions ------------------------
help()
{
printf "\nrsyncshot - compact snapshots on Linux using rsync and hard links.\n\n"
printf "Usage:\nrsyncshot <name> <number of backups to retain>\n"
printf " setup (installs rsyncshot and cron jobs)\n"
printf " help (prints this info)\n"
printf "Notes:\n"
printf '%s\n' "- rsyncshot must be run as root"
printf '%s\n\n' "- install and log locations defined in script."
}
error()
{
echo "ERROR: $0:" "$@" 1>&2;
echo "See \"rsyncshot help\" for usage."
exit 1;
}
setup()
{
# copy this file to directory on path and make executable
$CP -f "$0" "$SCRIPTLOC"
sudo chmod +x "$SCRIPTLOC"
echo "$0 copied to $SCRIPTLOC and made executable"
# make install home if it doesn't exist;
if [ ! -d $INSTALLHOME ]; then
mkdir -p $INSTALLHOME;
echo "Created install home at $INSTALLHOME";
fi
# create includes file and add default entries
if [ -f $INCLUDES ]; then $RM $INCLUDES; fi
printf "/home /etc /usr/local/bin" >> $INCLUDES;
echo "modify include file at $INCLUDES";
# create excludes file and add default entries
if [ -f $EXCLUDES ]; then $RM $EXCLUDES; fi
printf "*.pyc\n*.pyo\n*.class\n*.elc\n*.o\n*.tmp\n.cache*" >> $EXCLUDES;
echo "modify exclude file at $EXCLUDES";
# write out current crontab, append default entries, and install
touch "$LOGFILE"
crontab -l > crontemp;
{
echo "$CRON_H $FLOCKCHECK '$SCRIPTLOC hourly 22 >> $LOGFILE 2>&1'"
echo "$CRON_D $FLOCKCHECK '$SCRIPTLOC daily 6 >> $LOGFILE 2>&1'"
echo "$CRON_W $FLOCKCHECK '$SCRIPTLOC weekly 51 >> $LOGFILE 2>&1'"
} >> crontemp
crontab crontemp;
$RM crontemp;
echo "hourly, daily, and weekly cron jobs installed.";
}
# ----------------------------- Script ----------------------------
# uppercase for case-insensitivity
TYPE=$(tr '[a-z]' '[A-Z]' <<< $1);
if [ "$TYPE" = "HELP" ]; then help; exit; fi
# ensure we're running as root
if [ "$EUID" -ne 0 ]; then error "This script must be run as root."; fi
# display start information
echo "rsyncshot invoked on $(date -u) with: $0 $1 $2";
# if logfile was removed, recreate it.
[ ! -f "$LOGFILE" ] || touch "$LOGFILE"
# validate backup type
# first argument must be alpha characters
if ! [[ $1 =~ [a-zA-Z] ]]; then error "snapshot type not recognized."; fi
if [ "$TYPE" = "SETUP" ]; then setup; exit; fi
# validate max snapshots
# second argument must be numeric
if ! [[ $2 =~ [0-9] ]]; then error "max snapshots not a number."; fi
MAX=$(($2-1));
# validate include file (source directories) exist
# validates the include file exists, and checks the file contents are valid directories
if [ ! -f "$INCLUDES" ]; then error "include file $INCLUDES not found."; fi
SOURCES=$(<$INCLUDES);
for SOURCE in $SOURCES
do
if [ ! -d "$SOURCE" ]; then error "source $SOURCE not found"; fi
done
# validate exclude file (exclusion patterns) exist
if [ ! -f "$EXCLUDES" ]; then error "Exclude file $EXCLUDES not found."; fi
[ -d $MOUNTDIR ] || error "$MOUNTDIR doesn't exist."
# if destination filesystem not mounted attempt mounting; error if attempt fails
if ! grep -qs "$MOUNTDIR" /proc/mounts >> /dev/null 2>&1; then
mount "$MOUNTDIR" >> /dev/null 2>&1
if ! grep -qs "$MOUNTDIR" /proc/mounts >> /dev/null 2>&1; then
error "$MOUNTDIR not mounted and mount attempt failed."
fi
fi
[ -d "$DESTINATION" ] || mkdir "$DESTINATION" || \
error "$DESTINATION doesn't exist, and attempt to create directory failed."
# sync each backup directory in turn
for SOURCE in $SOURCES
do
rsync -avh -i --times \
--delete --delete-excluded \
--exclude-from="$EXCLUDES" \
--update "$SOURCE" "$DESTINATION"/latest ;
done
# delete max+1 snapshot if it exists
if [ -d "$DESTINATION"/"$TYPE"."$MAX" ]; then
$RM -rf "$DESTINATION"/"$TYPE"."$MAX";
fi
# rotate remaining snapshots descending
for (( start=$((MAX)); start>=0; start--)); do
end=$((start+1));
if [ -d "$DESTINATION"/"$TYPE".$start ]; then
$MV "$DESTINATION"/"$TYPE".$start "$DESTINATION"/"$TYPE".$end;
fi
done
# reset directory timestamp
touch "$DESTINATION"/latest
# hard link / copy to destination
$CP -al "$DESTINATION"/latest "$DESTINATION"/"$TYPE".0;
# make directory type read-only
chmod -w "$DESTINATION"/"$TYPE".0
# print time and exit
echo "rsyncshot completed $(date -u) ";
exit 0;