Whamcloud - gitweb
Branch HEAD
[fs/lustre-release.git] / lustre / utils / llbackup
1 #!/bin/bash
2 # do a parallel backup and restore of specified files
3 #
4 #  Copyright (C) 2008 Sun Microsystems, Inc.
5 #
6 #   This file is part of Lustre, http://www.lustre.org.
7 #
8 #   Lustre is free software; you can redistribute it and/or
9 #   modify it under the terms of version 2 of the GNU General Public
10 #   License as published by the Free Software Foundation.
11 #
12 #   Lustre is distributed in the hope that it will be useful,
13 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #   GNU General Public License for more details.
16 #
17 #   You should have received a copy of the GNU General Public License
18 #   along with Lustre; if not, write to the Free Software
19 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 #
21 # Author: Andreas Dilger <adilger@sun.com>
22 #
23
24 VERSION=1.1.0
25
26 export LC_ALL=C
27
28 RSH=${RSH:-"ssh"}                       # use "bash -c" for local testing
29 SPLITMB=${SPLITMB:-8192}                # target chunk size (uncompressed)
30 SPLITCOUNT=${SPLITCOUNT:-200}           # number of files to give each client
31 TAR=${TAR:-"tar"}                       # also htar in theory works (untested)
32
33 #PROGPATH=$(which $0 2> /dev/null) || PROGPATH=$PWD/$0
34 case $0 in
35         .*) PROGPATH=$PWD/$0 ;;
36         *)  PROGPATH=$0 ;;
37 esac
38
39 PROGNAME="$(basename $PROGPATH)"
40 LOGPREFIX="$PROGNAME"
41 log() {
42         echo "$LOGPREFIX: $*" 1>&2
43 }
44
45 fatal() {
46         log "ERROR: $*"
47         exit 1
48 }
49
50 usage() {
51         log "$*"
52         echo "usage: $PROGNAME [-chjvxz] [-C directory] [-e rsh] [-i inputlist]"
53         echo -e "\t\t[-l logdir] [-n nodes] [-s splitmb] [-T tar] -f ${FTYPE}base"
54         echo -e "\t-c create archive"
55         echo -e "\t-C directory: relative directory for filenames (default PWD)"
56         echo -e "\t-e rsh: specify the passwordless remote shell (default $RSH)"
57         if [ "$OP" = "backup" ]; then
58                 echo -e "\t-f outputfile: specify base output filename for backup"
59         else
60                 echo -e "\t-f ${OP}filelist: specify list of files to $OP"
61         fi
62         echo -e "\t-h: print help message and exit (use -x -h for restore help)"
63         if [ "$OP" = "backup" ]; then
64                 echo -e "\t-i inputfile: list of files to backup (default stdin)"
65         fi
66         echo -e "\t-j: use bzip2 compression on $FTYPE file(s)"
67         echo -e "\t-l logdir: directory for output logs"
68         echo -e "\t-n nodes: comma-separated list of client nodes to run ${OP}s"
69         if [ "$OP" = "backup" ]; then
70                 echo -e "\t-s splitmb: target size for backup chunks " \
71                         "(default ${SPLITMB}MiB)"
72                 echo -e "\t-S splitcount: number of files sent to each client "\
73                         "(default ${SPLITCOUNT})"
74         fi
75         echo -e "\t-t: list table of contents of tarfile"
76         echo -e "\t-T tar: specify the backup command (default $TAR)"
77         echo -e "\t-v: be verbose - list all files being processed"
78         echo -e "\t-V: print version number and exit"
79         echo -e "\t-x: extract files instead of backing them up"
80         echo -e "\t-z: use gzip compression on $FTYPE file(s)"
81         exit 1
82 }
83
84 usage_inactive() {
85         usage "inactive argument '$1 $2' in '$3' mode"
86 }
87
88 set_op_type() {
89         case $1 in
90         *backup*)       OP=backup;  FTYPE=output; TAROP="-c" ;;
91         *list*)         OP=list;    FTYPE=input;  TAROP="-t"; SPLITCOUNT=1 ;;
92         *restore*)      OP=restore; FTYPE=input;  TAROP="-x"; SPLITCOUNT=1 ;;
93         *)              FTYPE="output"; usage "unknown archive operation '$1'";;
94         esac
95 }
96
97 #echo ARGV: "$@"
98
99 # --fileonly, --remote are internal-use-only options
100 TEMPARGS=$(getopt -n $LOGPREFIX -o cC:e:f:hi:jl:n:ps:S:tT:vVxz --long create,extract,list,restore,directory:,rsh:,outputbase:,help,inputfile:,bzip2,logdir:,nodes:,permissions:splitmb,splitcount,tar:,verbose,version,gzip,fileonly,remote: -- "$@")
101
102 eval set -- "$TEMPARGS"
103
104 set_op_type $PROGNAME
105
106 # parse input arguments, and accumulate the client-specific args
107 while true; do
108         case "$1" in
109         -c|--create)    [ "$OP" != "backup" ] &&
110                                 usage "can't use $1 $TAROP at the same time"
111                         OP="backup"; ARGS="$ARGS $1"; shift ;;
112         -C|--directory) GOTODIR="$2"; cd "$2" || usage "error cd to -C $2";
113                                         ARGS="$ARGS $1 \"$2\""; shift 2 ;;
114         -e|--rsh)       RSH="$2"; shift 2;;
115         -f|--outputbase)OUTPUTBASE="$2";ARGS="$ARGS $1 \"$2\""; shift 2 ;;
116         -h|--help)      ARGS=""; break;;
117         -i|--inputfile) INPUT="$2"; shift 2;;
118         -j|--bzip2)     TARCOMP="-j";   ARGS="$ARGS $1"; shift ;;
119         -l|--logdir)    LOGDIR="$2";    ARGS="$ARGS $1 \"$2\""; shift 2 ;;
120         -n|--nodes)     NODELIST="$NODELIST,$2";
121                                         ARGS="$ARGS $1 \"$2\""; shift 2 ;;
122         -p|--permissions) PERM="-p";    ARGS="$ARGS $1"; shift ;;
123         -s|--splitmb)   [ "$OP" != "backup" ] && usage_inactive $1 $2 $OP
124                         SPLITMB=$2;     ARGS="$ARGS $1 \"$2\""; shift 2 ;;
125         -S|--splitcount)[ "$OP" != "backup" ] && usage_inactive $1 $2 $OP
126                         SPLITCOUNT=$2;  ARGS="$ARGS $1 \"$2\""; shift 2 ;;
127         -t|--list)      [ "$OP" != "backup" -a "$OP" != "list" ] &&
128                                 usage "can't use $1 $TAROP at the same time"
129                         OP="list"; ARGS="$ARGS $1"; shift ;;
130         -T|--tar)       TAR="$2";       ARGS="$ARGS $1 \"$2\""; shift 2 ;;
131         -v|--vebose)    [ "$VERBOSE" = "-v" ] && set -vx # be extra verbose
132                         VERBOSE="-v";   ARGS="$ARGS $1"; shift ;;
133         -V|--version)   echo "$LOGPREFIX: version $VERSION"; exit 0;;
134         -x|--extract|--restore)
135                         [ "$OP" != "backup" -a "$OP" != "restore" ] &&
136                                 usage "can't use $1 $TAROP at the same time"
137                         OP="restore"; ARGS="$ARGS $1"; shift ;;
138         -z|--gzip)      TARCOMP="-z";   ARGS="$ARGS $1"; shift ;;
139         # these commands are for internal use only
140         --remote)       NODENUM="$2"; LOGPREFIX="$(hostname).$2"; shift 2;;
141         --fileonly)     FILEONLY="yes"; shift;;
142         --)             shift; break;;
143         *) usage "unknown argument '$1'" 1>&2 ;;
144         esac
145 done
146
147 set_op_type $OP
148
149 #log "ARGS: $ARGS"
150
151 [ -z "$ARGS" ] && usage "$OP a list of files, running on multiple nodes"
152
153 # we should be able to use any backup tool that can accept filenames
154 # from an input file instead of just pathnames on the command-line.
155 # Unset TARCOMP for htar, as it doesn't support on-the-fly compression.
156 TAREXT=
157 case "$(basename $TAR)" in
158         htar*)              TARARG="-L"; TAROUT="-f"; TARCOMP=""; MINKB=0 ;;
159         tar*|gnutar*|gtar*) TARARG="-T"; TAROUT="-b 2048 -f"; TAREXT=.tar ;;
160         *)              fatal "unknown archiver '$TAR'" ;;
161 esac
162
163 if [ "$OP" = "backup" ]; then
164         [ -z "$OUTPUTBASE" ] && usage "'-f ${FTYPE}base' must be given for $OP"
165         # Make sure we leave some margin free in the output filesystem for the
166         # chunks.  If we are dumping to a network filesystem (denoted by having
167         # a ':' in the name, not sure how else to check) then we assume this
168         # filesystem is shared among all clients and expect the other nodes
169         # to also consume space there.
170         OUTPUTFS=$(dirname $OUTPUTBASE)
171         NETFS=$(df -P $OUTPUTFS | awk '/^[[:alnum:]]*:/ { print $1 }')
172         MINKB=${MINKB:-$((SPLITMB * 2 * 1024))}
173         [ "$NETFS" ] && MINKB=$(($(echo $NODELIST | tr ',' ' ' | wc -w) * $MINKB))
174
175         # Compress the output files as we go.
176         case "$TARCOMP" in
177                 -z) TAREXT="$TAREXT.gz";;
178                 -j) TAREXT="$TAREXT.bz2";;
179         esac
180 else
181         [ -z "$OUTPUTBASE" ] &&
182                 usage "-f ${OP}filelist must be specified for $OP"
183         # we want to be able to use this for a list of files to restore
184         # but it is convenient to use $INPUT for reading the pathnames
185         # of the tar files during restore/list operations to handle stdin
186         [ "$INPUT" ] && usage "-i inputbase unsupported for $OP"
187         INPUT=$OUTPUTBASE
188         TARARG=""
189 fi
190
191 [ -z "$NODELIST" ] && NODELIST="localhost"
192
193 # If we are writing to a char or block device (e.g. tape) don't add any suffix
194 # We can't currently specify a different target device per client...
195 if [ -b "$OUTPUTBASE" -o -c "$OUTPUTBASE" ]; then
196         MINKB=0
197         [ -z "$LOGDIR" ] && LOGDIR="/var/log"
198         LOGBASE="$LOGDIR/$PROGNAME"
199 elif [ -d "$OUTPUTBASE" ]; then
200         usage "-f $OUTPUTBASE must be a pathname, not a directory"
201 else
202         [ -z "$LOGDIR" ] && LOGBASE="$OUTPUTBASE" || LOGBASE="$LOGDIR/$PROGNAME"
203 fi
204 LOGBASE="$LOGBASE.$(date +%Y%m%d%H%M)"
205
206 # tar up a sinle list of files into a chunk.  We don't exit if there is an
207 # error returned, since that might happen frequently with e.g. files moving
208 # and no longer being available for backup.
209 # usage: run_one_tar {file_list} {chunk_nr} {chunkbytes}
210 DONE_MSG="FINISH_THIS_PROGRAM_NOW_I_TELL_YOU"
211 KILL_MSG="EXIT_THIS_PROGRAM_NOW_I_TELL_YOU"
212 run_one_backup() {
213 #set -vx
214         TMPLIST="$1"
215         CHUNK="$2"
216         CHUNKMB="$(($3 / 1048576))"
217         if [ -b "$OUTPUTBASE" -o -c "$OUTPUTBASE" ]; then
218                 OUTFILE="$OUTPUTBASE"
219         else
220                 OUTFILE="$OUTPUTBASE.$NODENUM.$CHUNK$TAREXT"
221         fi
222         CHUNKBASE="$LOGBASE.$NODENUM.$CHUNK"
223         LISTFILE="$CHUNKBASE.list"
224         LOG="$CHUNKBASE.log"
225
226         cp "$TMPLIST" "$LISTFILE"
227
228         SLEPT=0
229         FREEKB=$(df -P $OUTPUTFS 2> /dev/null | tail -n 1 | awk '{print $4}')
230         while [ $FREEKB -lt $MINKB ]; do
231                 sleep 5
232                 SLEPT=$((SLEPT + 5))
233                 if [ $((SLEPT % 60)) -eq 10 ]; then
234                         log "waiting ${SLEPT}s for ${MINKB}kB free in $OUTPUTFS"
235                 fi
236                 FREEKB=$(df -P $OUTPUTFS | tail -n 1 | awk '{print $4}')
237         done
238         [ $SLEPT -gt 0 ] && log "waited ${SLEPT}s for space in ${OUTPUTFS}"
239         log "$LISTFILE started - est. ${CHUNKMB}MB"
240         START=$(date +%s)
241         eval $TAR $TAROP $PERM $TARARG "$TMPLIST" -v $TARCOMP $TAROUT "$OUTFILE" \
242                 2>&1 >>"$LOG" | tee -a $LOG | grep -v "Removing leading"
243         RC=${PIPESTATUS[0]}
244         ELAPSE=$(($(date +%s) - START))
245         if [ $RC -eq 0 ]; then
246                 if [ -f "$OUTFILE" ]; then
247                         BYTES=$(stat -c '%s' "$OUTFILE")
248                         CHUNKMB=$((BYTES / 1048576))
249                         log "$LISTFILE finished - act. ${CHUNKMB}MB/${ELAPSE}s"
250                 else
251                         log "$LISTFILE finished OK - ${ELAPSE}s"
252                 fi
253                 echo "OK" > $CHUNKBASE.done
254         else
255                 echo "ERROR=$RC" > $CHUNKBASE.done
256                 log "ERROR: $LISTFILE exited with rc=$RC"
257         fi
258         rm $TMPLIST
259         return $RC
260 }
261
262 run_one_restore_or_list() {
263 #set -vx
264         INPUTFILE="$1"
265         LOG="$LOGBASE.$(basename $INPUTFILE).restore.log"
266
267         SLEPT=0
268         while [ $MINKB != 0 -a ! -r "$INPUTFILE" ]; do
269                 SLEPT=$((SLEPT + 5))
270                 if [ $((SLEPT % 60)) -eq 10 ]; then
271                         log "waiting ${SLEPT}s for $INPUTFILE staging"
272                 fi
273                 sleep 5
274         done
275         [ $SLEPT -gt 0 ] && log "waited ${SLEPT}s for $INPUTFILE staging"
276         log "$OP of $INPUTFILE started"
277         START=$(date +%s)
278         eval $TAR $TAROP -v $TARCOMP $TAROUT "$INPUTFILE" 2>&1 >>"$LOG" |
279                 tee -a "$LOG" | grep -v "Removing leading"
280         RC=${PIPESTATUS[0]}
281         ELAPSE=$(($(date +%s) - START))
282         [ "$OP" = "list" ] && cat $LOG
283         if [ $RC -eq 0 ]; then
284                 log "$INPUTFILE finished OK - ${ELAPSE}s"
285                 echo "OK" > $INPUTFILE.restore.done
286         else
287                 echo "ERROR=$RC" > $INPUTFILE.restore.done
288                 log "ERROR: $OP of $INPUTFILE exited with rc=$RC"
289         fi
290         return $RC
291 }
292
293 # Run as a remote command and read input filenames from stdin and create tar
294 # output files of the requested size.  The input filenames can either be:
295 # "bytes filename" or "filename" depending on whether FILEONLY is set.
296 #
297 # Read filenames until we have either a large enough list of small files,
298 # or we get a very large single file that is backed up by itself.
299 run_remotely() {
300 #set -vx
301         log "started thread"
302         RCMAX=0
303
304         [ "$FILEONLY" ] && PARAMS="FILENAME" || PARAMS="BYTES FILENAME"
305
306         if [ "$OP" = "backup" ]; then
307                 TMPBASE=$PROGNAME.$LOGPREFIX.temp
308                 TMPFILE="$(mktemp -t $TMPBASE.$(date +%s).XXXXXXX)"
309                 OUTPUTFILENUM=0
310                 SUMBYTES=0
311         fi
312         BYTES=""
313
314         while read $PARAMS; do
315                 [ "$FILENAME" = "$DONE_MSG" -o "$BYTES" = "$DONE_MSG" ] && break
316                 if [ "$FILENAME" = "$KILL_MSG" -o "$BYTES" = "$KILL_MSG" ]; then
317                         log "exiting $OP on request"
318                         [ "$TARPID" ] && kill -9 $TARPID 2> /dev/null
319                         exit 9
320                 fi
321
322                 case "$OP" in
323                 list|restore)
324                         run_one_restore_or_list $FILENAME; RC=$?
325                         ;;
326                 backup) STAT=($(stat -c '%s %F' "$FILENAME"))
327                         [ "$FILEONLY" ] && BYTES=${STAT[0]}
328                         # if this is a directory that has files in it, it will
329                         # be backed up as part of this (or some other) backup.
330                         # Only include it in the backup if empty, otherwise
331                         # the files therein will be backed up multiple times
332                         if [ "${STAT[1]}" = "directory" ]; then
333                                 NUM=`find "$FILENAME" -maxdepth 1|head -2|wc -l`
334                                 [ "$NUM" -gt 1 ] && continue
335                         fi
336                         [ "$VERBOSE" ] && log "$FILENAME"
337
338                         # if a file is > 3/4 of chunk size, archive by itself
339                         # avoid shell math: 1024 * 1024 / (3/4) = 1398101
340                         if [ $((BYTES / 1398101)) -gt $SPLITMB ]; then
341                                 # create a very temp list for just this file
342                                 TARLIST=$(mktemp -t $TMPBASE.$(date +%s).XXXXXX)
343                                 echo "$FILENAME" > "$TARLIST"
344                                 TARBYTES=$BYTES
345                         else
346                                 SUMBYTES=$((SUMBYTES + BYTES))
347                                 echo "$FILENAME" >> $TMPFILE
348
349                                 # not large enough input list, keep collecting
350                                 [ $((SUMBYTES >> 20)) -lt $SPLITMB ] && continue
351
352                                 TARBYTES=$SUMBYTES
353                                 SUMBYTES=0
354                                 TARLIST="$TMPFILE"
355                                 TMPFILE=$(mktemp -t $TMPBASE.$(date +%s).XXXXXXX)
356                         fi
357
358                         wait $TARPID
359                         RC=$?
360                         run_one_backup "$TARLIST" "$OUTPUTFILENUM" $TARBYTES &
361                         TARPID=$!
362                         OUTPUTFILENUM=$((OUTPUTFILENUM + 1))
363                         ;;
364                 esac
365
366                 [ $RC -gt $RCMAX ] && RCMAX=$RC
367         done
368
369         if [ "$TARPID" ]; then
370                 wait $TARPID
371                 RC=$?
372                 [ $RC -gt $RCMAX ] && RCMAX=$RC
373         fi
374
375         if [ -s "$TMPFILE" ]; then
376                 run_one_backup "$TMPFILE" "$OUTPUTFILENUM" $SUMBYTES
377                 RC=$?
378                 [ $RC -gt $RCMAX ] && RCMAX=$RC
379         fi
380         exit $RCMAX
381 }
382
383 # If we are a client then just run that subroutine and exit
384 [ "$NODENUM" ] && run_remotely && exit 0
385
386 # Tell the clients to exit.  Their input pipes might be busy so it may
387 # take a while for them to consume the files and finish.
388 CLEANING=no
389 cleanup() {
390         log "cleaning up remote processes"
391         for FD in $(seq $BASEFD $((BASEFD + NUMCLI - 1))); do
392                 echo "$DONE_MSG" >&$FD
393         done
394         CLEANING=yes
395
396         SLEPT=0
397         RUN=$(ps auxww | egrep -v "grep|bash" | grep -c "$PROGNAME.*remote")
398         while [ $RUN -gt 0 ]; do
399                 set +vx
400                 #ps auxww | grep "$PROGNAME.*remote" | egrep -v "grep|bash"
401                 sleep 1
402                 SLEPT=$((SLEPT + 1))
403                 [ $((SLEPT % 30)) -eq 0 ] &&
404                         log "wait for $RUN processes to finish"
405                 [ $((SLEPT % 300)) -eq 0 ] &&
406                         ps auxww |grep "$PROGNAME.*remote" |egrep -v "grep|bash"
407                 RUN=$(ps auxww|egrep -v "grep|bash"|grep -c "$PROGNAME.*remote")
408         done
409         trap 0
410 }
411
412 do_cleanup() {
413         if [ "$CLEANING" = "yes" ]; then
414                 log "killing all remote processes - may not stop immediately"
415                 for FD in $(seq $BASEFD $((BASEFD + NUMCLI - 1))); do
416                         echo "$KILL_MSG" >&$FD
417                 done
418                 sleep 1
419                 PROCS=$(ps auxww|awk '/$PROGNAME.*remote/ { print $2 }')
420                 [ "$PROCS" ] && kill -9 $PROCS
421                 trap 0
422         fi
423
424         cleanup
425 }
426
427 # values that only need to be determined on the master
428 # always read from stdin, even if it is a file, to be more consistent
429 case "$INPUT" in
430         -|"")   INPUT="standard input";;
431         *)      if [ ! -r "$INPUT" ]; then
432                         [ "$VERBOSE" ] && ls -l "$INPUT"
433                         usage "can't read input file '$INPUT'"
434                 fi
435                 exec <$INPUT ;;
436 esac
437
438 # if unspecified, run remote clients in the current PWD to get correct paths
439 [ -z "$GOTODIR" ] && ARGS="$ARGS -C \"$PWD\""
440
441 # main()
442 BASEFD=100
443 NUMCLI=0
444
445 # Check if the input list has the file size specified or not.  Input
446 # lines should be of the form "{bytes} {filename}" or "{filename}".
447 # If no size is given then the file sizes are determined by the clients
448 # to do the chunking (useful for making a full backup, but not as good
449 # at evenly distributing the data among clients).  In rare cases the first
450 # file specified may have a blank line and no size - check that as well.
451 if [ "$OP" = "backup" ]; then
452         read BYTES FILENAME
453         if [ -z "$FILENAME" -a -e "$BYTES" ]; then
454                 FILENAME="$BYTES"
455                 BYTES=""
456                 FILEONLY="yes" && ARGS="$ARGS --fileonly"
457         elif [ -e "$BYTES $FILENAME" ]; then
458                 FILENAME="$BYTES $FILENAME"
459                 BYTES=""
460                 FILEONLY="yes" && ARGS="$ARGS --fileonly"
461         elif [ ! -e "$FILENAME" ]; then
462                 log "input was '$BYTES $FILENAME'"
463                 fatal "first line of '$INPUT' is not a file"
464         fi
465 else
466         FILEONLY="yes" && ARGS="$ARGS --fileonly"
467 fi
468
469 # kill the $RSH processes if we get a signal
470 trap do_cleanup INT EXIT
471
472 # start up the remote processes, each one with its stdin attached to a
473 # different output file descriptor, so that we can communicate with them
474 # individually when sending files to back up.  We generate a remote log
475 # file and also return output to this process.
476 for CLIENT in $(echo $NODELIST | tr ',' ' '); do
477         FD=$((BASEFD+NUMCLI))
478         LOG=$OUTPUTBASE.$CLIENT.$FD.log
479         eval "exec $FD> >($RSH $CLIENT '$PROGPATH --remote=$NUMCLI $ARGS')"
480         RC=$?
481         if [ $RC -eq 0 ]; then
482                 log "starting $0.$NUMCLI on $CLIENT"
483                 NUMCLI=$((NUMCLI + 1))
484         else
485                 log "ERROR: failed '$RSH $CLIENT $PROGPATH': RC=$?"
486         fi
487 done
488
489 if [ $NUMCLI -eq 0 ]; then
490         fatal "unable to start any threads"
491 fi
492
493 CURRCLI=0
494 # We don't want to use "BYTES FILENAME" if the input doesn't include the
495 # size, as this might cause problems with files with whitespace in them.
496 # Instead we just have two different loops depending on whether the size
497 # is in the input file or not.  We dish out the files either by size
498 # (to fill a chunk), or just round-robin and hope for the best.
499 if [ "$FILEONLY" ]; then
500         if [ "$FILENAME" ]; then
501                 [ "$VERBOSE" ] && log "$FILENAME"
502                 echo "$FILENAME" 1>&$BASEFD     # rewrite initial line
503         fi
504         # if we don't know the size, just round-robin among the clients
505         while read FILENAME; do
506                 FD=$((BASEFD+CURRCLI))
507                 [ -n "$VERBOSE" -a "$OP" != "backup" ] && log "$OP $FILENAME"
508                 echo "$FILENAME" 1>&$FD
509
510                 COUNT=$((COUNT + 1))
511                 if [ $COUNT -ge $SPLITCOUNT ]; then
512                         CURRCLI=$(((CURRCLI + 1) % NUMCLI))
513                         COUNT=0
514                 fi
515         done
516 else
517         [ "$VERBOSE" ] && log "$FILENAME"
518         echo $BYTES "$FILENAME" 1>&$BASEFD      # rewrite initial line
519         # if we know the size, then give each client enough to start a chunk
520         while read BYTES FILENAME; do
521                 FD=$((BASEFD+CURRCLI))
522                 [ "$VERBOSE" ] && log "$FILENAME"
523                 echo $BYTES "$FILENAME" >&$FD
524
525                 # take tar blocking factor into account
526                 [ $BYTES -lt 10240 ] && BYTES=10240
527                 SUMBYTES=$((SUMBYTES + BYTES))
528                 if [ $((SUMBYTES / 1048576)) -ge $SPLITMB ]; then
529                         CURRCLI=$(((CURRCLI + 1) % NUMCLI))
530                         SUMBYTES=0
531                 fi
532         done
533 fi
534
535 # Once all of the files have been given out, wait for the remote processes
536 # to complete.  That might take a while depending on the size of the backup.
537 cleanup