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