Add a vim modeline with indentation settings.
[matthijs/upstream/backupninja.git] / handlers / maildir.in
1 # -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
2 # vim: set filetype=sh sw=3 sts=3 expandtab autoindent:
3
4 ###############################################################
5 #
6 #  This handler slowly creates a backup of each user's maildir
7 #  to a remote server. It is designed to be run with low overhead
8 #  in terms of cpu and bandwidth so it runs pretty slow.
9 #  Hardlinking is used to save storage space.
10 #
11 #  This handler expects that your maildir directory structure is
12 #  either one of the following:
13 #
14 #  1. /$srcdir/[a-zA-Z0-9]/$user for example:
15 #  /var/maildir/a/anarchist
16 #  /var/maildir/a/arthur
17 #  ...
18 #  /var/maildir/Z/Zaphod
19 #  /var/maildir/Z/Zebra
20 #
21 #  2. or the following:
22 #  /var/maildir/domain.org/user1
23 #  /var/maildir/domain.org/user2
24 #  ...
25 #  /var/maildir/anotherdomain.org/user1
26 #  /var/maildir/anotherdomain.org/user2
27 #  ...
28 #
29 #  if the configuration is setup to have keepdaily at 3,
30 #  keepweekly is 2, and keepmonthly is 1, then each user's
31 #  maildir backup snapshot directory will contain these files:
32 #    daily.1
33 #    daily.2
34 #    daily.3
35 #    weekly.1
36 #    weekly.2
37 #    monthly.1
38 #
39 #  The basic algorithm is to rsync each maildir individually,
40 #  and to use hard links for retaining historical data.
41 #
42 #  We handle each maildir individually because it becomes very
43 #  unweldy to hardlink and rsync many hundreds of thousands
44 #  of files at once. It is much faster to take on smaller
45 #  chunks at a time.
46 #
47 #  For the backup rotation to work, destuser must be able to run
48 #  arbitrary bash commands on the desthost.
49 #
50 #  Any maildir which is deleted from the source will be moved to
51 #  "deleted" directory in the destination. It is up to you to
52 #  periodically remove this directory or old maildirs in it.
53 #
54 ##############################################################
55
56 getconf rotate yes
57 getconf remove yes
58 getconf backup yes
59
60 getconf loadlimit 5
61 getconf speedlimit 0
62 getconf keepdaily 5
63 getconf keepweekly 3
64 getconf keepmonthly 1
65
66 getconf srcdir /var/maildir
67 getconf destdir
68 getconf desthost
69 getconf destport 22
70 getconf destuser
71 getconf destid_file /root/.ssh/id_rsa
72
73 getconf multiconnection notset
74
75 failedcount=0
76 # strip trailing /
77 destdir=${destdir%/}
78 srcdir=${srcdir%/}
79
80 [ -d $srcdir ] || fatal "source directory $srcdir doesn't exist"
81
82 [ "$multiconnection" == "notset" ] && fatal "The maildir handler uses a very different destination format. See the example .maildir for more information"
83
84 if [ $test ]; then
85    testflags="--dry-run -v"
86 fi
87
88 rsyncflags="$testflags -e 'ssh -p $destport -i $destid_file' -r -v --ignore-existing --delete --size-only --bwlimit=$speedlimit"
89 excludes="--exclude '.Trash/\*' --exclude '.Mistakes/\*' --exclude '.Spam/\*'"
90
91 ##################################################################
92 ### FUNCTIONS
93
94 function do_user() {
95    local user=$1
96    local btype=$2
97    local userdir=${3%/}
98    local source="$srcdir/$userdir/$user/"
99    local target="$destdir/$userdir/$user/$btype.1"
100    if [ ! -d $source ]; then
101       warning "maildir $source not found"
102       return
103    fi
104
105    debug "syncing"
106    ret=`$RSYNC -e "ssh -p $destport -i $destid_file" -r \
107       --links --ignore-existing --delete --size-only --bwlimit=$speedlimit \
108       --exclude '.Trash/*' --exclude '.Mistakes/*' --exclude '.Spam/*' \
109       $source $destuser@$desthost:$target \
110       2>&1`
111    ret=$?
112    # ignore 0 (success) and 24 (file vanished before it could be copied)
113    if [ $ret != 0 -a $ret != 24 ]; then
114       warning "rsync $user failed"
115       warning "  returned: $ret"
116       let "failedcount = failedcount + 1"
117       if [ $failedcount -gt 100 ]; then
118          fatal "100 rsync errors -- something is not working right. bailing out."
119       fi
120    fi
121    ssh -o PasswordAuthentication=no $desthost -l $destuser -i $destid_file "date +%c%n%s > $target/created"
122 }
123
124 # remove any maildirs from backup which might have been deleted
125 # and add new ones which have just been created.
126 # (actually, it just moved them to the directory "deleted")
127
128 function do_remove() {
129    local tmp1=`maketemp maildir-tmp-file`
130    local tmp2=`maketemp maildir-tmp-file`
131
132    ssh -p $destport -i $destid_file $destuser@$desthost mkdir -p "$destdir/deleted"
133       cd "$srcdir"
134       for userdir in `ls -d1 */`; do
135          ls -1 "$srcdir/$userdir" | sort > $tmp1
136          ssh -p $destport -i $destid_file $destuser@$desthost ls -1 "$destdir/$userdir" | sort > $tmp2
137       for deluser in `join -v 2 $tmp1 $tmp2`; do
138          [ "$deluser" != "" ] || continue
139          info "removing $destuser@$desthost:$destdir/$userdir$deluser/"
140          ssh -p $destport -i $destid_file $destuser@$desthost mv "$destdir/$userdir$deluser/" "$destdir/deleted"
141          ssh -p $destport -i $destid_file $destuser@$desthost "date +%c%n%s > '$destdir/deleted/$deluser/deleted_on'"
142       done
143    done
144    rm $tmp1
145    rm $tmp2
146 }
147
148 function do_rotate() {
149    [ "$rotate" == "yes" ] || return;
150    local user=$1
151    local userdir=${2%/}
152    local backuproot="$destdir/$userdir/$user"
153 (
154    ssh -T -o PasswordAuthentication=no $desthost -l $destuser -i $destid_file <<EOF
155 ##### BEGIN REMOTE SCRIPT #####
156    seconds_daily=86400
157    seconds_weekly=604800
158    seconds_monthly=2628000
159    keepdaily=$keepdaily
160    keepweekly=$keepweekly
161    keepmonthly=$keepmonthly
162    now=\`date +%s\`
163
164    if [ ! -d "$backuproot" ]; then
165       echo "Debug: skipping rotate of $user. $backuproot doesn't exist."
166       exit
167    fi
168    for rottype in daily weekly monthly; do
169       seconds=\$((seconds_\${rottype}))
170
171       dir="$backuproot/\$rottype"
172       if [ ! -d \$dir.1 ]; then
173          echo "Debug: \$dir.1 does not exist, skipping."
174          continue 1
175       elif [ ! -f \$dir.1/created ]; then
176          echo "Warning: \$dir.1/created does not exist. This backup may be only partially completed. Skipping rotation."
177          continue 1
178       fi
179
180       # Rotate the current list of backups, if we can.
181       oldest=\`find $backuproot -maxdepth 1 -type d -name \$rottype'.*' | @SED@ 's/^.*\.//' | sort -n | tail -1\`
182       #echo "Debug: oldest \$oldest"
183       [ "\$oldest" == "" ] && oldest=0
184       for (( i=\$oldest; i > 0; i-- )); do
185          if [ -d \$dir.\$i ]; then
186             if [ -f \$dir.\$i/created ]; then
187                created=\`tail -1 \$dir.\$i/created\`
188             else
189                created=0
190             fi
191             cutoff_time=\$(( now - (seconds*(i-1)) ))
192             if [ ! \$created -gt \$cutoff_time ]; then
193                next=\$(( i + 1 ))
194                if [ ! -d \$dir.\$next ]; then
195                   echo "Debug: \$rottype.\$i --> \$rottype.\$next"
196                   mv \$dir.\$i \$dir.\$next
197                   date +%c%n%s > \$dir.\$next/rotated
198                else
199                   echo "Debug: skipping rotation of \$dir.\$i because \$dir.\$next already exists."
200                fi
201             else
202                echo "Debug: skipping rotation of \$dir.\$i because it was created" \$(( (now-created)/86400)) "days ago ("\$(( (now-cutoff_time)/86400))" needed)."
203             fi
204          fi
205       done
206    done
207
208    max=\$((keepdaily+1))
209    if [ \( \$keepweekly -gt 0 -a -d $backuproot/daily.\$max \) -a ! -d $backuproot/weekly.1 ]; then
210       echo "Debug: daily.\$max --> weekly.1"
211       mv $backuproot/daily.\$max $backuproot/weekly.1
212       date +%c%n%s > $backuproot/weekly.1/rotated
213    fi
214
215    max=\$((keepweekly+1))
216    if [ \( \$keepmonthly -gt 0 -a -d $backuproot/weekly.\$max \) -a ! -d $backuproot/monthly.1 ]; then
217       echo "Debug: weekly.\$max --> monthly.1"
218       mv $backuproot/weekly.\$max $backuproot/monthly.1
219       date +%c%n%s > $backuproot/monthly.1/rotated
220    fi
221
222    for rottype in daily weekly monthly; do
223       max=\$((keep\${rottype}+1))
224       dir="$backuproot/\$rottype"
225       oldest=\`find $backuproot -maxdepth 1 -type d -name \$rottype'.*' | @SED@ 's/^.*\.//' | sort -n | tail -1\`
226       [ "\$oldest" == "" ] && oldest=0
227       # if we've rotated the last backup off the stack, remove it.
228       for (( i=\$oldest; i >= \$max; i-- )); do
229          if [ -d \$dir.\$i ]; then
230             if [ -d $backuproot/rotate.tmp ]; then
231                echo "Debug: removing rotate.tmp"
232                rm -rf $backuproot/rotate.tmp
233             fi
234             echo "Debug: moving \$rottype.\$i to rotate.tmp"
235             mv \$dir.\$i $backuproot/rotate.tmp
236          fi
237       done
238    done
239 ####### END REMOTE SCRIPT #######
240 EOF
241 ) | (while read a; do passthru $a; done)
242
243 }
244
245
246 function setup_remote_dirs() {
247    local user=$1
248    local backuptype=$2
249    local userdir=${3%/}
250    local dir="$destdir/$userdir/$user/$backuptype"
251    local tmpdir="$destdir/$userdir/$user/rotate.tmp"
252 (
253    ssh -T -o PasswordAuthentication=no $desthost -l $destuser -i $destid_file <<EOF
254       if [ ! -d $destdir ]; then
255          echo "Fatal: Destination directory $destdir does not exist on host $desthost."
256          exit 1
257       elif [ -d $dir.1 ]; then
258          if [ -f $dir.1/created ]; then
259             echo "Warning: $dir.1 already exists. Overwriting contents."
260          else
261             echo "Warning: we seem to be resuming a partially written $dir.1"
262          fi
263       else
264          if [ -d $tmpdir ]; then
265             mv $tmpdir $dir.1
266             if [ \$? == 1 ]; then
267                echo "Fatal: could mv $destdir/rotate.tmp $dir.1 on host $desthost"
268                exit 1
269             fi
270          else
271             mkdir --parents $dir.1
272             if [ \$? == 1 ]; then
273                echo "Fatal: could not create directory $dir.1 on host $desthost"
274                exit 1
275             fi
276          fi
277          if [ -d $dir.2 ]; then
278             echo "Debug: update links $backuptype.2 --> $backuptype.1"
279             cp -alf $dir.2/. $dir.1
280             #if [ \$? == 1 ]; then
281             #   echo "Fatal: could not create hard links to $dir.1 on host $desthost"
282             #   exit 1
283             #fi
284          fi
285       fi
286       [ -f $dir.1/created ] && rm $dir.1/created
287       [ -f $dir.1/rotated ] && rm $dir.1/rotated
288       exit 0
289 EOF
290 ) | (while read a; do passthru $a; done)
291
292    if [ $? == 1 ]; then exit; fi
293 }
294
295 function start_mux() {
296    if [ "$multiconnection" == "yes" ]; then
297       debug "Starting dummy ssh connection"
298       ssh -p $destport -i $destid_file $destuser@$desthost sleep 1d &
299       sleep 1
300    fi
301 }
302
303 function end_mux() {
304    if [ "$multiconnection" == "yes" ]; then
305       debug "Stopping dummy ssh connection"
306       ssh -p $destport -i $destid_file $destuser@$desthost pkill sleep
307    fi
308 }
309
310 ###
311 ##################################################################
312
313 # see if we can login
314 debug "ssh -o PasswordAuthentication=no $desthost -l $destuser -i $destid_file 'echo -n 1'"
315 if [ ! $test ]; then
316    result=`ssh -o PasswordAuthentication=no $desthost -l $destuser -i $destid_file 'echo -n 1' 2>&1`
317    if [ "$result" != "1" ]; then
318       fatal "Can't connect to $desthost as $destuser using $destid_file."
319    fi
320 fi
321
322 end_mux
323 start_mux
324
325 ## SANITY CHECKS ##
326 status=`ssh -p $destport -i $destid_file $destuser@$desthost "[ -d \"$destdir\" ] && echo 'ok'"`
327 if [ "$status" != "ok" ]; then
328    end_mux
329    fatal "Destination directory $destdir doesn't exist!"
330    exit
331 fi
332
333 ### REMOVE OLD MAILDIRS ###
334
335 if [ "$remove" == "yes" ]; then
336    do_remove
337 fi
338
339 ### MAKE BACKUPS ###
340
341 if [ "$backup" == "yes" ]; then
342    if [ $keepdaily -gt 0 ]; then btype=daily
343    elif [ $keepweekly -gt 0 ]; then btype=weekly
344    elif [ $keepmonthly -gt 0 ]; then btype=monthly
345    else fatal "keeping no backups"; fi
346
347    if [ "$testuser" != "" ]; then
348       cd "$srcdir/${user:0:1}"
349       do_rotate $testuser
350       setup_remote_dirs $testuser $btype
351       do_user $testuser $btype
352    else
353       [ -d "$srcdir" ] || fatal "directory $srcdir not found."
354       cd "$srcdir"
355       for userdir in `ls -d1 */`; do
356          [ -d "$srcdir/$userdir" ] || fatal "directory $srcdir/$userdir not found."
357          cd "$srcdir/$userdir"
358          debug $userdir
359          for user in `ls -1`; do
360             [ "$user" != "" ] || continue
361             debug "$user $userdir"
362             do_rotate $user $userdir
363             setup_remote_dirs $user $btype $userdir
364             do_user $user $btype $userdir
365          done
366       done
367    fi
368 fi
369
370 end_mux