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