summaryrefslogtreecommitdiff
path: root/rsyncshot
blob: 8887533130bf8ea985ca509ad6fd29bbd9e8aee8 (plain)
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
#!/bin/bash
# rsyncshot
# Craig Jennings craigmartinjennings@gmail.com
# Inspired by Mike Rubel: http://www.mikerubel.org/computers/rsync_snapshots/

# 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 BACKUPLOCATION to point to the mount point of your backup 

MOUNTPOINT=/media/backup;
SCRIPTLOC=/usr/local/bin/rsyncshot;
DESTINATION=$MOUNTPOINT/$HOSTNAME

INSTALLHOME=/etc/rsyncshot
LOGHOME=/var/log/rsyncshot.log;

INCLUDES="$INSTALLHOME/include.txt";
EXCLUDES="$INSTALLHOME/exclude.txt";

# Sidestep Alias Conflicts
# Copy, move, and rm commands are often aliased to require user input. 
# Using a variable allows us to sidestep this and complete the actions without any interaction. 

CP="/usr/bin/cp"
MV="/usr/bin/mv"
RM="/usr/bin/rm"

# Default Cron Job Entries
# CRON_H = hourly on minute 0 from 1am to 11pm
# CRON_D = daily at midnight, Monday - Saturday
# CRON_W = weekly at midnight on Sundays

CRON_H="0 1-23 * * * $SCRIPTLOC hourly 22";
CRON_D="0 0 * * 1-6 $SCRIPTLOC daily 6";
CRON_W="0 0 * * 7 $SCRIPTLOC weekly 51";

# Help Function

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' "- install and log locations defined in script." 
    printf '%s\n'
}

# Error Function

error()
{
    echo "ERROR: $0: $@" 1>&2;
    echo "See \"rsyncshot help\" for usage."
    exit 1;
}

# Setup Function

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 is 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
    crontab -l > crontemp;
    echo "$CRON_H >> $LOGHOME 2>&1" >> crontemp;
    echo "$CRON_D >> $LOGHOME 2>&1" >> crontemp;
    echo "$CRON_W >> $LOGHOME 2>&1">> crontemp;
    crontab crontemp;
    $RM crontemp;
    echo "hourly, daily, and weekly cron jobs installed.";
}

# Display Help If Requested
# Make the argument 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";

# 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

# Validate Mountpoint 
# Fail if mountpoint doesn't exist. 
# Attempt mounting if destination filesystem not mounted; error if attempt fails. 

[ -d $MOUNTPOINT ] || error "$MOUNTPOINT doesn't exist!"

if grep -qs "$MOUNTPOINT" /proc/mounts  ; then
    true
else if  [ $? -eq 0 ]; then 
         true
     else
         error "$MOUNTPOINT unmounted, and mount attempt failed."
     fi
fi

# Validate Destination Directory Exists

[ -d $DESTINATION ] || mkdir $DESTINATION || error "$DESTINATION doesn't exist, and attempt to create 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

# If Exists, Delete Max+1 Snapshot

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 Only 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;