Newer
Older
# -*- mode: sh; sh-basic-offset: 8; indent-tabs-mode: nil; -*-
###############################################################
#
# This handler slowly creates a backup of each user's maildir
# to a remote server. It is designed to be run with low overhead
# in terms of cpu and bandwidth so it runs pretty slow.
#
# if destdir is /backup/maildir/, then it will contain the files
# daily.1
# daily.2
# daily.3
# weekly.1
# weekly.2
# monthly.1
# if keepdaily is 3, keepweekly is 2, and keepmonthly is 1.
#
# The basic algorithm is to rsync each maildir individually,
# and to use hard links for retaining historical data.
#
# We rsync each maildir individually because it becomes very
# unweldy to start a single rsync of many hundreds of thousands
# of files.
#
# For the backup rotation to work, destuser must be able to run
# arbitrary bash commands on the desthost.
#
##############################################################
getconf rotate yes
getconf remove yes
getconf loadlimit 5
getconf speedlimit 0
getconf keepdaily 5
getconf keepweekly 3
getconf keepmonthly 1
getconf srcdir /var/maildir
getconf destdir
getconf desthost
getconf destport 22
getconf destuser
# strip trailing /
destdir=${destdir%/}
srcdir=${srcdir%/}
#getconf letter
#getconf testuser elijah
getconf backup yes
#letters=e
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"
[ -d $srcdir ] || fatal "source directory $srcdir doesn't exist"
[ ! $test ] || testflags="--dry-run -v"
rsyncflags="$testflags -e 'ssh -p $destport' -r -v --ignore-existing --delete --size-only --bwlimit=$speedlimit"
excludes="--exclude '.Trash/\*' --exclude '.Mistakes/\*' --exclude '.Spam/\*'"
# see if we can login
debug "ssh -o PasswordAuthentication=no $desthost -l $destuser 'echo -n 1'"
if [ ! $test ]; then
result=`ssh -o PasswordAuthentication=no $desthost -l $destuser 'echo -n 1' 2>&1`
if [ "$result" != "1" ]; then
fatal "Can't connect to $desthost as $destuser."
fi
fi
##################################################################
### FUNCTIONS
function do_user() {
local user=$1
local letter=${user:0:1}
local dir="$srcdir/$letter/$user"
[ -d $dir ] || fatal "maildir $dir not found".
# while true; do
# load=`uptime | sed 's/^.*load average: \\([^,]*\\).*$/\\1/'`
# over=`expr $load \> $loadlimit`
# if [ $over == 1 ]; then
# info "load $load, sleeping..."
# sleep 600
# else
# break
# fi
# done
cmd="$RSYNC $rsyncflags $excludes $dir $destuser@$desthost:$destdir/$letter"
ret=`rsync -e "ssh -p $destport" -r \
--links --ignore-existing --delete --size-only --bwlimit=$speedlimit \
--exclude '.Trash/*' --exclude '.Mistakes/*' --exclude '.Spam/*' \
$dir $destuser@$desthost:$destdir/$letter \
2>&1`
ret=$?
# ignore 0 (success) and 24 (file vanished before it could be copied)
if [ $ret != 0 -a $ret != 24 ]; then
warning "rsync $user failed"
warning " returned: $ret"
let "failedcount = failedcount + 1"
if [ $failedcount -gt 100 ]; then
fatal "100 rsync errors -- something is not working right. bailing out."
fi
fi
}
# remove any maildirs from backup which might have been deleted
# and add new ones which have just been created.
function do_remove() {
local tmp1=`maketemp maildir-tmp-file`
local tmp2=`maketemp maildir-tmp-file`
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
ls -1 "$srcdir/$i" | sort > $tmp1
ssh -p $destport $desthost ls -1 '$destdir/maildir/$i' | sort > $tmp2
for deluser in `join -v 2 $tmp1 $tmp2`; do
cmd="ssh -p $destport $desthost rm -vr '$destdir/maildir/$i/$deluser/'"
debug $cmd
done
done
rm $tmp1
rm $tmp2
}
function do_rotate() {
backuproot=$destdir
ssh -T -o PasswordAuthentication=no $desthost -l $destuser <<EOF
##### BEGIN REMOTE SCRIPT #####
seconds_daily=86400
seconds_weekly=604800
seconds_monthly=2628000
keepdaily=$keepdaily
keepweekly=$keepweekly
keepmonthly=$keepmonthly
for rottype in daily weekly monthly; do
dir="$backuproot/\$rottype"
if [ ! -d \$dir.1 ]; then
echo "Info: \$dir.1 does not exist. This backup is missing, so we are skipping the rotation."
continue 1
elif [ ! -f \$dir.1/created ]; then
echo "Warning: \$dir.1/created does not exist. This backup may be only partially completed. Skipping rotation."
continue 1
fi
# Rotate the current list of backups, if we can.
oldest=\`find $backuproot -type d -maxdepth 1 -name \$rottype'.*' | sed 's/^.*\.//' | sort -n | tail -1\`
echo "Debug: oldest \$oldest"
for (( i=\$oldest; i > 0; i-- )); do
if [ -d \$dir.\$i ]; then
if [ -f \$dir.\$i/created ]; then
created=\`tail -1 \$dir.\$i/created\`
cutoff_time=\$(( now - (seconds*(i-1)) ))
if [ ! \$created -gt \$cutoff_time ]; then
mv \$dir.\$i \$dir.\$next
date +%c%n%s > \$dir.\$next/rotated
else
echo "Info: skipping rotation of \$dir.\$i because \$dir.\$next already exists."
fi
echo "Info: skipping rotation of \$dir.\$i because it was created" \$(( (now-created)/86400)) "days ago ("\$(( (now-cutoff_time)/86400))" needed)."
fi
done
done
max=\$((keepdaily+1))
if [ \( \$keepweekly -gt 0 -a -d $backuproot/daily.\$max \) -a ! -d $backuproot/weekly.1 ]; then
echo mv $backuproot/daily.\$max $backuproot/weekly.1
mv $backuproot/daily.\$max $backuproot/weekly.1
date +%c%n%s > $backuproot/weekly.1/rotated
fi
max=\$((keepweekly+1))
if [ \( \$keepmonthly -gt 0 -a -d $backuproot/weekly.\$max \) -a ! -d $backuproot/monthly.1 ]; then
echo mv $backuproot/weekly.\$max $backuproot/monthly.1
mv $backuproot/weekly.\$max $backuproot/monthly.1
date +%c%n%s > $backuproot/monthly.1/rotated
fi
for rottype in daily weekly monthly; do
oldest=\`find $backuproot -type d -maxdepth 1 -name \$rottype'.*' | sed 's/^.*\.//' | sort -n | tail -1\`
# if we've rotated the last backup off the stack, remove it.
for (( i=\$oldest; i >= \$max; i-- )); do
if [ -d \$dir.\$i ]; then
if [ -d $backuproot/rotate.tmp ]; then
echo "Info: removing $backuproot/rotate.tmp"
rm -rf $backuproot/rotate.tmp
fi
echo "Info: moving \$dir.\$i to $backuproot/rotate.tmp"
mv \$dir.\$i $backuproot/rotate.tmp
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
function setup_remote_dirs() {
local backuptype=$1
local dir="$destdir/$backuptype"
(
ssh -T -o PasswordAuthentication=no $desthost -l $destuser <<EOF
if [ ! -d $destdir ]; then
echo "Fatal: Destination directory $destdir does not exist on host $desthost."
exit 1
elif [ -d $dir.1 ]; then
if [ -f $dir.1/created ]; then
echo "Warning: $dir.1 already exists. Overwriting contents."
else
echo "Warning: we seem to be resuming a partially written $dir.1"
fi
else
if [ -d $destdir/rotate.tmp ]; then
mv $destdir/rotate.tmp $dir.1
if [ \$? == 1 ]; then
echo "Fatal: could mv $destdir/rotate.tmp $dir.1 on host $desthost"
exit 1
fi
else
mkdir $dir.1
if [ \$? == 1 ]; then
echo "Fatal: could not create directory $dir.1 on host $desthost"
exit 1
fi
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
mkdir $dir.1/\$i
done
fi
if [ -d $destdir/$backuptype.2 ]; then
echo "Info: updating hard links to $dir.1. This may take a while."
cp -alf $destdir/$backuptype.2/. $dir.1
#if [ \$? == 1 ]; then
# echo "Fatal: could not create hard links to $dir.1 on host $desthost"
# exit 1
#fi
fi
fi
[ -f $dir.1/created ] && rm $dir.1/created
[ -f $dir.1/rotated ] && rm $dir.1/rotated
exit 0
EOF
) | (while read a; do passthru $a; done)
if [ $? == 1 ]; then exit; fi
}
###
##################################################################
### ROTATE BACKUPS ###
if [ "$rotate" == "yes" ]; then
do_rotate
if [ "$remove" == "yes" ]; then
debug remove
if [ "$backup" == "yes" ]; then
if [ $keepdaily -gt 0 ]; then btype=daily
elif [ $keepweekly -gt 0 ]; then btype=weekly
elif [ $keepmonthly -gt 0 ]; then btype=monthly
else fatal "keeping no backups"; fi
setup_remote_dirs $btype
for i in $letters; do
[ -d "$srcdir/$i" ] || fatal "directory $srcdir/$i not found."
cd "$srcdir/$i"
debug $i
for user in `ls -1`; do
if [ "$testuser" != "" -a "$testuser" != "$user" ]; then continue; fi
do_user $user $destdir/$btype.1
done
done
ssh -o PasswordAuthentication=no $desthost -l $destuser "date +%c%n%s > $destdir/$btype.1/created"