72f992c14432223f9462ac02343bea7fe10e118d
[BackupPC.git] / bin / BackupPC_dump
1 #!/bin/perl
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_dump: Dump a single client.
5 #
6 # DESCRIPTION
7 #
8 #   Usage: BackupPC_dump [-i] [-f] [-d] [-e] [-v] <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 #     -v   verbose.  for manual usage: prints failure reasons in more detail.
28 #
29 #   BackupPC_dump is run periodically by BackupPC to backup $client.
30 #   The file $TopDir/pc/$client/backups is read to decide whether a
31 #   full or incremental backup needs to be run.  If no backup is
32 #   scheduled, or a ping to $client fails, then BackupPC_dump quits.
33 #
34 #   The backup is done using the selected XferMethod (smb, tar, rsync,
35 #   backuppcd etc), extracting the dump into $TopDir/pc/$client/new.
36 #   The xfer output is put into $TopDir/pc/$client/XferLOG.
37 #
38 #   If the dump succeeds (based on parsing the output of the XferMethod):
39 #     - $TopDir/pc/$client/new is renamed to $TopDir/pc/$client/nnn, where
40 #           nnn is the next sequential dump number.
41 #     - $TopDir/pc/$client/XferLOG is renamed to $TopDir/pc/$client/XferLOG.nnn.
42 #     - $TopDir/pc/$client/backups is updated.
43 #
44 #   If the dump fails:
45 #     - $TopDir/pc/$client/new is moved to $TopDir/trash for later removal.
46 #     - $TopDir/pc/$client/XferLOG is renamed to $TopDir/pc/$client/XferLOG.bad
47 #           for later viewing.
48 #
49 #   BackupPC_dump communicates to BackupPC via printing to STDOUT.
50 #
51 # AUTHOR
52 #   Craig Barratt  <cbarratt@users.sourceforge.net>
53 #
54 # COPYRIGHT
55 #   Copyright (C) 2001-2003  Craig Barratt
56 #
57 #   This program is free software; you can redistribute it and/or modify
58 #   it under the terms of the GNU General Public License as published by
59 #   the Free Software Foundation; either version 2 of the License, or
60 #   (at your option) any later version.
61 #
62 #   This program is distributed in the hope that it will be useful,
63 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
64 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
65 #   GNU General Public License for more details.
66 #
67 #   You should have received a copy of the GNU General Public License
68 #   along with this program; if not, write to the Free Software
69 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
70 #
71 #========================================================================
72 #
73 # Version 3.0.0alpha, released 23 Jan 2006.
74 #
75 # See http://backuppc.sourceforge.net.
76 #
77 #========================================================================
78
79 use strict;
80 no  utf8;
81 use lib "/usr/local/BackupPC/lib";
82 use BackupPC::Lib;
83 use BackupPC::FileZIO;
84 use BackupPC::Storage;
85 use BackupPC::Xfer::Smb;
86 use BackupPC::Xfer::Tar;
87 use BackupPC::Xfer::Rsync;
88 use BackupPC::Xfer::BackupPCd;
89 use Socket;
90 use File::Path;
91 use File::Find;
92 use Getopt::Std;
93
94 ###########################################################################
95 # Initialize
96 ###########################################################################
97
98 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
99 my $TopDir = $bpc->TopDir();
100 my $BinDir = $bpc->BinDir();
101 my %Conf   = $bpc->Conf();
102 my $NeedPostCmd;
103 my $Hosts;
104 my $SigName;
105 my $Abort;
106
107 $bpc->ChildInit();
108
109 my %opts;
110 if ( !getopts("defiv", \%opts) || @ARGV != 1 ) {
111     print("usage: $0 [-d] [-e] [-f] [-i] [-v] <client>\n");
112     exit(1);
113 }
114 if ( $ARGV[0] !~ /^([\w\.\s-]+)$/ ) {
115     print("$0: bad client name '$ARGV[0]'\n");
116     exit(1);
117 }
118 my $client = $1;   # BackupPC's client name (might not be real host name)
119 my $hostIP;        # this is the IP address
120 my $host;          # this is the real host name
121
122 my($clientURI, $user);
123
124 $bpc->verbose(1) if ( $opts{v} );
125
126 if ( $opts{d} ) {
127     #
128     # The client name $client is simply a DHCP address.  We need to check
129     # if there is any machine at this address, and if so, get the actual
130     # host name via NetBios using nmblookup.
131     #
132     $hostIP = $client;
133     if ( $bpc->CheckHostAlive($hostIP) < 0 ) {
134         print(STDERR "Exiting because CheckHostAlive($hostIP) failed\n")
135                             if ( $opts{v} );
136         exit(1);
137     }
138     if ( $Conf{NmbLookupCmd} eq "" ) {
139         print(STDERR "Exiting because \$Conf{NmbLookupCmd} is empty\n")
140                             if ( $opts{v} );
141         exit(1);
142     }
143     ($client, $user) = $bpc->NetBiosInfoGet($hostIP);
144     if ( $client !~ /^([\w\.\s-]+)$/ ) {
145         print(STDERR "Exiting because NetBiosInfoGet($hostIP) returned"
146                    . " '$client', an invalid host name\n") if ( $opts{v} );
147         exit(1)
148     }
149     $Hosts = $bpc->HostInfoRead($client);
150     $host = $client;
151 } else {
152     $Hosts = $bpc->HostInfoRead($client);
153 }
154 if ( !defined($Hosts->{$client}) ) {
155     print(STDERR "Exiting because host $client does not exist in the"
156                . " hosts file\n") if ( $opts{v} );
157     exit(1)
158 }
159
160 my $Dir     = "$TopDir/pc/$client";
161 my @xferPid = ();
162 my $tarPid  = -1;
163
164 #
165 # Re-read config file, so we can include the PC-specific config
166 #
167 $clientURI = $bpc->uriEsc($client);
168 if ( defined(my $error = $bpc->ConfigRead($client)) ) {
169     print("dump failed: Can't read PC's config file: $error\n");
170     exit(1);
171 }
172 %Conf = $bpc->Conf();
173
174 #
175 # Catch various signals
176 #
177 $SIG{INT}  = \&catch_signal;
178 $SIG{ALRM} = \&catch_signal;
179 $SIG{TERM} = \&catch_signal;
180 $SIG{PIPE} = \&catch_signal;
181 $SIG{STOP} = \&catch_signal;
182 $SIG{TSTP} = \&catch_signal;
183 $SIG{TTIN} = \&catch_signal;
184 my $Pid = $$;
185
186 #
187 # Make sure we eventually timeout if there is no activity from
188 # the data transport program.
189 #
190 alarm($Conf{ClientTimeout});
191
192 mkpath($Dir, 0, 0777) if ( !-d $Dir );
193 if ( !-f "$Dir/LOCK" ) {
194     open(LOCK, ">", "$Dir/LOCK") && close(LOCK);
195 }
196 open(LOG, ">>", "$Dir/LOG");
197 select(LOG); $| = 1; select(STDOUT);
198
199 #
200 # For the -e option we just expire backups and quit
201 #
202 if ( $opts{e} ) {
203     BackupExpire($client);
204     exit(0);
205 }
206
207 #
208 # For archive hosts we don't bother any further
209 #
210 if ($Conf{XferMethod} eq "archive" ) {
211     print(STDERR "Exiting because the XferMethod is set to archive\n")
212                 if ( $opts{v} );
213     exit(0);
214 }
215
216 if ( !$opts{d} ) {
217     #
218     # In the non-DHCP case, make sure the host can be looked up
219     # via NS, or otherwise find the IP address via NetBios.
220     #
221     if ( $Conf{ClientNameAlias} ne "" ) {
222         $host = $Conf{ClientNameAlias};
223     } else {
224         $host = $client;
225     }
226     if ( !defined(gethostbyname($host)) ) {
227         #
228         # Ok, NS doesn't know about it.  Maybe it is a NetBios name
229         # instead.
230         #
231         print(STDERR "Name server doesn't know about $host; trying NetBios\n")
232                         if ( $opts{v} );
233         if ( !defined($hostIP = $bpc->NetBiosHostIPFind($host)) ) {
234             print(LOG $bpc->timeStamp, "Can't find host $host via netbios\n");
235             print("host not found\n");
236             exit(1);
237         }
238     } else {
239         $hostIP = $host;
240     }
241 }
242
243 ###########################################################################
244 # Figure out what to do and do it
245 ###########################################################################
246
247 #
248 # See if we should skip this host during a certain range
249 # of times.
250 #
251 my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
252 if ( $err ne "" ) {
253     print("Can't connect to server ($err)\n");
254     print(LOG $bpc->timeStamp, "Can't connect to server ($err)\n");
255     exit(1);
256 }
257 my $reply = $bpc->ServerMesg("status host($clientURI)");
258 $reply = $1 if ( $reply =~ /(.*)/s );
259 my(%StatusHost);
260 eval($reply);
261 $bpc->ServerDisconnect();
262
263 #
264 # For DHCP tell BackupPC which host this is
265 #
266 if ( $opts{d} ) {
267     if ( $StatusHost{activeJob} ) {
268         # oops, something is already running for this host
269         print(STDERR "Exiting because backup is already running for $client\n")
270                         if ( $opts{v} );
271         exit(0);
272     }
273     print("DHCP $hostIP $clientURI\n");
274 }
275
276 my($needLink, @Backups, $type, $lastBkupNum, $lastFullBkupNum);
277 my $lastFull = 0;
278 my $lastIncr = 0;
279 my $partialIdx = -1;
280 my $partialNum;
281 my $lastPartial = 0;
282
283 if ( $Conf{FullPeriod} == -1 && !$opts{f} && !$opts{i}
284         || $Conf{FullPeriod} == -2 ) {
285     print(STDERR "Exiting because backups are disabled with"
286                . " \$Conf{FullPeriod} = $Conf{FullPeriod}\n") if ( $opts{v} );
287     #
288     # Tell BackupPC to ignore old failed backups on hosts that
289     # have backups disabled.
290     #
291     print("backups disabled\n")
292                 if ( defined($StatusHost{errorTime})
293                      && $StatusHost{reason} ne "Reason_backup_done"
294                      && time - $StatusHost{errorTime} > 4 * 24 * 3600 );
295     NothingToDo($needLink);
296 }
297
298 if ( !$opts{i} && !$opts{f} && $Conf{BlackoutGoodCnt} >= 0
299              && $StatusHost{aliveCnt} >= $Conf{BlackoutGoodCnt} ) {
300     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
301     my($currHours) = $hour + $min / 60 + $sec / 3600;
302     my $blackout;
303
304     #
305     # Handle backward compatibility with original separate scalar
306     # parameters.
307     #
308     if ( defined($Conf{BlackoutHourBegin}) ) {
309         push(@{$Conf{BlackoutPeriods}},
310              {
311                  hourBegin => $Conf{BlackoutHourBegin},
312                  hourEnd   => $Conf{BlackoutHourEnd},
313                  weekDays  => $Conf{BlackoutWeekDays},
314              }
315         );
316     }
317     foreach my $p ( @{$Conf{BlackoutPeriods}} ) {
318         #
319         # Allow blackout to span midnight (specified by BlackoutHourBegin
320         # being greater than BlackoutHourEnd)
321         #
322         next if ( ref($p->{weekDays}) ne "ARRAY" 
323                     || !defined($p->{hourBegin})
324                     || !defined($p->{hourEnd})
325                 );
326         if ( $p->{hourBegin} > $p->{hourEnd} ) {
327             $blackout = $p->{hourBegin} <= $currHours
328                           || $currHours <= $p->{hourEnd};
329             if ( $currHours <= $p->{hourEnd} ) {
330                 #
331                 # This is after midnight, so decrement the weekday for the
332                 # weekday check (eg: Monday 11pm-1am means Monday 2300 to
333                 # Tuesday 0100, not Monday 2300-2400 plus Monday 0000-0100).
334                 #
335                 $wday--;
336                 $wday += 7 if ( $wday < 0 );
337             }
338         } else {
339             $blackout = $p->{hourBegin} <= $currHours
340                           && $currHours <= $p->{hourEnd};
341         }
342         if ( $blackout && grep($_ == $wday, @{$p->{weekDays}}) ) {
343 #           print(LOG $bpc->timeStamp, "skipping because of blackout"
344 #                      . " (alive $StatusHost{aliveCnt} times)\n");
345             print(STDERR "Skipping $client because of blackout\n")
346                             if ( $opts{v} );
347             NothingToDo($needLink);
348         }
349     }
350 }
351
352 if ( !$opts{i} && !$opts{f} && $StatusHost{backoffTime} > time ) {
353     printf(LOG "%sskipping because of user requested delay (%.1f hours left)\n",
354                 $bpc->timeStamp, ($StatusHost{backoffTime} - time) / 3600);
355     NothingToDo($needLink);
356 }
357
358 #
359 # Now see if there are any old backups we should delete
360 #
361 BackupExpire($client);
362
363 #
364 # Read Backup information, and find times of the most recent full and
365 # incremental backups
366 #
367 @Backups = $bpc->BackupInfoRead($client);
368 for ( my $i = 0 ; $i < @Backups ; $i++ ) {
369     $needLink = 1 if ( $Backups[$i]{nFilesNew} eq ""
370                         || -f "$Dir/NewFileList.$Backups[$i]{num}" );
371     $lastBkupNum = $Backups[$i]{num};
372     if ( $Backups[$i]{type} eq "full" ) {
373         if ( $lastFull < $Backups[$i]{startTime} ) {
374             $lastFull = $Backups[$i]{startTime};
375             $lastFullBkupNum = $Backups[$i]{num};
376         }
377     } elsif ( $Backups[$i]{type} eq "incr" ) {
378         $lastIncr = $Backups[$i]{startTime}
379                 if ( $lastIncr < $Backups[$i]{startTime} );
380     } elsif ( $Backups[$i]{type} eq "partial" ) {
381         $partialIdx  = $i;
382         $lastPartial = $Backups[$i]{startTime};
383         $partialNum  = $Backups[$i]{num};
384     }
385 }
386
387 #
388 # Decide whether we do nothing, or a full or incremental backup.
389 #
390 if ( @Backups == 0
391         || $opts{f}
392         || (!$opts{i} && (time - $lastFull > $Conf{FullPeriod} * 24*3600
393             && time - $lastIncr > $Conf{IncrPeriod} * 24*3600)) ) {
394     $type = "full";
395 } elsif ( $opts{i} || (time - $lastIncr > $Conf{IncrPeriod} * 24*3600
396         && time - $lastFull > $Conf{IncrPeriod} * 24*3600) ) {
397     $type = "incr";
398 } else {
399     NothingToDo($needLink);
400 }
401
402 #
403 # Check if $host is alive
404 #
405 my $delay = $bpc->CheckHostAlive($hostIP);
406 if ( $delay < 0 ) {
407     print(LOG $bpc->timeStamp, "no ping response\n");
408     print("no ping response\n");
409     print("link $clientURI\n") if ( $needLink );
410     exit(1);
411 } elsif ( $delay > $Conf{PingMaxMsec} ) {
412     printf(LOG "%sping too slow: %.4gmsec\n", $bpc->timeStamp, $delay);
413     printf("ping too slow: %.4gmsec (threshold is %gmsec)\n",
414                     $delay, $Conf{PingMaxMsec});
415     print("link $clientURI\n") if ( $needLink );
416     exit(1);
417 }
418
419 #
420 # Make sure it is really the machine we expect (only for fixed addresses,
421 # since we got the DHCP address above).
422 #
423 if ( !$opts{d} && (my $errMsg = CorrectHostCheck($hostIP, $host)) ) {
424     print(LOG $bpc->timeStamp, "dump failed: $errMsg\n");
425     print("dump failed: $errMsg\n");
426     exit(1);
427 } elsif ( $opts{d} ) {
428     print(LOG $bpc->timeStamp, "$host is dhcp $hostIP, user is $user\n");
429 }
430
431 #
432 # Get a clean directory $Dir/new
433 #
434 $bpc->RmTreeDefer("$TopDir/trash", "$Dir/new") if ( -d "$Dir/new" );
435
436 #
437 # Setup file extension for compression and open XferLOG output file
438 #
439 if ( $Conf{CompressLevel} && !BackupPC::FileZIO->compOk ) {
440     print(LOG $bpc->timeStamp, "dump failed: can't find Compress::Zlib\n");
441     print("dump failed: can't find Compress::Zlib\n");
442     exit(1);
443 }
444 my $fileExt = $Conf{CompressLevel} > 0 ? ".z" : "";
445 my $XferLOG = BackupPC::FileZIO->open("$Dir/XferLOG$fileExt", 1,
446                                      $Conf{CompressLevel});
447 if ( !defined($XferLOG) ) {
448     print(LOG $bpc->timeStamp, "dump failed: unable to open/create"
449                              . " $Dir/XferLOG$fileExt\n");
450     print("dump failed: unable to open/create $Dir/XferLOG$fileExt\n");
451     exit(1);
452 }
453
454 #
455 # Ignore the partial dump in the case of an incremental
456 # or when the partial is too old.  A partial is a partial full.
457 #
458 if ( $type ne "full" || time - $lastPartial > $Conf{PartialAgeMax} * 24*3600 ) {
459     $partialNum = undef;
460     $partialIdx = -1;
461 }
462
463 #
464 # If this is a partial, copy the old XferLOG file
465 #
466 if ( $partialNum ) {
467     my($compress, $fileName);
468     if ( -f "$Dir/XferLOG.$partialNum.z" ) {
469         $fileName = "$Dir/XferLOG.$partialNum.z";
470         $compress = 1;
471     } elsif ( -f "$Dir/XferLOG.$partialNum" ) {
472         $fileName = "$Dir/XferLOG.$partialNum";
473         $compress = 0;
474     }
475     if ( my $oldLOG = BackupPC::FileZIO->open($fileName, 0, $compress) ) {
476         my $data;
477         while ( $oldLOG->read(\$data, 65536) > 0 ) {
478             $XferLOG->write(\$data);
479         }
480         $oldLOG->close;
481     }
482 }
483
484 $XferLOG->writeTeeStderr(1) if ( $opts{v} );
485 unlink("$Dir/NewFileList") if ( -f "$Dir/NewFileList" );
486
487 my $startTime     = time();
488 my $tarErrs       = 0;
489 my $nFilesExist   = 0;
490 my $sizeExist     = 0;
491 my $sizeExistComp = 0;
492 my $nFilesTotal   = 0;
493 my $sizeTotal     = 0;
494 my($logMsg, %stat, $xfer, $ShareNames, $noFilesErr);
495 my $newFilesFH;
496
497 if ( $Conf{XferMethod} eq "tar" ) {
498     $ShareNames = $Conf{TarShareName};
499 } elsif ( $Conf{XferMethod} eq "rsync" || $Conf{XferMethod} eq "rsyncd" ) {
500     $ShareNames = $Conf{RsyncShareName};
501 } elsif ( $Conf{XferMethod} eq "backuppcd" ) {
502     $ShareNames = $Conf{BackupPCdShareName};
503 } else {
504     $ShareNames = $Conf{SmbShareName};
505 }
506
507 $ShareNames = [ $ShareNames ] unless ref($ShareNames) eq "ARRAY";
508
509 #
510 # Run an optional pre-dump command
511 #
512 UserCommandRun("DumpPreUserCmd");
513 $NeedPostCmd = 1;
514
515 #
516 # Now backup each of the shares
517 #
518 for my $shareName ( @$ShareNames ) {
519     local(*RH, *WH);
520
521     $stat{xferOK} = $stat{hostAbort} = undef;
522     $stat{hostError} = $stat{lastOutputLine} = undef;
523     if ( -d "$Dir/new/$shareName" ) {
524         print(LOG $bpc->timeStamp,
525                   "unexpected repeated share name $shareName skipped\n");
526         next;
527     }
528
529     UserCommandRun("DumpPreShareCmd", $shareName);
530
531     if ( $Conf{XferMethod} eq "tar" ) {
532         #
533         # Use tar (eg: tar/ssh) as the transport program.
534         #
535         $xfer = BackupPC::Xfer::Tar->new($bpc);
536     } elsif ( $Conf{XferMethod} eq "rsync" || $Conf{XferMethod} eq "rsyncd" ) {
537         #
538         # Use rsync as the transport program.
539         #
540         if ( !defined($xfer = BackupPC::Xfer::Rsync->new($bpc)) ) {
541             my $errStr = BackupPC::Xfer::Rsync::errStr;
542             print(LOG $bpc->timeStamp, "dump failed: $errStr\n");
543             print("dump failed: $errStr\n");
544             UserCommandRun("DumpPostShareCmd", $shareName) if ( $NeedPostCmd );
545             UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
546             exit(1);
547         }
548     } elsif ( $Conf{XferMethod} eq "backuppcd" ) {
549         #
550         # Use backuppcd as the transport program.
551         #
552         if ( !defined($xfer = BackupPC::Xfer::BackupPCd->new($bpc)) ) {
553             my $errStr = BackupPC::Xfer::BackupPCd::errStr;
554             print(LOG $bpc->timeStamp, "dump failed: $errStr\n");
555             print("dump failed: $errStr\n");
556             UserCommandRun("DumpPostShareCmd", $shareName) if ( $NeedPostCmd );
557             UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
558             exit(1);
559         }
560     } else {
561         #
562         # Default is to use smbclient (smb) as the transport program.
563         #
564         $xfer = BackupPC::Xfer::Smb->new($bpc);
565     }
566
567     my $useTar = $xfer->useTar;
568
569     if ( $useTar ) {
570         #
571         # This xfer method outputs a tar format file, so we start a
572         # BackupPC_tarExtract to extract the data.
573         #
574         # Create a socketpair to connect the Xfer method to BackupPC_tarExtract
575         # WH is the write handle for writing, provided to the transport
576         # program, and RH is the other end of the socket for reading,
577         # provided to BackupPC_tarExtract.
578         #
579         if ( socketpair(RH, WH, AF_UNIX, SOCK_STREAM, PF_UNSPEC) ) {
580             shutdown(RH, 1);    # no writing to this socket
581             shutdown(WH, 0);    # no reading from this socket
582             setsockopt(RH, SOL_SOCKET, SO_RCVBUF, 8 * 65536);
583             setsockopt(WH, SOL_SOCKET, SO_SNDBUF, 8 * 65536);
584         } else {
585             #
586             # Default to pipe() if socketpair() doesn't work.
587             #
588             pipe(RH, WH);
589         }
590
591         #
592         # fork a child for BackupPC_tarExtract.  TAR is a file handle
593         # on which we (the parent) read the stdout & stderr from
594         # BackupPC_tarExtract.
595         #
596         if ( !defined($tarPid = open(TAR, "-|")) ) {
597             print(LOG $bpc->timeStamp, "can't fork to run tar\n");
598             print("can't fork to run tar\n");
599             close(RH);
600             close(WH);
601             last;
602         }
603         binmode(TAR);
604         if ( !$tarPid ) {
605             #
606             # This is the tar child.  Close the write end of the pipe,
607             # clone STDERR to STDOUT, clone STDIN from RH, and then
608             # exec BackupPC_tarExtract.
609             #
610             setpgrp 0,0;
611             close(WH);
612             close(STDERR);
613             open(STDERR, ">&STDOUT");
614             close(STDIN);
615             open(STDIN, "<&RH");
616             alarm(0);
617             exec("$BinDir/BackupPC_tarExtract", $client, $shareName,
618                          $Conf{CompressLevel});
619             print(LOG $bpc->timeStamp,
620                         "can't exec $BinDir/BackupPC_tarExtract\n");
621             exit(0);
622         }
623     } elsif ( !defined($newFilesFH) ) {
624         #
625         # We need to create the NewFileList output file
626         #
627         local(*NEW_FILES);
628         open(NEW_FILES, ">", "$TopDir/pc/$client/NewFileList")
629                      || die("can't open $TopDir/pc/$client/NewFileList");
630         $newFilesFH = *NEW_FILES;
631         binmode(NEW_FILES);
632     }
633
634     #
635     # Run the transport program
636     #
637     $xfer->args({
638         host        => $host,
639         client      => $client,
640         hostIP      => $hostIP,
641         shareName   => $shareName,
642         pipeRH      => *RH,
643         pipeWH      => *WH,
644         XferLOG     => $XferLOG,
645         newFilesFH  => $newFilesFH,
646         outDir      => $Dir,
647         type        => $type,
648         lastFull    => $lastFull,
649         lastBkupNum => $lastBkupNum,
650         lastFullBkupNum => $lastFullBkupNum,
651         backups     => \@Backups,
652         compress    => $Conf{CompressLevel},
653         XferMethod  => $Conf{XferMethod},
654         logLevel    => $Conf{XferLogLevel},
655         pidHandler  => \&pidHandler,
656         partialNum  => $partialNum,
657     });
658
659     if ( !defined($logMsg = $xfer->start()) ) {
660         print(LOG $bpc->timeStamp, "xfer start failed: ", $xfer->errStr, "\n");
661         print("dump failed: ", $xfer->errStr, "\n");
662         print("link $clientURI\n") if ( $needLink );
663         #
664         # kill off the tar process, first nicely then forcefully
665         #
666         if ( $tarPid > 0 ) {
667             kill($bpc->sigName2num("INT"), $tarPid);
668             sleep(1);
669             kill($bpc->sigName2num("KILL"), $tarPid);
670         }
671         if ( @xferPid ) {
672             kill($bpc->sigName2num("INT"), @xferPid);
673             sleep(1);
674             kill($bpc->sigName2num("KILL"), @xferPid);
675         }
676         UserCommandRun("DumpPostShareCmd", $shareName) if ( $NeedPostCmd );
677         UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
678         exit(1);
679     }
680
681     @xferPid = $xfer->xferPid;
682
683     if ( $useTar ) {
684         #
685         # The parent must close both handles on the pipe since the children
686         # are using these handles now.
687         #
688         close(RH);
689         close(WH);
690     }
691     print(LOG $bpc->timeStamp, $logMsg, "\n");
692     print("started $type dump, share=$shareName\n");
693
694     pidHandler(@xferPid);
695
696     if ( $useTar ) {
697         #
698         # Parse the output of the transfer program and BackupPC_tarExtract
699         # while they run.  Since we might be reading from two or more children
700         # we use a select.
701         #
702         my($FDread, $tarOut, $mesg);
703         vec($FDread, fileno(TAR), 1) = 1 if ( $useTar );
704         $xfer->setSelectMask(\$FDread);
705
706         SCAN: while ( 1 ) {
707             my $ein = $FDread;
708             last if ( $FDread =~ /^\0*$/ );
709             select(my $rout = $FDread, undef, $ein, undef);
710             if ( $useTar ) {
711                 if ( vec($rout, fileno(TAR), 1) ) {
712                     if ( sysread(TAR, $mesg, 8192) <= 0 ) {
713                         vec($FDread, fileno(TAR), 1) = 0;
714                         close(TAR);
715                     } else {
716                         $tarOut .= $mesg;
717                     }
718                 }
719                 while ( $tarOut =~ /(.*?)[\n\r]+(.*)/s ) {
720                     $_ = $1;
721                     $tarOut = $2;
722                     if ( /^  / ) {
723                         $XferLOG->write(\"$_\n");
724                     } else {
725                         $XferLOG->write(\"tarExtract: $_\n");
726                     }
727                     if ( /^BackupPC_tarExtact aborting \((.*)\)/ ) {
728                         $stat{hostError} = $1;
729                     }
730                     if ( /^Done: (\d+) errors, (\d+) filesExist, (\d+) sizeExist, (\d+) sizeExistComp, (\d+) filesTotal, (\d+) sizeTotal/ ) {
731                         $tarErrs       += $1;
732                         $nFilesExist   += $2;
733                         $sizeExist     += $3;
734                         $sizeExistComp += $4;
735                         $nFilesTotal   += $5;
736                         $sizeTotal     += $6;
737                     }
738                 }
739             }
740             last if ( !$xfer->readOutput(\$FDread, $rout) );
741             while ( my $str = $xfer->logMsgGet ) {
742                 print(LOG $bpc->timeStamp, "xfer: $str\n");
743             }
744             if ( $xfer->getStats->{fileCnt} == 1 ) {
745                 #
746                 # Make sure it is still the machine we expect.  We do this while
747                 # the transfer is running to avoid a potential race condition if
748                 # the ip address was reassigned by dhcp just before we started
749                 # the transfer.
750                 #
751                 if ( my $errMsg = CorrectHostCheck($hostIP, $host) ) {
752                     $stat{hostError} = $errMsg if ( $stat{hostError} eq "" );
753                     last SCAN;
754                 }
755             }
756         }
757     } else {
758         #
759         # otherwise the xfer module does everything for us
760         #
761         my @results = $xfer->run();
762         $tarErrs       += $results[0];
763         $nFilesExist   += $results[1];
764         $sizeExist     += $results[2];
765         $sizeExistComp += $results[3];
766         $nFilesTotal   += $results[4];
767         $sizeTotal     += $results[5];
768     }
769
770     #
771     # Merge the xfer status (need to accumulate counts)
772     #
773     my $newStat = $xfer->getStats;
774     if ( $newStat->{fileCnt} == 0 ) {
775        $noFilesErr ||= "No files dumped for share $shareName";
776     }
777     foreach my $k ( (keys(%stat), keys(%$newStat)) ) {
778         next if ( !defined($newStat->{$k}) );
779         if ( $k =~ /Cnt$/ ) {
780             $stat{$k} += $newStat->{$k};
781             delete($newStat->{$k});
782             next;
783         }
784         if ( !defined($stat{$k}) ) {
785             $stat{$k} = $newStat->{$k};
786             delete($newStat->{$k});
787             next;
788         }
789     }
790
791     UserCommandRun("DumpPostShareCmd", $shareName) if ( $NeedPostCmd );
792
793     $stat{xferOK} = 0 if ( $stat{hostError} || $stat{hostAbort} );
794     if ( !$stat{xferOK} ) {
795         #
796         # kill off the tranfer program, first nicely then forcefully
797         #
798         if ( @xferPid ) {
799             kill($bpc->sigName2num("INT"), @xferPid);
800             sleep(1);
801             kill($bpc->sigName2num("KILL"), @xferPid);
802         }
803         #
804         # kill off the tar process, first nicely then forcefully
805         #
806         if ( $tarPid > 0 ) {
807             kill($bpc->sigName2num("INT"), $tarPid);
808             sleep(1);
809             kill($bpc->sigName2num("KILL"), $tarPid);
810         }
811         #
812         # don't do any more shares on this host
813         #
814         last;
815     }
816 }
817
818 #
819 # If this is a full, and any share had zero files then consider the dump bad
820 #
821 if ( $type eq "full" && $stat{hostError} eq ""
822             && length($noFilesErr) && $Conf{BackupZeroFilesIsFatal} ) {
823     $stat{hostError} = $noFilesErr;
824     $stat{xferOK} = 0;
825 }
826
827 $stat{xferOK} = 0 if ( $Abort );
828
829 #
830 # Do one last check to make sure it is still the machine we expect.
831 #
832 if ( $stat{xferOK} && (my $errMsg = CorrectHostCheck($hostIP, $host)) ) {
833     $stat{hostError} = $errMsg;
834     $stat{xferOK} = 0;
835 }
836
837 UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
838 close($newFilesFH) if ( defined($newFilesFH) );
839
840 my $endTime = time();
841
842 #
843 # If the dump failed, clean up
844 #
845 if ( !$stat{xferOK} ) {
846     $stat{hostError} = $stat{lastOutputLine} if ( $stat{hostError} eq "" );
847     if ( $stat{hostError} ) {
848         print(LOG $bpc->timeStamp,
849                   "Got fatal error during xfer ($stat{hostError})\n");
850         $XferLOG->write(\"Got fatal error during xfer ($stat{hostError})\n");
851     }
852     if ( !$Abort ) {
853         #
854         # wait a short while and see if the system is still alive
855         #
856         sleep(5);
857         if ( $bpc->CheckHostAlive($hostIP) < 0 ) {
858             $stat{hostAbort} = 1;
859         }
860         if ( $stat{hostAbort} ) {
861             $stat{hostError} = "lost network connection during backup";
862         }
863         print(LOG $bpc->timeStamp, "Backup aborted ($stat{hostError})\n");
864         $XferLOG->write(\"Backup aborted ($stat{hostError})\n");
865     } else {
866         $XferLOG->write(\"Backup aborted by user signal\n");
867     }
868
869     #
870     # Close the log file and call BackupFailCleanup, which exits.
871     #
872     BackupFailCleanup();
873 }
874
875 my $newNum = BackupSave();
876
877 my $otherCount = $stat{xferErrCnt} - $stat{xferBadFileCnt}
878                                    - $stat{xferBadShareCnt};
879 print(LOG $bpc->timeStamp,
880           "$type backup $newNum complete, $stat{fileCnt} files,"
881         . " $stat{byteCnt} bytes,"
882         . " $stat{xferErrCnt} xferErrs ($stat{xferBadFileCnt} bad files,"
883         . " $stat{xferBadShareCnt} bad shares, $otherCount other)\n");
884
885 BackupExpire($client);
886
887 print("$type backup complete\n");
888
889 ###########################################################################
890 # Subroutines
891 ###########################################################################
892
893 sub NothingToDo
894 {
895     my($needLink) = @_;
896
897     print("nothing to do\n");
898     print("link $clientURI\n") if ( $needLink );
899     exit(0);
900 }
901
902 sub catch_signal
903 {
904     my $sigName = shift;
905
906     #
907     # The first time we receive a signal we try to gracefully
908     # abort the backup.  This allows us to keep a partial dump
909     # with the in-progress file deleted and attribute caches
910     # flushed to disk etc.
911     #
912     if ( !length($SigName) ) {
913         my $reason;
914         if ( $sigName eq "INT" ) {
915             $reason = "aborted by user (signal=$sigName)";
916         } else {
917             $reason = "aborted by signal=$sigName";
918         }
919         if ( $Pid == $$ ) {
920             #
921             # Parent logs a message
922             #
923             print(LOG $bpc->timeStamp,
924                     "Aborting backup up after signal $sigName\n");
925
926             #
927             # Tell xfer to abort
928             #
929             $xfer->abort($reason);
930
931             #
932             # Send ALRMs to BackupPC_tarExtract if we are using it
933             #
934             if ( $tarPid > 0 ) {
935                 kill($bpc->sigName2num("ARLM"), $tarPid);
936             }
937
938             #
939             # Schedule a 20 second timer in case the clean
940             # abort doesn't complete
941             #
942             alarm(20);
943         } else {
944             #
945             # Children ignore anything other than ALRM and INT
946             #
947             if ( $sigName ne "ALRM" && $sigName ne "INT" ) {
948                 return;
949             }
950
951             #
952             # The child also tells xfer to abort
953             #
954             $xfer->abort($reason);
955
956             #
957             # Schedule a 15 second timer in case the clean
958             # abort doesn't complete
959             #
960             alarm(15);
961         }
962         $SigName = $sigName;
963         $Abort = 1;
964         return;
965     }
966
967     #
968     # This is a second signal: time to clean up.
969     #
970     if ( $Pid != $$ && ($sigName eq "ALRM" || $sigName eq "INT") ) {
971         #
972         # Children quit quietly on ALRM or INT
973         #
974         exit(1)
975     }
976
977     #
978     # Ignore other signals in children
979     #
980     return if ( $Pid != $$ );
981
982     $SIG{$sigName} = 'IGNORE';
983     UserCommandRun("DumpPostUserCmd") if ( $NeedPostCmd );
984     $XferLOG->write(\"exiting after signal $sigName\n");
985     if ( @xferPid ) {
986         kill($bpc->sigName2num("INT"), @xferPid);
987         sleep(1);
988         kill($bpc->sigName2num("KILL"), @xferPid);
989     }
990     if ( $tarPid > 0 ) {
991         kill($bpc->sigName2num("INT"), $tarPid);
992         sleep(1);
993         kill($bpc->sigName2num("KILL"), $tarPid);
994     }
995     if ( $sigName eq "INT" ) {
996         $stat{hostError} = "aborted by user (signal=$sigName)";
997     } else {
998         $stat{hostError} = "received signal=$sigName";
999     }
1000     BackupFailCleanup();
1001 }
1002
1003 sub CheckForNewFiles
1004 {
1005     if ( -f _ && $File::Find::name !~ /\/fattrib$/ ) {
1006         $nFilesTotal++;
1007     } elsif ( -d _ ) {
1008         #
1009         # No need to check entire tree
1010         #
1011         $File::Find::prune = 1 if ( $nFilesTotal );
1012     }
1013 }
1014
1015 sub BackupFailCleanup
1016 {
1017     my $fileExt = $Conf{CompressLevel} > 0 ? ".z" : "";
1018     my $keepPartial = 0;
1019
1020     #
1021     # We keep this backup if it is a full and we actually backed
1022     # up some files.
1023     #
1024     if ( $type eq "full" ) {
1025         if ( $nFilesTotal == 0 && $xfer->getStats->{fileCnt} == 0 ) {
1026             #
1027             # Xfer didn't report any files, but check in the new
1028             # directory just in case.
1029             #
1030             find(\&CheckForNewFiles, "$Dir/new");
1031             $keepPartial = 1 if ( $nFilesTotal );
1032         } else {
1033             #
1034             # Xfer reported some files
1035             #
1036             $keepPartial = 1;
1037         }
1038     }
1039
1040     #
1041     # Don't keep partials if they are disabled
1042     #
1043     $keepPartial = 0 if ( $Conf{PartialAgeMax} < 0 );
1044
1045     if ( !$keepPartial ) {
1046         #
1047         # No point in saving this dump; get rid of eveything.
1048         #
1049         $XferLOG->close();
1050         unlink("$Dir/timeStamp.level0")    if ( -f "$Dir/timeStamp.level0" );
1051         unlink("$Dir/SmbLOG.bad")          if ( -f "$Dir/SmbLOG.bad" );
1052         unlink("$Dir/SmbLOG.bad$fileExt")  if ( -f "$Dir/SmbLOG.bad$fileExt" );
1053         unlink("$Dir/XferLOG.bad")         if ( -f "$Dir/XferLOG.bad" );
1054         unlink("$Dir/XferLOG.bad$fileExt") if ( -f "$Dir/XferLOG.bad$fileExt" );
1055         unlink("$Dir/NewFileList")         if ( -f "$Dir/NewFileList" );
1056         rename("$Dir/XferLOG$fileExt", "$Dir/XferLOG.bad$fileExt");
1057         $bpc->RmTreeDefer("$TopDir/trash", "$Dir/new") if ( -d "$Dir/new" );
1058         print("dump failed: $stat{hostError}\n");
1059         $XferLOG->close();
1060         print("link $clientURI\n") if ( $needLink );
1061         exit(1);
1062     }
1063     #
1064     # Ok, now we should save this as a partial dump
1065     #
1066     $type = "partial";
1067     my $newNum = BackupSave();
1068     print("dump failed: $stat{hostError}\n");
1069     print("link $clientURI\n") if ( $needLink );
1070     print(LOG $bpc->timeStamp, "Saved partial dump $newNum\n");
1071     exit(2);
1072 }
1073
1074 #
1075 # Decide which old backups should be expired by moving them
1076 # to $TopDir/trash.
1077 #
1078 sub BackupExpire
1079 {
1080     my($client) = @_;
1081     my($Dir) = "$TopDir/pc/$client";
1082     my(@Backups) = $bpc->BackupInfoRead($client);
1083     my($cntFull, $cntIncr, $firstFull, $firstIncr, $oldestIncr,
1084        $oldestFull, $changes);
1085
1086     if ( $Conf{FullKeepCnt} <= 0 ) {
1087         print(LOG $bpc->timeStamp,
1088                   "Invalid value for \$Conf{FullKeepCnt}=$Conf{FullKeepCnt}\n");
1089         print(STDERR
1090             "Invalid value for \$Conf{FullKeepCnt}=$Conf{FullKeepCnt}\n")
1091                             if ( $opts{v} );
1092         return;
1093     }
1094     while ( 1 ) {
1095         $cntFull = $cntIncr = 0;
1096         $oldestIncr = $oldestFull = 0;
1097         for ( my $i = 0 ; $i < @Backups ; $i++ ) {
1098             if ( $Backups[$i]{type} eq "full" ) {
1099                 $firstFull = $i if ( $cntFull == 0 );
1100                 $cntFull++;
1101             } else {
1102                 $firstIncr = $i if ( $cntIncr == 0 );
1103                 $cntIncr++;
1104             }
1105         }
1106         $oldestIncr = (time - $Backups[$firstIncr]{startTime}) / (24 * 3600)
1107                         if ( $cntIncr > 0 );
1108         $oldestFull = (time - $Backups[$firstFull]{startTime}) / (24 * 3600)
1109                         if ( $cntFull > 0 );
1110         if ( $cntIncr > $Conf{IncrKeepCnt}
1111                 || ($cntIncr > $Conf{IncrKeepCntMin}
1112                     && $oldestIncr > $Conf{IncrAgeMax})
1113                && (@Backups <= $firstIncr + 1
1114                         || $Backups[$firstIncr]{noFill}
1115                         || !$Backups[$firstIncr + 1]{noFill}) ) {
1116             #
1117             # Only delete an incr backup if the Conf settings are satisfied.
1118             # We also must make sure that either this backup is the most
1119             # recent one, or it is not filled, or the next backup is filled.
1120             # (We can't deleted a filled incr if the next backup is not
1121             # filled.)
1122             # 
1123             print(LOG $bpc->timeStamp,
1124                       "removing incr backup $Backups[$firstIncr]{num}\n");
1125             BackupRemove($client, \@Backups, $firstIncr);
1126             $changes++;
1127             next;
1128         }
1129
1130         #
1131         # Delete any old full backups, according to $Conf{FullKeepCntMin}
1132         # and $Conf{FullAgeMax}.
1133         #
1134         # First make sure that $Conf{FullAgeMax} is at least bigger
1135         # than $Conf{FullPeriod} * $Conf{FullKeepCnt}, including
1136         # the exponential array case.
1137         #
1138         my $fullKeepCnt = $Conf{FullKeepCnt};
1139         $fullKeepCnt = [$fullKeepCnt] if ( ref($fullKeepCnt) ne "ARRAY" );
1140         my $fullAgeMax;
1141         my $fullPeriod = int(0.5 + $Conf{FullPeriod});
1142         $fullPeriod = 7 if ( $fullPeriod <= 0 );
1143         for ( my $i = 0 ; $i < @$fullKeepCnt ; $i++ ) {
1144             $fullAgeMax += $fullKeepCnt->[$i] * $fullPeriod;
1145             $fullPeriod *= 2;
1146         }
1147         $fullAgeMax += $fullPeriod;     # add some buffer
1148
1149         if ( $cntFull > $Conf{FullKeepCntMin}
1150                && $oldestFull > $Conf{FullAgeMax}
1151                && $oldestFull > $fullAgeMax
1152                && $Conf{FullKeepCntMin} > 0
1153                && $Conf{FullAgeMax} > 0
1154                && (@Backups <= $firstFull + 1
1155                         || !$Backups[$firstFull + 1]{noFill}) ) {
1156             #
1157             # Only delete a full backup if the Conf settings are satisfied.
1158             # We also must make sure that either this backup is the most
1159             # recent one, or the next backup is filled.
1160             # (We can't deleted a full backup if the next backup is not
1161             # filled.)
1162             # 
1163             print(LOG $bpc->timeStamp,
1164                    "removing old full backup $Backups[$firstFull]{num}\n");
1165             BackupRemove($client, \@Backups, $firstFull);
1166             $changes++;
1167             next;
1168         }
1169
1170         #
1171         # Do new-style full backup expiry, which includes the the case
1172         # where $Conf{FullKeepCnt} is an array.
1173         #
1174         last if ( !BackupFullExpire($client, \@Backups) );
1175     }
1176     $bpc->BackupInfoWrite($client, @Backups) if ( $changes );
1177 }
1178
1179 #
1180 # Handle full backup expiry, using exponential periods.
1181 #
1182 sub BackupFullExpire
1183 {
1184     my($client, $Backups) = @_;
1185     my $fullCnt = 0;
1186     my $fullPeriod = $Conf{FullPeriod};
1187     my $origFullPeriod = $fullPeriod;
1188     my $fullKeepCnt = $Conf{FullKeepCnt};
1189     my $fullKeepIdx = 0;
1190     my(@delete, @fullList);
1191
1192     #
1193     # Don't delete anything if $Conf{FullPeriod} or $Conf{FullKeepCnt} are
1194     # not defined - possibly a corrupted config.pl file.
1195     #
1196     return if ( !defined($Conf{FullPeriod}) || !defined($Conf{FullKeepCnt}) );
1197
1198     #
1199     # If regular backups are still disabled with $Conf{FullPeriod} < 0,
1200     # we still expire backups based on a typical FullPeriod value - weekly.
1201     #
1202     $fullPeriod = 7 if ( $fullPeriod <= 0 );
1203
1204     $fullKeepCnt = [$fullKeepCnt] if ( ref($fullKeepCnt) ne "ARRAY" );
1205
1206     for ( my $i = 0 ; $i < @$Backups ; $i++ ) {
1207         next if ( $Backups->[$i]{type} ne "full" );
1208         push(@fullList, $i);
1209     }
1210     for ( my $k = @fullList - 1 ; $k >= 0 ; $k-- ) {
1211         my $i = $fullList[$k];
1212         my $prevFull = $fullList[$k-1] if ( $k > 0 );
1213         #
1214         # Don't delete any full that is followed by an unfilled backup,
1215         # since it is needed for restore.
1216         #
1217         my $noDelete = $i + 1 < @$Backups ? $Backups->[$i+1]{noFill} : 0;
1218
1219         if ( !$noDelete && 
1220               ($fullKeepIdx >= @$fullKeepCnt
1221               || $k > 0
1222                  && $fullKeepIdx > 0
1223                  && $Backups->[$i]{startTime} - $Backups->[$prevFull]{startTime}
1224                              < ($fullPeriod - $origFullPeriod / 2) * 24 * 3600
1225                )
1226             ) {
1227             #
1228             # Delete the full backup
1229             #
1230             #print("Deleting backup $i ($prevFull)\n");
1231             unshift(@delete, $i);
1232         } else {
1233             $fullCnt++;
1234             while ( $fullKeepIdx < @$fullKeepCnt
1235                      && $fullCnt >= $fullKeepCnt->[$fullKeepIdx] ) {
1236                 $fullKeepIdx++;
1237                 $fullCnt = 0;
1238                 $fullPeriod = 2 * $fullPeriod;
1239             }
1240         }
1241     }
1242     #
1243     # Now actually delete the backups
1244     #
1245     for ( my $i = @delete - 1 ; $i >= 0 ; $i-- ) {
1246         print(LOG $bpc->timeStamp,
1247                "removing full backup $Backups->[$delete[$i]]{num}\n");
1248         BackupRemove($client, $Backups, $delete[$i]);
1249     }
1250     return @delete;
1251 }
1252
1253 #
1254 # Removes any partial backups
1255 #
1256 sub BackupPartialRemove
1257 {
1258     my($client, $Backups) = @_;
1259
1260     for ( my $i = @$Backups - 1 ; $i >= 0 ; $i-- ) {
1261         next if ( $Backups->[$i]{type} ne "partial" );
1262         BackupRemove($client, $Backups, $i);
1263     }
1264 }
1265
1266 sub BackupSave
1267 {
1268     my @Backups = $bpc->BackupInfoRead($client);
1269     my $num  = -1;
1270     my $newFilesFH;
1271
1272     #
1273     # Since we got a good backup we should remove any partial dumps
1274     # (the new backup might also be a partial, but that's ok).
1275     #
1276     BackupPartialRemove($client, \@Backups);
1277
1278     #
1279     # Number the new backup
1280     #
1281     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
1282         $num = $Backups[$i]{num} if ( $num < $Backups[$i]{num} );
1283     }
1284     $num++;
1285     $bpc->RmTreeDefer("$TopDir/trash", "$Dir/$num") if ( -d "$Dir/$num" );
1286     if ( !rename("$Dir/new", "$Dir/$num") ) {
1287         print(LOG $bpc->timeStamp, "Rename $Dir/new -> $Dir/$num failed\n");
1288         $stat{xferOK} = 0;
1289     }
1290     $needLink = 1 if ( -f "$Dir/NewFileList" );
1291
1292     #
1293     # Add the new backup information to the backup file
1294     #
1295     my $i = @Backups;
1296     $Backups[$i]{num}           = $num;
1297     $Backups[$i]{type}          = $type;
1298     $Backups[$i]{startTime}     = $startTime;
1299     $Backups[$i]{endTime}       = $endTime;
1300     $Backups[$i]{size}          = $sizeTotal;
1301     $Backups[$i]{nFiles}        = $nFilesTotal;
1302     $Backups[$i]{xferErrs}      = $stat{xferErrCnt} || 0;
1303     $Backups[$i]{xferBadFile}   = $stat{xferBadFileCnt} || 0;
1304     $Backups[$i]{xferBadShare}  = $stat{xferBadShareCnt} || 0;
1305     $Backups[$i]{nFilesExist}   = $nFilesExist;
1306     $Backups[$i]{sizeExist}     = $sizeExist;
1307     $Backups[$i]{sizeExistComp} = $sizeExistComp;
1308     $Backups[$i]{tarErrs}       = $tarErrs;
1309     $Backups[$i]{compress}      = $Conf{CompressLevel};
1310     $Backups[$i]{noFill}        = $type eq "incr" ? 1 : 0;
1311     $Backups[$i]{level}         = $type eq "incr" ? 1 : 0;
1312     $Backups[$i]{mangle}        = 1;        # name mangling always on for v1.04+
1313     $Backups[$i]{xferMethod}    = $Conf{XferMethod};
1314     $Backups[$i]{charset}       = $Conf{ClientCharset};
1315     #
1316     # Save the main backups file
1317     #
1318     $bpc->BackupInfoWrite($client, @Backups);
1319     #
1320     # Save just this backup's info in case the main backups file
1321     # gets corrupted
1322     #
1323     BackupPC::Storage->backupInfoWrite($Dir, $Backups[$i]{num},
1324                                              $Backups[$i]);
1325
1326     unlink("$Dir/timeStamp.level0") if ( -f "$Dir/timeStamp.level0" );
1327     foreach my $ext ( qw(bad bad.z) ) {
1328         next if ( !-f "$Dir/XferLOG.$ext" );
1329         unlink("$Dir/XferLOG.$ext.old") if ( -f "$Dir/XferLOG.$ext" );
1330         rename("$Dir/XferLOG.$ext", "$Dir/XferLOG.$ext.old");
1331     }
1332
1333     #
1334     # Now remove the bad files, replacing them if possible with links to
1335     # earlier backups.
1336     #
1337     foreach my $f ( $xfer->getBadFiles ) {
1338         my $j;
1339         my $shareM = $bpc->fileNameEltMangle($f->{share});
1340         my $fileM  = $bpc->fileNameMangle($f->{file});
1341         unlink("$Dir/$num/$shareM/$fileM");
1342         for ( $j = $i - 1 ; $j >= 0 ; $j-- ) {
1343             my $file;
1344             if ( $Backups[$j]{mangle} ) {
1345                 $file = "$shareM/$fileM";
1346             } else {
1347                 $file = "$f->{share}/$f->{file}";
1348             }
1349             next if ( !-f "$Dir/$Backups[$j]{num}/$file" );
1350
1351             my($exists, $digest, $origSize, $outSize, $errs)
1352                                 = BackupPC::PoolWrite::LinkOrCopy(
1353                                       $bpc,
1354                                       "$Dir/$Backups[$j]{num}/$file",
1355                                       $Backups[$j]{compress},
1356                                       "$Dir/$num/$shareM/$fileM",
1357                                       $Conf{CompressLevel});
1358             if ( !$exists ) {
1359                 #
1360                 # the hard link failed, most likely because the target
1361                 # file has too many links.  We have copied the file
1362                 # instead, so add this to the new file list.
1363                 #
1364                 if ( !defined($newFilesFH) ) {
1365                     my $str = "Appending to NewFileList for $shareM/$fileM\n";
1366                     $XferLOG->write(\$str);
1367                     open($newFilesFH, ">>", "$TopDir/pc/$client/NewFileList")
1368                          || die("can't open $TopDir/pc/$client/NewFileList");
1369                     binmode($newFilesFH);
1370                 }
1371                 if ( -f "$Dir/$num/$shareM/$fileM" ) {
1372                     print($newFilesFH "$digest $origSize $shareM/$fileM\n");
1373                 } else {
1374                     my $str = "Unable to link/copy $num/$f->{share}/$f->{file}"
1375                             . " to $Backups[$j]{num}/$f->{share}/$f->{file}\n";
1376                     $XferLOG->write(\$str);
1377                 }
1378             } else {
1379                 my $str = "Bad file $num/$f->{share}/$f->{file} replaced"
1380                         . " by link to"
1381                         . " $Backups[$j]{num}/$f->{share}/$f->{file}\n";
1382                 $XferLOG->write(\$str);
1383             }
1384             last;
1385         }
1386         if ( $j < 0 ) {
1387             my $str = "Removed bad file $num/$f->{share}/$f->{file}"
1388                     . " (no older copy to link to)\n";
1389             $XferLOG->write(\$str);
1390         }
1391     }
1392     close($newFilesFH) if ( defined($newFilesFH) );
1393     $XferLOG->close();
1394     rename("$Dir/XferLOG$fileExt", "$Dir/XferLOG.$num$fileExt");
1395     rename("$Dir/NewFileList", "$Dir/NewFileList.$num");
1396
1397     return $num;
1398 }
1399
1400 #
1401 # Removes a specific backup
1402 #
1403 sub BackupRemove
1404 {
1405     my($client, $Backups, $idx) = @_;
1406     my($Dir) = "$TopDir/pc/$client";
1407
1408     if ( $Backups->[$idx]{num} eq "" ) {
1409         print("BackupRemove: ignoring empty backup number for idx $idx\n");
1410         return;
1411     }
1412
1413     $bpc->RmTreeDefer("$TopDir/trash",
1414                       "$Dir/$Backups->[$idx]{num}");
1415     unlink("$Dir/SmbLOG.$Backups->[$idx]{num}")
1416                 if ( -f "$Dir/SmbLOG.$Backups->[$idx]{num}" );
1417     unlink("$Dir/SmbLOG.$Backups->[$idx]{num}.z")
1418                 if ( -f "$Dir/SmbLOG.$Backups->[$idx]{num}.z" );
1419     unlink("$Dir/XferLOG.$Backups->[$idx]{num}")
1420                 if ( -f "$Dir/XferLOG.$Backups->[$idx]{num}" );
1421     unlink("$Dir/XferLOG.$Backups->[$idx]{num}.z")
1422                 if ( -f "$Dir/XferLOG.$Backups->[$idx]{num}.z" );
1423     splice(@{$Backups}, $idx, 1);
1424 }
1425
1426 sub CorrectHostCheck
1427 {
1428     my($hostIP, $host) = @_;
1429     return if ( $hostIP eq $host && !$Conf{FixedIPNetBiosNameCheck}
1430                 || $Conf{NmbLookupCmd} eq "" );
1431     my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($hostIP);
1432     return "host $host has mismatching netbios name $netBiosHost"
1433                 if ( $netBiosHost ne $host );
1434     return;
1435 }
1436
1437 #
1438 # The Xfer method might tell us from time to time about processes
1439 # it forks.  We tell BackupPC about this (for status displays) and
1440 # keep track of the pids in case we cancel the backup
1441 #
1442 sub pidHandler
1443 {
1444     @xferPid = @_;
1445     @xferPid = grep(/./, @xferPid);
1446     return if ( !@xferPid && $tarPid < 0 );
1447     my @pids = @xferPid;
1448     push(@pids, $tarPid) if ( $tarPid > 0 );
1449     my $str = join(",", @pids);
1450     $XferLOG->write(\"Xfer PIDs are now $str\n") if ( defined($XferLOG) );
1451     print("xferPids $str\n");
1452 }
1453
1454 #
1455 # Run an optional pre- or post-dump command
1456 #
1457 sub UserCommandRun
1458 {
1459     my($cmdType, $sharename) = @_;
1460
1461     return if ( !defined($Conf{$cmdType}) );
1462     my $vars = {
1463         xfer       => $xfer,
1464         client     => $client,
1465         host       => $host,
1466         hostIP     => $hostIP,
1467         user       => $Hosts->{$client}{user},
1468         moreUsers  => $Hosts->{$client}{moreUsers},
1469         share      => $ShareNames->[0],
1470         shares     => $ShareNames,
1471         XferMethod => $Conf{XferMethod},
1472         sshPath    => $Conf{SshPath},
1473         LOG        => *LOG,
1474         XferLOG    => $XferLOG,
1475         stat       => \%stat,
1476         xferOK     => $stat{xferOK} || 0,
1477         hostError  => $stat{hostError},
1478         type       => $type,
1479         cmdType    => $cmdType,
1480     };
1481
1482     if ($cmdType eq 'DumpPreShareCmd' || $cmdType eq 'DumpPostShareCmd') {
1483         $vars->{share} = $sharename;
1484     }
1485
1486     my $cmd = $bpc->cmdVarSubstitute($Conf{$cmdType}, $vars);
1487     $XferLOG->write(\"Executing $cmdType: @$cmd\n");
1488     #
1489     # Run the user's command, dumping the stdout/stderr into the
1490     # Xfer log file.  Also supply the optional $vars and %Conf in
1491     # case the command is really perl code instead of a shell
1492     # command.
1493     #
1494     $bpc->cmdSystemOrEval($cmd,
1495             sub {
1496                 $XferLOG->write(\$_[0]);
1497             },
1498             $vars, \%Conf);
1499 }