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