5219bef54a32f03072cec111c89b43d14901a62e
[matthijs/upstream/backupninja.git] / handlers / maildir
1 ###############################################################
2 #
3 #  This handler slowly creates a backup of each user's maildir
4 #  to a remote server. It is designed to be run with low overhead
5 #  in terms of cpu and bandwidth so it runs pretty slow.
6 #
7 #  if destdir is /backup/maildir/, then it will contain the files
8 #    daily.1
9 #    daily.2
10 #    daily.3
11 #    weekly.1
12 #    weekly.2
13 #    monthly.1
14 #  if keepdaily is 3, keepweekly is 2, and keepmonthly is 1. 
15 #
16 #  The basic algorithm is to rsync each maildir individually,
17 #  and to use hard links for retaining historical data.
18 #
19 #  We rsync each maildir individually because it becomes very
20 #  unweldy to start a single rsync of many hundreds of thousands
21 #  of files. 
22 #
23 #  For the backup rotation to work, destuser must be able to run 
24 #  arbitrary bash commands on the desthost.
25 #
26 ##############################################################
27
28 getconf rotate yes
29 getconf remove yes
30
31 getconf loadlimit 5
32 getconf speedlimit 0
33 getconf keepdaily 5
34 getconf keepweekly 3
35 getconf keepmonthly 1
36
37 getconf srcdir /var/maildir
38 getconf destdir
39 getconf desthost
40 getconf destport 22
41 getconf destuser
42
43 failedcount=0
44
45 # strip trailing /
46 destdir=${destdir%/}
47 srcdir=${srcdir%/}
48
49 # used for testing
50 #getconf letter
51 #getconf testuser elijah
52 getconf backup yes
53 #letters=e
54 letters="a b c d e f g h i j k l m n o p q r s t u v w x y z"
55
56 [ -d $srcdir ] || fatal "source directory $srcdir doesn't exist"
57
58 [ ! $test ] || testflags="--dry-run -v"
59 rsyncflags="$testflags -e 'ssh -p $destport' -r -v --ignore-existing --delete --size-only --bwlimit=$speedlimit"
60 excludes="--exclude '.Trash/\*' --exclude '.Mistakes/\*' --exclude '.Spam/\*'"
61
62 # see if we can login
63 debug "ssh -o PasswordAuthentication=no $desthost -l $destuser 'echo -n 1'"
64 if [ ! $test ]; then
65         result=`ssh -o PasswordAuthentication=no $desthost -l $destuser 'echo -n 1' 2>&1`
66         if [ "$result" != "1" ]; then
67                 fatal "Can't connect to $desthost as $destuser."
68         fi
69 fi
70
71 ##################################################################
72 ### FUNCTIONS
73
74 function do_user() {
75         local user=$1
76         local destdir=$2
77         local letter=${user:0:1}
78         local dir="$srcdir/$letter/$user"
79         [ -d $dir ] || fatal "maildir $dir not found".
80
81 #       while true; do
82 #               load=`uptime | sed 's/^.*load average: \\([^,]*\\).*$/\\1/'`
83 #               over=`expr $load \> $loadlimit`
84 #               if [ $over == 1 ]; then
85 #                       info "load $load, sleeping..."
86 #                       sleep 600
87 #               else
88 #                       break
89 #               fi
90 #       done
91         
92         cmd="$RSYNC $rsyncflags $excludes $dir $destuser@$desthost:$destdir/$letter"
93         ret=`rsync -e "ssh -p $destport" -r \
94 --links --ignore-existing --delete --size-only --bwlimit=$speedlimit \
95 --exclude '.Trash/*' --exclude '.Mistakes/*' --exclude '.Spam/*' \
96 $dir $destuser@$desthost:$destdir/$letter \
97 2>&1`
98         ret=$?
99         # ignore 0 (success) and 24 (file vanished before it could be copied)
100         if [ $ret != 0 -a $ret != 24 ]; then
101                 warning "rsync $user failed"
102                 warning "  returned: $ret"
103                 let "failedcount = failedcount + 1"
104                 if [ $failedcount -gt 100 ]; then
105                         fatal "100 rsync errors -- something is not working right. bailing out."
106                 fi
107         fi
108 }
109
110 # remove any maildirs from backup which might have been deleted
111 # and add new ones which have just been created.
112
113 function do_remove() {
114         local tmp1=`maketemp maildir-tmp-file`
115         local tmp2=`maketemp maildir-tmp-file`
116         
117         for i in a b c d e f g h i j k l m n o p q r s t u v w x y z; do
118                 ls -1 "$srcdir/$i" | sort > $tmp1
119                 ssh -p $destport $desthost ls -1 '$destdir/maildir/$i' | sort > $tmp2
120                 for deluser in `join -v 2 $tmp1 $tmp2`; do
121                         cmd="ssh -p $destport $desthost rm -vr '$destdir/maildir/$i/$deluser/'"
122                         debug $cmd
123                 done
124         done
125         rm $tmp1
126         rm $tmp2        
127 }
128
129 function do_rotate() {
130         backuproot=$destdir
131
132 (
133         debug Connecting to $desthost
134         ssh -T -o PasswordAuthentication=no $desthost -l $destuser <<EOF
135 ##### BEGIN REMOTE SCRIPT #####
136         seconds_daily=86400
137         seconds_weekly=604800
138         seconds_monthly=2628000
139         keepdaily=$keepdaily
140         keepweekly=$keepweekly
141         keepmonthly=$keepmonthly
142         now=\`date +%s\`
143
144         for rottype in daily weekly monthly; do
145                 seconds=\$((seconds_\${rottype}))
146
147                 dir="$backuproot/\$rottype"
148                 if [ ! -d \$dir.1 ]; then
149                         echo "Info: \$dir.1 does not exist. This backup is missing, so we are skipping the rotation."
150                         continue 1
151                 elif [ ! -f \$dir.1/created ]; then
152                         echo "Warning: \$dir.1/created does not exist. This backup may be only partially completed. Skipping rotation."
153                         continue 1
154                 fi
155                 
156                 # Rotate the current list of backups, if we can.
157                 oldest=\`find $backuproot -type d -maxdepth 1 -name \$rottype'.*' | sed 's/^.*\.//' | sort -n | tail -1\`
158                 echo "Debug: oldest \$oldest"
159                 [ "\$oldest" == "" ] && oldest=0
160                 for (( i=\$oldest; i > 0; i-- )); do
161                         if [ -d \$dir.\$i ]; then
162                                 if [ -f \$dir.\$i/created ]; then
163                                         created=\`tail -1 \$dir.\$i/created\`
164                                 else
165                                         created=0
166                                 fi
167                                 cutoff_time=\$(( now - (seconds*(i-1)) ))
168                                 if [ ! \$created -gt \$cutoff_time ]; then
169                                         next=\$(( i + 1 ))
170                                         if [ ! -d \$dir.\$next ]; then
171                                                 echo "Debug: mv \$dir.\$i \$dir.\$next"
172                                                 mv \$dir.\$i \$dir.\$next
173                                                 date +%c%n%s > \$dir.\$next/rotated
174                                         else
175                                                 echo "Info: skipping rotation of \$dir.\$i because \$dir.\$next already exists."
176                                         fi
177                                 else
178                                         echo "Info: skipping rotation of \$dir.\$i because it was created" \$(( (now-created)/86400)) "days ago ("\$(( (now-cutoff_time)/86400))" needed)."
179                                 fi
180                         fi
181                 done
182         done
183
184         max=\$((keepdaily+1))
185         if [ \( \$keepweekly -gt 0 -a -d $backuproot/daily.\$max \) -a ! -d $backuproot/weekly.1 ]; then
186                 echo mv $backuproot/daily.\$max $backuproot/weekly.1
187                 mv $backuproot/daily.\$max $backuproot/weekly.1
188                 date +%c%n%s > $backuproot/weekly.1/rotated
189         fi
190
191         max=\$((keepweekly+1))
192         if [ \( \$keepmonthly -gt 0 -a -d $backuproot/weekly.\$max \) -a ! -d $backuproot/monthly.1 ]; then
193                 echo mv $backuproot/weekly.\$max $backuproot/monthly.1
194                 mv $backuproot/weekly.\$max $backuproot/monthly.1
195                 date +%c%n%s > $backuproot/monthly.1/rotated
196         fi
197
198         for rottype in daily weekly monthly; do
199                 max=\$((keep\${rottype}+1))
200                 dir="$backuproot/\$rottype"
201                 oldest=\`find $backuproot -type d -maxdepth 1 -name \$rottype'.*' | sed 's/^.*\.//' | sort -n | tail -1\`
202                 [ "\$oldest" == "" ] && oldest=0 
203                 # if we've rotated the last backup off the stack, remove it.
204                 for (( i=\$oldest; i >= \$max; i-- )); do
205                         if [ -d \$dir.\$i ]; then
206                                 if [ -d $backuproot/rotate.tmp ]; then
207                                         echo "Info: removing $backuproot/rotate.tmp"
208                                         rm -rf $backuproot/rotate.tmp
209                                 fi
210                                 echo "Info: moving \$dir.\$i to $backuproot/rotate.tmp"
211                                 mv \$dir.\$i $backuproot/rotate.tmp
212                         fi
213                 done
214         done
215 ####### END REMOTE SCRIPT #######
216 EOF
217 ) | (while read a; do passthru $a; done)
218
219 }
220
221
222 function setup_remote_dirs() {
223         local backuptype=$1
224         local dir="$destdir/$backuptype"
225
226 (
227         ssh -T -o PasswordAuthentication=no $desthost -l $destuser <<EOF
228                 if [ ! -d $destdir ]; then
229                         echo "Fatal: Destination directory $destdir does not exist on host $desthost."
230                         exit 1
231                 elif [ -d $dir.1 ]; then
232                         if [ -f $dir.1/created ]; then
233                                 echo "Warning: $dir.1 already exists. Overwriting contents."
234                         else
235                                 echo "Warning: we seem to be resuming a partially written $dir.1"
236                         fi
237                 else
238                         if [ -d $destdir/rotate.tmp ]; then
239                                 mv $destdir/rotate.tmp $dir.1
240                                 if [ \$? == 1 ]; then
241                                         echo "Fatal: could mv $destdir/rotate.tmp $dir.1 on host $desthost"
242                                         exit 1
243                                 fi
244                         else
245                                 mkdir $dir.1
246                                 if [ \$? == 1 ]; then
247                                         echo "Fatal: could not create directory $dir.1 on host $desthost"
248                                         exit 1
249                                 fi
250                                 for i in a b c d e f g h i j k l m n o p q r s t u v w y x z; do
251                                         mkdir $dir.1/\$i
252                                 done
253                         fi
254                         if [ -d $destdir/$backuptype.2 ]; then
255                                 echo "Info: updating hard links to $dir.1. This may take a while."
256                                 cp -alf $destdir/$backuptype.2/. $dir.1
257                                 #if [ \$? == 1 ]; then
258                                 #       echo "Fatal: could not create hard links to $dir.1 on host $desthost"
259                                 #       exit 1
260                                 #fi
261                         fi
262                 fi
263                 [ -f $dir.1/created ] && rm $dir.1/created
264                 [ -f $dir.1/rotated ] && rm $dir.1/rotated
265                 exit 0
266 EOF
267 ) | (while read a; do passthru $a; done)
268
269         if [ $? == 1 ]; then exit; fi
270 }
271
272 ###
273 ##################################################################
274
275 ### ROTATE BACKUPS ###
276
277 if [ "$rotate" == "yes" ]; then
278         do_rotate
279 fi
280
281 ### REMOVE OLD MAILDIRS ###
282
283 if [ "$remove" == "yes" ]; then
284         debug remove
285 fi
286
287 ### MAKE BACKUPS ###
288
289 if [ "$backup" == "yes" ]; then
290         if [ $keepdaily -gt 0 ]; then btype=daily
291         elif [ $keepweekly -gt 0 ]; then btype=weekly
292         elif [ $keepmonthly -gt 0 ]; then btype=monthly
293         else fatal "keeping no backups"; fi
294
295         setup_remote_dirs $btype
296         
297         for i in $letters; do
298                 [ -d "$srcdir/$i" ] || fatal "directory $srcdir/$i not found."
299                 cd "$srcdir/$i"
300                 debug $i
301                 for user in `ls -1`; do
302                         if [ "$testuser" != "" -a "$testuser" != "$user" ]; then continue; fi
303                         do_user $user $destdir/$btype.1
304                 done
305         done
306
307         ssh -o PasswordAuthentication=no $desthost -l $destuser "date +%c%n%s > $destdir/$btype.1/created"
308 fi