1bb28c1982faa29a96380f4b572ab65bc928c585
[BackupPC.git] / bin / BackupPC_dump
1 #!/bin/perl -T
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_dump: Dump a single client.
5 #
6 # DESCRIPTION
7 #
8 #   Usage: BackupPC_dump [-i] [-f] [-d] [-e] <client>
9 #
10 #   Flags:
11 #
12 #     -i   Do an incremental dump, overriding any scheduling (but a full
13 #          dump will be done if no dumps have yet succeeded)
14 #
15 #     -f   Do a full dump, overriding any scheduling.
16 #
17 #     -d   Host is a DHCP pool address, and the client argument
18 #          just an IP address.  We lookup the NetBios name from
19 #          the IP address.
20 #
21 #     -e   Just do an dump expiry check for the client.  Don't do anything
22 #          else.  This is used periodically by BackupPC to make sure that
23 #          dhcp hosts have correctly expired old backups.  Without this,
24 #          dhcp hosts that are no longer on the network will not expire
25 #          old backups.
26 #
27 #   BackupPC_dump is run periodically by BackupPC to backup $client.
28 #   The file $TopDir/pc/$client/backups is read to decide whether a
29 #   full or incremental backup needs to be run.  If no backup is
30 #   scheduled, or a ping to $client fails, then BackupPC_dump quits.
31 #
32 #   The backup is done using the selected XferMethod (smb, tar, rsync etc),
33 #   extracting the dump into $TopDir/pc/$client/new.  The xfer output is
34 #   put into $TopDir/pc/$client/XferLOG.
35 #
36 #   If the dump succeeds (based on parsing the output of the XferMethod):
37 #     - $TopDir/pc/$client/new is renamed to $TopDir/pc/$client/nnn, where
38 #           nnn is the next sequential dump number.
39 #     - $TopDir/pc/$client/XferLOG is renamed to $TopDir/pc/$client/XferLOG.nnn.
40 #     - $TopDir/pc/$client/backups is updated.
41 #
42 #   If the dump fails:
43 #     - $TopDir/pc/$client/new is moved to $TopDir/trash for later removal.
44 #     - $TopDir/pc/$client/XferLOG is renamed to $TopDir/pc/$client/XferLOG.bad
45 #           for later viewing.
46 #
47 #   BackupPC_dump communicates to BackupPC via printing to STDOUT.
48 #
49 # AUTHOR
50 #   Craig Barratt  <cbarratt@users.sourceforge.net>
51 #
52 # COPYRIGHT
53 #   Copyright (C) 2001  Craig Barratt
54 #
55 #   This program is free software; you can redistribute it and/or modify
56 #   it under the terms of the GNU General Public License as published by
57 #   the Free Software Foundation; either version 2 of the License, or
58 #   (at your option) any later version.
59 #
60 #   This program is distributed in the hope that it will be useful,
61 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
62 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
63 #   GNU General Public License for more details.
64 #
65 #   You should have received a copy of the GNU General Public License
66 #   along with this program; if not, write to the Free Software
67 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
68 #
69 #========================================================================
70 #
71 # Version 2.0.0_CVS, released 3 Feb 2003.
72 #
73 # See http://backuppc.sourceforge.net.
74 #
75 #========================================================================
76
77 use strict;
78 use lib "/usr/local/BackupPC/lib";
79 use BackupPC::Lib;
80 use BackupPC::FileZIO;
81 use BackupPC::Xfer::Smb;
82 use BackupPC::Xfer::Tar;
83 use BackupPC::Xfer::Rsync;
84 use File::Path;
85 use Getopt::Std;
86
87 ###########################################################################
88 # Initialize
89 ###########################################################################
90
91 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
92 my $TopDir = $bpc->TopDir();
93 my $BinDir = $bpc->BinDir();
94 my %Conf   = $bpc->Conf();
95 my $NeedPostCmd;
96
97 $bpc->ChildInit();
98
99 my %opts;
100 getopts("defi", \%opts);
101 if ( @ARGV != 1 ) {
102     print("usage: $0 [-d] [-e] [-f] [-i] <client>\n");
103     exit(1);
104 }
105 if ( $ARGV[0] !~ /^([\w\.\s-]+)$/ ) {
106     print("$0: bad client name '$ARGV[0]'\n");
107     exit(1);
108 }
109 my $client = $1;   # BackupPC's client name (might not be real host name)
110 my $hostIP;        # this is the IP address
111 my $host;          # this is the real host name
112
113 my($clientURI, $user);
114
115 if ( $opts{d} ) {
116     #
117     # The client name $client is simply a DHCP address.  We need to check
118     # if there is any machine at this address, and if so, get the actual
119     # host name via NetBios using nmblookup.
120     #
121     $hostIP = $client;
122     exit(1) if ( $bpc->CheckHostAlive($hostIP) < 0
123                     || $Conf{NmbLookupCmd} eq "" );
124     ($client, $user) = $bpc->NetBiosInfoGet($hostIP);
125     exit(1) if ( $host !~ /^([\w\.\s-]+)$/ );
126     my $hosts = $bpc->HostInfoRead($client);
127     exit(1) if ( !defined($hosts->{$client}) );
128     $host = $client;
129 }
130
131 my $Dir     = "$TopDir/pc/$client";
132 my $xferPid = -1;
133 my $tarPid  = -1;
134
135 #
136 # Re-read config file, so we can include the PC-specific config
137 #
138 $clientURI = $bpc->uriEsc($client);
139 if ( defined(my $error = $bpc->ConfigRead($client)) ) {
140     print("dump failed: Can't read PC's config file: $error\n");
141     exit(1);
142 }
143 %Conf = $bpc->Conf();
144
145 #
146 # Catch various signals
147 #
148 $SIG{INT}  = \&catch_signal;
149 $SIG{ALRM} = \&catch_signal;
150 $SIG{TERM} = \&catch_signal;
151 $SIG{PIPE} = \&catch_signal;
152 my $Pid = $$;
153
154 #
155 # Make sure we eventually timeout if there is no activity from
156 # the data transport program.
157 #
158 alarm($Conf{ClientTimeout});
159
160 mkpath($Dir, 0, 0777) if ( !-d $Dir );
161 if ( !-f "$Dir/LOCK" ) {
162     open(LOCK, ">", "$Dir/LOCK") && close(LOCK);
163 }
164 open(LOG, ">>", "$Dir/LOG");
165 select(LOG); $| = 1; select(STDOUT);
166
167 if ( !$opts{d} ) {
168     #
169     # In the non-DHCP case, make sure the host can be looked up
170     # via NS, or otherwise find the IP address via NetBios.
171     #
172     if ( $Conf{ClientNameAlias} ne "" ) {
173         $host = $Conf{ClientNameAlias};
174     } else {
175         $host = $client;
176     }
177     if ( !defined(gethostbyname($host)) ) {
178         #
179         # Ok, NS doesn't know about it.  Maybe it is a NetBios name
180         # instead.
181         #
182         if ( !defined($hostIP = $bpc->NetBiosHostIPFind($host)) ) {
183             print(LOG $bpc->timeStamp,
184                             "dump failed: Can't find host $host\n");
185             print("dump failed: Can't find host $host\n");
186             exit(1);
187         }
188     } else {
189         $hostIP = $host;
190     }
191 }
192
193 ###########################################################################
194 # Figure out what to do and do it
195 ###########################################################################
196
197 #
198 # For the -e option we just expire backups and quit
199 #
200 if ( $opts{e} ) {
201     BackupExpire($client);
202     exit(0);
203 }
204
205 #
206 # See if we should skip this host during a certain range
207 # of times.
208 #
209 my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
210 if ( $err ne "" ) {
211     print("Can't connect to server ($err)\n");
212     print(LOG $bpc->timeStamp, "Can't connect to server ($err)\n");
213     exit(1);
214 }
215 my $reply = $bpc->ServerMesg("status host($clientURI)");
216 $reply = $1 if ( $reply =~ /(.*)/s );
217 my(%StatusHost);
218 eval($reply);
219 $bpc->ServerDisconnect();
220
221 #
222 # For DHCP tell BackupPC which host this is
223 #
224 if ( $opts{d} ) {
225     if ( $StatusHost{activeJob} ) {
226         # oops, something is already running for this host
227         exit(0);
228     }
229     print("DHCP $hostIP $clientURI\n");
230 }
231
232 my($needLink, @Backups, $type, $lastBkupNum, $lastFullBkupNum);
233 my $lastFull = 0;
234 my $lastIncr = 0;
235
236 if ( $Conf{FullPeriod} == -1 && !$opts{f} && !$opts{i}
237         || $Conf{FullPeriod} == -2 ) {
238     NothingToDo($needLink);
239 }
240
241 if ( !$opts{i} && !$opts{f} && $Conf{BlackoutGoodCnt} >= 0
242              && $StatusHost{aliveCnt} >= $Conf{BlackoutGoodCnt} ) {
243     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
244     my($currHours) = $hour + $min / 60 + $sec / 3600;
245     if ( $Conf{BlackoutHourBegin} <= $currHours
246               && $currHours <= $Conf{BlackoutHourEnd}
247               && grep($_ == $wday, @{$Conf{BlackoutWeekDays}}) ) {
248 #        print(LOG $bpc->timeStamp, "skipping because of blackout"
249 #                    . " (alive $StatusHost{aliveCnt} times)\n");
250         NothingToDo($needLink);
251     }
252 }
253
254 if ( !$opts{i} && !$opts{f} && $StatusHost{backoffTime} > time ) {
255     printf(LOG "%sskipping because of user requested delay (%.1f hours left)",
256                 $bpc->timeStamp, ($StatusHost{backoffTime} - time) / 3600);
257     NothingToDo($needLink);
258 }
259
260 #
261 # Now see if there are any old backups we should delete
262 #
263 BackupExpire($client);
264
265 #
266 # Read Backup information, and find times of the most recent full and
267 # incremental backups
268 #
269 @Backups = $bpc->BackupInfoRead($client);
270 for ( my $i = 0 ; $i < @Backups ; $i++ ) {
271     $needLink = 1 if ( $Backups[$i]{nFilesNew} eq ""
272                         || -f "$Dir/NewFileList.$Backups[$i]{num}" );
273     $lastBkupNum = $Backups[$i]{num};
274     if ( $Backups[$i]{type} eq "full" ) {
275         if ( $lastFull < $Backups[$i]{startTime} ) {
276             $lastFull = $Backups[$i]{startTime};
277             $lastFullBkupNum = $Backups[$i]{num};
278         }
279     } else {
280         $lastIncr = $Backups[$i]{startTime}
281                 if ( $lastIncr < $Backups[$i]{startTime} );
282     }
283 }
284
285 #
286 # Decide whether we do nothing, or a full or incremental backup.
287 #
288 if ( @Backups == 0
289         || $opts{f}
290         || (!$opts{i} && (time - $lastFull > $Conf{FullPeriod} * 24*3600
291             && time - $lastIncr > $Conf{IncrPeriod} * 24*3600)) ) {
292     $type = "full";
293 } elsif ( $opts{i} || (time - $lastIncr > $Conf{IncrPeriod} * 24*3600
294         && time - $lastFull > $Conf{IncrPeriod} * 24*3600) ) {
295     $type = "incr";
296 } else {
297     NothingToDo($needLink);
298 }
299
300 #
301 # Check if $host is alive
302 #
303 my $delay = $bpc->CheckHostAlive($hostIP);
304 if ( $delay < 0 ) {
305     print(LOG $bpc->timeStamp, "no ping response\n");
306     print("no ping response\n");
307     print("link $clientURI\n") if ( $needLink );
308     exit(1);
309 } elsif ( $delay > $Conf{PingMaxMsec} ) {
310     printf(LOG "%sping too slow: %.4gmsec\n", $bpc->timeStamp, $delay);
311     printf("ping too slow: %.4gmsec (threshold is %gmsec)\n",
312                     $delay, $Conf{PingMaxMsec});
313     print("link $clientURI\n") if ( $needLink );
314     exit(1);
315 }
316
317 #
318 # Make sure it is really the machine we expect (only for fixed addresses,
319 # since we got the DHCP address above).
320 #
321 if ( !$opts{d} && (my $errMsg = CorrectHostCheck($hostIP, $host)) ) {
322     print(LOG $bpc->timeStamp, "dump failed: $errMsg\n");
323     print("dump failed: $errMsg\n");
324     exit(1);
325 } elsif ( $opts{d} ) {
326     print(LOG $bpc->timeStamp, "$host is dhcp $hostIP, user is $user\n");
327 }
328
329 #
330 # Get a clean directory $Dir/new
331 #
332 $bpc->RmTreeDefer("$TopDir/trash", "$Dir/new") if ( -d "$Dir/new" );
333
334 #
335 # Setup file extension for compression and open XferLOG output file
336 #
337 $Conf{CompressLevel} = 0 if ( !BackupPC::FileZIO->compOk );
338 my $fileExt = $Conf{CompressLevel} > 0 ? ".z" : "";
339 my $XferLOG = BackupPC::FileZIO->open("$Dir/XferLOG$fileExt", 1,
340                                      $Conf{CompressLevel});
341 if ( !defined($XferLOG) ) {
342     print(LOG $bpc->timeStamp, "dump failed: unable to open/create"
343                              . " $Dir/XferLOG$fileExt\n");
344     print("dump failed: unable to open/create $Dir/XferLOG$fileExt\n");
345     exit(1);
346 }
347 unlink("$Dir/NewFileList");
348 my $startTime = time();
349
350 my $tarErrs       = 0;
351 my $nFilesExist   = 0;
352 my $sizeExist     = 0;
353 my $sizeExistComp = 0;
354 my $nFilesTotal   = 0;
355 my $sizeTotal     = 0;
356 my($logMsg, %stat, $xfer, $ShareNames);
357 my $newFilesFH;
358
359 if ( $Conf{XferMethod} eq "tar" ) {
360     $ShareNames = $Conf{TarShareName};
361 } elsif ( $Conf{XferMethod} eq "rsync" || $Conf{XferMethod} eq "rsyncd" ) {
362     $ShareNames = $Conf{RsyncShareName};
363 } else {
364     $ShareNames = $Conf{SmbShareName};
365 }
366
367 $ShareNames = [ $ShareNames ] unless ref($ShareNames) eq "ARRAY";
368
369 #
370 # Run an optional pre-dump command
371 #
372 UserCommandRun("DumpPreUserCmd");
373 $NeedPostCmd = 1;
374
375 #
376 # Now backup each of the shares
377 #
378 for my $shareName ( @$ShareNames ) {
379     local(*RH, *WH);
380
381     $stat{xferOK} = $stat{hostAbort} = undef;
382     $stat{hostError} = $stat{lastOutputLine} = undef;
383     if ( -d "$Dir/new/$shareName" ) {
384         print(LOG $bpc->timeStamp,
385                   "unexpected repeated share name $shareName skipped\n");
386         next;
387     }
388
389     if ( $Conf{XferMethod} eq "tar" ) {
390         #
391         # Use tar (eg: tar/ssh) as the transport program.
392         #
393         $xfer = BackupPC::Xfer::Tar->new($bpc);
394     } elsif ( $Conf{XferMethod} eq "rsync" || $Conf{XferMethod} eq "rsyncd" ) {
395         #
396         # Use rsync as the transport program.
397         #
398         if ( !defined($xfer = BackupPC::Xfer::Rsync->new($bpc)) ) {
399             my $errStr = BackupPC::Xfer::Rsync::errStr;
400             print(LOG $bpc->timeStamp, "dump failed: $errStr\n");
401             print("dump failed: $errStr\n");
402             UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
403             exit(1);
404         }
405     } else {
406         #
407         # Default is to use smbclient (smb) as the transport program.
408         #
409         $xfer = BackupPC::Xfer::Smb->new($bpc);
410     }
411
412     my $useTar = $xfer->useTar;
413
414     if ( $useTar ) {
415         #
416         # This xfer method outputs a tar format file, so we start a
417         # BackupPC_tarExtract to extract the data.
418         #
419         # Create a pipe to connect the Xfer method to BackupPC_tarExtract
420         # WH is the write handle for writing, provided to the transport
421         # program, and RH is the other end of the pipe for reading,
422         # provided to BackupPC_tarExtract.
423         #
424         pipe(RH, WH);
425
426         #
427         # fork a child for BackupPC_tarExtract.  TAR is a file handle
428         # on which we (the parent) read the stdout & stderr from
429         # BackupPC_tarExtract.
430         #
431         if ( !defined($tarPid = open(TAR, "-|")) ) {
432             print(LOG $bpc->timeStamp, "can't fork to run tar\n");
433             print("can't fork to run tar\n");
434             close(RH);
435             close(WH);
436             last;
437         }
438         if ( !$tarPid ) {
439             #
440             # This is the tar child.  Close the write end of the pipe,
441             # clone STDERR to STDOUT, clone STDIN from RH, and then
442             # exec BackupPC_tarExtract.
443             #
444             setpgrp 0,0;
445             close(WH);
446             close(STDERR);
447             open(STDERR, ">&STDOUT");
448             close(STDIN);
449             open(STDIN, "<&RH");
450             exec("$BinDir/BackupPC_tarExtract", $client, $shareName,
451                          $Conf{CompressLevel});
452             print(LOG $bpc->timeStamp,
453                         "can't exec $BinDir/BackupPC_tarExtract\n");
454             exit(0);
455         }
456     } elsif ( !defined($newFilesFH) ) {
457         #
458         # We need to create the NewFileList output file
459         #
460         local(*NEW_FILES);
461         open(NEW_FILES, ">", "$TopDir/pc/$client/NewFileList")
462                      || die("can't open $TopDir/pc/$client/NewFileList");
463         $newFilesFH = *NEW_FILES;
464     }
465
466     #
467     # Run the transport program
468     #
469     $xfer->args({
470         host        => $host,
471         client      => $client,
472         hostIP      => $hostIP,
473         shareName   => $shareName,
474         pipeRH      => *RH,
475         pipeWH      => *WH,
476         XferLOG     => $XferLOG,
477         newFilesFH  => $newFilesFH,
478         outDir      => $Dir,
479         type        => $type,
480         lastFull    => $lastFull,
481         lastBkupNum => $lastBkupNum,
482         lastFullBkupNum => $lastFullBkupNum,
483         backups     => \@Backups,
484         compress    => $Conf{CompressLevel},
485         XferMethod  => $Conf{XferMethod},
486     });
487
488     if ( !defined($logMsg = $xfer->start()) ) {
489         print(LOG $bpc->timeStamp, "xfer start failed: ", $xfer->errStr, "\n");
490         print("dump failed: ", $xfer->errStr, "\n");
491         print("link $clientURI\n") if ( $needLink );
492         #
493         # kill off the tar process, first nicely then forcefully
494         #
495         if ( $tarPid > 0 ) {
496             kill(2, $tarPid);
497             sleep(1);
498             kill(9, $tarPid);
499         }
500         UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
501         exit(1);
502     }
503
504     $xferPid = $xfer->xferPid;
505     if ( $useTar ) {
506         #
507         # The parent must close both handles on the pipe since the children
508         # are using these handles now.
509         #
510         close(RH);
511         close(WH);
512         print(LOG $bpc->timeStamp, $logMsg,
513                                    " (xferPid=$xferPid, tarPid=$tarPid)\n");
514     } elsif ( $xferPid > 0 ) {
515         print(LOG $bpc->timeStamp, $logMsg, " (xferPid=$xferPid)\n");
516     } else {
517         print(LOG $bpc->timeStamp, $logMsg, "\n");
518     }
519     print("started $type dump, pid=$xferPid, tarPid=$tarPid,"
520             . " share=$shareName\n");
521
522     if ( $useTar || $xferPid > 0 ) {
523         #
524         # Parse the output of the transfer program and BackupPC_tarExtract
525         # while they run.  Since we might be reading from two or more children
526         # we use a select.
527         #
528         my($FDread, $tarOut, $mesg);
529         vec($FDread, fileno(TAR), 1) = 1 if ( $useTar );
530         $xfer->setSelectMask(\$FDread);
531
532         SCAN: while ( 1 ) {
533             my $ein = $FDread;
534             last if ( $FDread =~ /^\0*$/ );
535             select(my $rout = $FDread, undef, $ein, undef);
536             if ( $useTar ) {
537                 if ( vec($rout, fileno(TAR), 1) ) {
538                     if ( sysread(TAR, $mesg, 8192) <= 0 ) {
539                         vec($FDread, fileno(TAR), 1) = 0;
540                         close(TAR);
541                     } else {
542                         $tarOut .= $mesg;
543                     }
544                 }
545                 while ( $tarOut =~ /(.*?)[\n\r]+(.*)/s ) {
546                     $_ = $1;
547                     $tarOut = $2;
548                     $XferLOG->write(\"tarExtract: $_\n");
549                     if ( /^Done: (\d+) errors, (\d+) filesExist, (\d+) sizeExist, (\d+) sizeExistComp, (\d+) filesTotal, (\d+) sizeTotal/ ) {
550                         $tarErrs       += $1;
551                         $nFilesExist   += $2;
552                         $sizeExist     += $3;
553                         $sizeExistComp += $4;
554                         $nFilesTotal   += $5;
555                         $sizeTotal     += $6;
556                     }
557                 }
558             }
559             last if ( !$xfer->readOutput(\$FDread, $rout) );
560             while ( my $str = $xfer->logMsgGet ) {
561                 print(LOG $bpc->timeStamp, "xfer: $str\n");
562             }
563             if ( $xfer->getStats->{fileCnt} == 1 ) {
564                 #
565                 # Make sure it is still the machine we expect.  We do this while
566                 # the transfer is running to avoid a potential race condition if
567                 # the ip address was reassigned by dhcp just before we started
568                 # the transfer.
569                 #
570                 if ( my $errMsg = CorrectHostCheck($hostIP, $host) ) {
571                     $stat{hostError} = $errMsg;
572                     last SCAN;
573                 }
574             }
575         }
576     } else {
577         #
578         # otherwise the xfer module does everything for us
579         #
580         my @results = $xfer->run();
581         $tarErrs       += $results[0];
582         $nFilesExist   += $results[1];
583         $sizeExist     += $results[2];
584         $sizeExistComp += $results[3];
585         $nFilesTotal   += $results[4];
586         $sizeTotal     += $results[5];
587     }
588
589     #
590     # Merge the xfer status (need to accumulate counts)
591     #
592     my $newStat = $xfer->getStats;
593     foreach my $k ( (keys(%stat), keys(%$newStat)) ) {
594         next if ( !defined($newStat->{$k}) );
595         if ( $k =~ /Cnt$/ ) {
596             $stat{$k} += $newStat->{$k};
597             delete($newStat->{$k});
598             next;
599         }
600         if ( !defined($stat{$k}) ) {
601             $stat{$k} = $newStat->{$k};
602             delete($newStat->{$k});
603             next;
604         }
605     }
606     $stat{xferOK} = 0 if ( $stat{hostError} || $stat{hostAbort} );
607     if ( !$stat{xferOK} ) {
608         #
609         # kill off the tranfer program, first nicely then forcefully
610         #
611         if ( $xferPid > 0 ) {
612             kill(2, $xferPid);
613             sleep(1);
614             kill(9, $xferPid);
615         }
616         #
617         # kill off the tar process, first nicely then forcefully
618         #
619         if ( $tarPid > 0 ) {
620             kill(2, $tarPid);
621             sleep(1);
622             kill(9, $tarPid);
623         }
624         #
625         # don't do any more shares on this host
626         #
627         last;
628     }
629 }
630 my $lastNum  = -1;
631
632 #
633 # Do one last check to make sure it is still the machine we expect.
634 #
635 if ( $stat{xferOK} && (my $errMsg = CorrectHostCheck($hostIP, $host)) ) {
636     $stat{hostError} = $errMsg;
637     $stat{xferOK} = 0;
638 }
639
640 UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
641 $XferLOG->close();
642 close($newFilesFH) if ( defined($newFilesFH) );
643
644 if ( $stat{xferOK} ) {
645     @Backups = $bpc->BackupInfoRead($client);
646     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
647         $lastNum = $Backups[$i]{num} if ( $lastNum < $Backups[$i]{num} );
648     }
649     $lastNum++;
650     $bpc->RmTreeDefer("$TopDir/trash", "$Dir/$lastNum")
651                                 if ( -d "$Dir/$lastNum" );
652     if ( !rename("$Dir/new", "$Dir/$lastNum") ) {
653         print(LOG $bpc->timeStamp,
654                   "Rename $Dir/new -> $Dir/$lastNum failed\n");
655         $stat{xferOK} = 0;
656     }
657     rename("$Dir/XferLOG$fileExt", "$Dir/XferLOG.$lastNum$fileExt");
658     rename("$Dir/NewFileList", "$Dir/NewFileList.$lastNum");
659 }
660 my $endTime = time();
661
662 #
663 # If the dump failed, clean up
664 #
665 if ( !$stat{xferOK} ) {
666     #
667     # wait a short while and see if the system is still alive
668     #
669     $stat{hostError} = $stat{lastOutputLine} if ( $stat{hostError} eq "" );
670     if ( $stat{hostError} ) {
671         print(LOG $bpc->timeStamp,
672                   "Got fatal error during xfer ($stat{hostError})\n");
673     }
674     sleep(10);
675     if ( $bpc->CheckHostAlive($hostIP) < 0 ) {
676         $stat{hostAbort} = 1;
677     }
678     if ( $stat{hostAbort} ) {
679         $stat{hostError} = "lost network connection during backup";
680     }
681     print(LOG $bpc->timeStamp, "Dump aborted ($stat{hostError})\n");
682     unlink("$Dir/timeStamp.level0");
683     unlink("$Dir/SmbLOG.bad");
684     unlink("$Dir/SmbLOG.bad$fileExt");
685     unlink("$Dir/XferLOG.bad");
686     unlink("$Dir/XferLOG.bad$fileExt");
687     unlink("$Dir/NewFileList");
688     rename("$Dir/XferLOG$fileExt", "$Dir/XferLOG.bad$fileExt");
689     $bpc->RmTreeDefer("$TopDir/trash", "$Dir/new") if ( -d "$Dir/new" );
690     print("dump failed: $stat{hostError}\n");
691     print("link $clientURI\n") if ( $needLink );
692     exit(1);
693 }
694
695 #
696 # Add the new backup information to the backup file
697 #
698 @Backups = $bpc->BackupInfoRead($client);
699 my $i = @Backups;
700 $Backups[$i]{num}           = $lastNum;
701 $Backups[$i]{type}          = $type;
702 $Backups[$i]{startTime}     = $startTime;
703 $Backups[$i]{endTime}       = $endTime;
704 $Backups[$i]{size}          = $sizeTotal;
705 $Backups[$i]{nFiles}        = $nFilesTotal;
706 $Backups[$i]{xferErrs}      = $stat{xferErrCnt} || 0;
707 $Backups[$i]{xferBadFile}   = $stat{xferBadFileCnt} || 0;
708 $Backups[$i]{xferBadShare}  = $stat{xferBadShareCnt} || 0;
709 $Backups[$i]{nFilesExist}   = $nFilesExist;
710 $Backups[$i]{sizeExist}     = $sizeExist;
711 $Backups[$i]{sizeExistComp} = $sizeExistComp;
712 $Backups[$i]{tarErrs}       = $tarErrs;
713 $Backups[$i]{compress}      = $Conf{CompressLevel};
714 $Backups[$i]{noFill}        = $type eq "full" ? 0 : 1;
715 $Backups[$i]{mangle}        = 1;        # name mangling always on for v1.04+
716 $bpc->BackupInfoWrite($client, @Backups);
717
718 unlink("$Dir/timeStamp.level0");
719
720 #
721 # Now remove the bad files, replacing them if possible with links to
722 # earlier backups.
723 #
724 foreach my $file ( $xfer->getBadFiles ) {
725     my $j;
726     unlink("$Dir/$lastNum/$file");
727     for ( $j = $i - 1 ; $j >= 0 ; $j-- ) {
728         next if ( !-f "$Dir/$Backups[$j]{num}/$file" );
729         if ( !link("$Dir/$Backups[$j]{num}/$file", "$Dir/$lastNum/$file") ) {
730             print(LOG $bpc->timeStamp,
731                       "Unable to link $lastNum/$file to"
732                     . " $Backups[$j]{num}/$file\n");
733         } else {
734             print(LOG $bpc->timeStamp,
735                       "Bad file $lastNum/$file replaced by link to"
736                     . " $Backups[$j]{num}/$file\n");
737         }
738         last;
739     }
740     if ( $j < 0 ) {
741         print(LOG $bpc->timeStamp,
742                   "Removed bad file $lastNum/$file (no older"
743                 . " copy to link to)\n");
744     }
745 }
746
747 my $otherCount = $stat{xferErrCnt} - $stat{xferBadFileCnt}
748                                    - $stat{xferBadShareCnt};
749 print(LOG $bpc->timeStamp,
750           "$type backup $lastNum complete, $stat{fileCnt} files,"
751         . " $stat{byteCnt} bytes,"
752         . " $stat{xferErrCnt} xferErrs ($stat{xferBadFileCnt} bad files,"
753         . " $stat{xferBadShareCnt} bad shares, $otherCount other)\n");
754
755 BackupExpire($client);
756
757 print("$type backup complete\n");
758
759 ###########################################################################
760 # Subroutines
761 ###########################################################################
762
763 sub NothingToDo
764 {
765     my($needLink) = @_;
766
767     print("nothing to do\n");
768     print("link $clientURI\n") if ( $needLink );
769     exit(0);
770 }
771
772 sub catch_signal
773 {
774     my $signame = shift;
775     my $fileExt = $Conf{CompressLevel} > 0 ? ".z" : "";
776
777     #
778     # Ignore signals in children
779     #
780     return if ( $Pid != $$ );
781
782     print(LOG $bpc->timeStamp, "cleaning up after signal $signame\n");
783     $SIG{$signame} = 'IGNORE';
784     UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
785     $XferLOG->write(\"exiting after signal $signame\n");
786     $XferLOG->close();
787     if ( $xferPid > 0 ) {
788         if ( kill(2, $xferPid) <= 0 ) {
789             sleep(1);
790             kill(9, $xferPid);
791         }
792     }
793     if ( $tarPid > 0 ) {
794         if ( kill(2, $tarPid) <= 0 ) {
795             sleep(1);
796             kill(9, $tarPid);
797         }
798     }
799     unlink("$Dir/timeStamp.level0");
800     unlink("$Dir/NewFileList");
801     unlink("$Dir/XferLOG.bad");
802     unlink("$Dir/XferLOG.bad$fileExt");
803     rename("$Dir/XferLOG$fileExt", "$Dir/XferLOG.bad$fileExt");
804     $bpc->RmTreeDefer("$TopDir/trash", "$Dir/new") if ( -d "$Dir/new" );
805     if ( $signame eq "INT" ) {
806         print("dump failed: aborted by user (signal=$signame)\n");
807     } else {
808         print("dump failed: received signal=$signame\n");
809     }
810     print("link $clientURI\n") if ( $needLink );
811     exit(1);
812 }
813
814 #
815 # Decide which old backups should be expired by moving them
816 # to $TopDir/trash.
817 #
818 sub BackupExpire
819 {
820     my($client) = @_;
821     my($Dir) = "$TopDir/pc/$client";
822     my(@Backups) = $bpc->BackupInfoRead($client);
823     my($cntFull, $cntIncr, $firstFull, $firstIncr, $oldestIncr, $oldestFull);
824
825     while ( 1 ) {
826         $cntFull = $cntIncr = 0;
827         $oldestIncr = $oldestFull = 0;
828         for ( $i = 0 ; $i < @Backups ; $i++ ) {
829             if ( $Backups[$i]{type} eq "full" ) {
830                 $firstFull = $i if ( $cntFull == 0 );
831                 $cntFull++;
832             } else {
833                 $firstIncr = $i if ( $cntIncr == 0 );
834                 $cntIncr++;
835             }
836         }
837         $oldestIncr = (time - $Backups[$firstIncr]{startTime}) / (24 * 3600)
838                         if ( $cntIncr > 0 );
839         $oldestFull = (time - $Backups[$firstFull]{startTime}) / (24 * 3600)
840                         if ( $cntFull > 0 );
841         if ( $cntIncr > $Conf{IncrKeepCnt}
842                 || ($cntIncr > $Conf{IncrKeepCntMin}
843                     && $oldestIncr > $Conf{IncrAgeMax})
844                && (@Backups <= $firstIncr + 1
845                         || $Backups[$firstIncr]{noFill}
846                         || !$Backups[$firstIncr + 1]{noFill}) ) {
847             #
848             # Only delete an incr backup if the Conf settings are satisfied.
849             # We also must make sure that either this backup is the most
850             # recent one, or it is not filled, or the next backup is filled.
851             # (We can't deleted a filled incr if the next backup is not
852             # filled.)
853             # 
854             print(LOG $bpc->timeStamp,
855                       "removing incr backup $Backups[$firstIncr]{num}\n");
856             $bpc->RmTreeDefer("$TopDir/trash",
857                               "$Dir/$Backups[$firstIncr]{num}");
858             unlink("$Dir/SmbLOG.$Backups[$firstIncr]{num}")
859                         if ( -f "$Dir/SmbLOG.$Backups[$firstIncr]{num}" );
860             unlink("$Dir/SmbLOG.$Backups[$firstIncr]{num}.z")
861                         if ( -f "$Dir/SmbLOG.$Backups[$firstIncr]{num}.z" );
862             unlink("$Dir/XferLOG.$Backups[$firstIncr]{num}")
863                         if ( -f "$Dir/XferLOG.$Backups[$firstIncr]{num}" );
864             unlink("$Dir/XferLOG.$Backups[$firstIncr]{num}.z")
865                         if ( -f "$Dir/XferLOG.$Backups[$firstIncr]{num}.z" );
866             splice(@Backups, $firstIncr, 1);
867         } elsif ( ($cntFull > $Conf{FullKeepCnt}
868                     || ($cntFull > $Conf{FullKeepCntMin}
869                         && $oldestFull > $Conf{FullAgeMax}))
870                && (@Backups <= $firstFull + 1
871                         || !$Backups[$firstFull + 1]{noFill}) ) {
872             #
873             # Only delete a full backup if the Conf settings are satisfied.
874             # We also must make sure that either this backup is the most
875             # recent one, or the next backup is filled.
876             # (We can't deleted a full backup if the next backup is not
877             # filled.)
878             # 
879             print(LOG $bpc->timeStamp,
880                    "removing full backup $Backups[$firstFull]{num}\n");
881             $bpc->RmTreeDefer("$TopDir/trash",
882                               "$Dir/$Backups[$firstFull]{num}");
883             unlink("$Dir/SmbLOG.$Backups[$firstFull]{num}")
884                         if ( -f "$Dir/SmbLOG.$Backups[$firstFull]{num}" );
885             unlink("$Dir/SmbLOG.$Backups[$firstFull]{num}.z")
886                         if ( -f "$Dir/SmbLOG.$Backups[$firstFull]{num}.z" );
887             unlink("$Dir/XferLOG.$Backups[$firstFull]{num}")
888                         if ( -f "$Dir/XferLOG.$Backups[$firstFull]{num}" );
889             unlink("$Dir/XferLOG.$Backups[$firstFull]{num}.z")
890                         if ( -f "$Dir/XferLOG.$Backups[$firstFull]{num}.z" );
891             splice(@Backups, $firstFull, 1);
892         } else {
893             last;
894         }
895     }
896     $bpc->BackupInfoWrite($client, @Backups);
897 }
898
899 sub CorrectHostCheck
900 {
901     my($hostIP, $host) = @_;
902     return if ( $hostIP eq $host && !$Conf{FixedIPNetBiosNameCheck}
903                 || $Conf{NmbLookupCmd} eq "" );
904     my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($hostIP);
905     return "host $host has mismatching netbios name $netBiosHost"
906                 if ( $netBiosHost ne $host );
907     return;
908 }
909
910 #
911 # Run an optional pre- or post-dump command
912 #
913 sub UserCommandRun
914 {
915     my($type) = @_;
916
917     return if ( !defined($Conf{$type}) );
918     my $vars = {
919         xfer    => $xfer,
920         client  => $client,
921         host    => $host,
922         hostIP  => $hostIP,
923         share   => $ShareNames->[0],
924         shares  => $ShareNames,
925         XferMethod => $Conf{XferMethod},
926         sshPath => $Conf{SshPath},
927         LOG     => *LOG,
928         XferLOG => $XferLOG,
929         stat    => \%stat,
930         xferOK  => $stat{xferOK},
931         type    => $type,
932     };
933     my $cmd = $bpc->cmdVarSubstitute($Conf{$type}, $vars);
934     $XferLOG->write(\"Executing $type: @$cmd\n");
935     #
936     # Run the user's command, dumping the stdout/stderr into the
937     # Xfer log file.  Also supply the optional $vars and %Conf in
938     # case the command is really perl code instead of a shell
939     # command.
940     #
941     $bpc->cmdSystemOrEval($cmd,
942             sub {
943                 $XferLOG->write(\$_[0]);
944             },
945             $vars, \%Conf);
946 }