88db7f87a8c156bbb01302e4c6515b1c9e623d50
[BackupPC.git] / lib / BackupPC / Lib.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::Lib package
4 #
5 # DESCRIPTION
6 #
7 #   This library defines a BackupPC::Lib class and a variety of utility
8 #   functions used by BackupPC.
9 #
10 # AUTHOR
11 #   Craig Barratt  <cbarratt@users.sourceforge.net>
12 #
13 # COPYRIGHT
14 #   Copyright (C) 2001-2003  Craig Barratt
15 #
16 #   This program is free software; you can redistribute it and/or modify
17 #   it under the terms of the GNU General Public License as published by
18 #   the Free Software Foundation; either version 2 of the License, or
19 #   (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 #   GNU General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29 #
30 #========================================================================
31 #
32 # Version 3.1.0beta0, released 3 Sep 2007.
33 #
34 # See http://backuppc.sourceforge.net.
35 #
36 #========================================================================
37
38 package BackupPC::Lib;
39
40 use strict;
41
42 use vars qw(%Conf %Lang);
43 use BackupPC::Storage;
44 use Fcntl ':mode';
45 use Carp;
46 use File::Path;
47 use File::Compare;
48 use Socket;
49 use Cwd;
50 use Digest::MD5;
51 use Config;
52 use Encode qw/from_to encode_utf8/;
53
54 use vars qw( $IODirentOk );
55 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
56
57 require Exporter;
58 require DynaLoader;
59
60 @ISA = qw(Exporter DynaLoader);
61 @EXPORT_OK = qw( BPC_DT_UNKNOWN
62                  BPC_DT_FIFO
63                  BPC_DT_CHR
64                  BPC_DT_DIR
65                  BPC_DT_BLK
66                  BPC_DT_REG
67                  BPC_DT_LNK
68                  BPC_DT_SOCK
69                );
70 @EXPORT = qw( );
71 %EXPORT_TAGS = ('BPC_DT_ALL' => [@EXPORT, @EXPORT_OK]);
72
73 BEGIN {
74     eval "use IO::Dirent qw( readdirent DT_DIR );";
75     $IODirentOk = 1 if ( !$@ );
76 };
77
78 #
79 # The need to match the constants in IO::Dirent
80 #
81 use constant BPC_DT_UNKNOWN =>   0;
82 use constant BPC_DT_FIFO    =>   1;    ## named pipe (fifo)
83 use constant BPC_DT_CHR     =>   2;    ## character special
84 use constant BPC_DT_DIR     =>   4;    ## directory
85 use constant BPC_DT_BLK     =>   6;    ## block special
86 use constant BPC_DT_REG     =>   8;    ## regular
87 use constant BPC_DT_LNK     =>  10;    ## symbolic link
88 use constant BPC_DT_SOCK    =>  12;    ## socket
89
90 sub new
91 {
92     my $class = shift;
93     my($topDir, $installDir, $confDir, $noUserCheck) = @_;
94
95     #
96     # Whether to use filesystem hierarchy standard for file layout.
97     # If set, text config files are below /etc/BackupPC.
98     #
99     my $useFHS = 0;
100     my $paths;
101
102     #
103     # Set defaults for $topDir and $installDir.
104     #
105     $topDir     = '/tera0/backup/BackupPC' if ( $topDir eq "" );
106     $installDir = '/usr/local/BackupPC'    if ( $installDir eq "" );
107
108     #
109     # Pick some initial defaults.  For FHS the only critical
110     # path is the ConfDir, since we get everything else out
111     # of the main config file.
112     #
113     if ( $useFHS ) {
114         $paths = {
115             useFHS     => $useFHS,
116             TopDir     => $topDir,
117             InstallDir => $installDir,
118             ConfDir    => $confDir eq "" ? '/tera0/backup/BackupPC/conf' : $confDir,
119             LogDir     => '/var/log/BackupPC',
120         };
121     } else {
122         $paths = {
123             useFHS     => $useFHS,
124             TopDir     => $topDir,
125             InstallDir => $installDir,
126             ConfDir    => $confDir eq "" ? "$topDir/conf" : $confDir,
127             LogDir     => "$topDir/log",
128         };
129     }
130
131     my $bpc = bless {
132         %$paths,
133         Version => '3.1.0beta0',
134     }, $class;
135
136     $bpc->{storage} = BackupPC::Storage->new($paths);
137
138     #
139     # Clean up %ENV and setup other variables.
140     #
141     delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
142     $bpc->{PoolDir}  = "$bpc->{TopDir}/pool";
143     $bpc->{CPoolDir} = "$bpc->{TopDir}/cpool";
144     if ( defined(my $error = $bpc->ConfigRead()) ) {
145         print(STDERR $error, "\n");
146         return;
147     }
148
149     #
150     # Update the paths based on the config file
151     #
152     foreach my $dir ( qw(TopDir ConfDir InstallDir LogDir) ) {
153         next if ( $bpc->{Conf}{$dir} eq "" );
154         $paths->{$dir} = $bpc->{$dir} = $bpc->{Conf}{$dir};
155     }
156     $bpc->{storage}->setPaths($paths);
157
158     #
159     # Verify we are running as the correct user
160     #
161     if ( !$noUserCheck
162             && $bpc->{Conf}{BackupPCUserVerify}
163             && $> != (my $uid = (getpwnam($bpc->{Conf}{BackupPCUser}))[2]) ) {
164         print(STDERR "$0: Wrong user: my userid is $>, instead of $uid"
165             . " ($bpc->{Conf}{BackupPCUser})\n");
166         print(STDERR "Please su $bpc->{Conf}{BackupPCUser} first\n");
167         return;
168     }
169     return $bpc;
170 }
171
172 sub TopDir
173 {
174     my($bpc) = @_;
175     return $bpc->{TopDir};
176 }
177
178 sub BinDir
179 {
180     my($bpc) = @_;
181     return "$bpc->{InstallDir}/bin";
182 }
183
184 sub LogDir
185 {
186     my($bpc) = @_;
187     return $bpc->{LogDir};
188 }
189
190 sub ConfDir
191 {
192     my($bpc) = @_;
193     return $bpc->{ConfDir};
194 }
195
196 sub LibDir
197 {
198     my($bpc) = @_;
199     return "$bpc->{InstallDir}/lib";
200 }
201
202 sub InstallDir
203 {
204     my($bpc) = @_;
205     return $bpc->{InstallDir};
206 }
207
208 sub useFHS
209 {
210     my($bpc) = @_;
211     return $bpc->{useFHS};
212 }
213
214 sub Version
215 {
216     my($bpc) = @_;
217     return $bpc->{Version};
218 }
219
220 sub Conf
221 {
222     my($bpc) = @_;
223     return %{$bpc->{Conf}};
224 }
225
226 sub Lang
227 {
228     my($bpc) = @_;
229     return $bpc->{Lang};
230 }
231
232 sub adminJob
233 {
234     my($bpc, $num) = @_;
235     return " admin " if ( !$num );
236     return " admin$num ";
237 }
238
239 sub isAdminJob
240 {
241     my($bpc, $str) = @_;
242     return $str =~ /^ admin/;
243 }
244
245 sub trashJob
246 {
247     return " trashClean ";
248 }
249
250 sub ConfValue
251 {
252     my($bpc, $param) = @_;
253
254     return $bpc->{Conf}{$param};
255 }
256
257 sub verbose
258 {
259     my($bpc, $param) = @_;
260
261     $bpc->{verbose} = $param if ( defined($param) );
262     return $bpc->{verbose};
263 }
264
265 sub sigName2num
266 {
267     my($bpc, $sig) = @_;
268
269     if ( !defined($bpc->{SigName2Num}) ) {
270         my $i = 0;
271         foreach my $name ( split(' ', $Config{sig_name}) ) {
272             $bpc->{SigName2Num}{$name} = $i;
273             $i++;
274         }
275     }
276     return $bpc->{SigName2Num}{$sig};
277 }
278
279 #
280 # Generate an ISO 8601 format timeStamp (but without the "T").
281 # See http://www.w3.org/TR/NOTE-datetime and
282 # http://www.cl.cam.ac.uk/~mgk25/iso-time.html
283 #
284 sub timeStamp
285 {
286     my($bpc, $t, $noPad) = @_;
287     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
288               = localtime($t || time);
289     return sprintf("%04d-%02d-%02d %02d:%02d:%02d",
290                     $year + 1900, $mon + 1, $mday, $hour, $min, $sec)
291              . ($noPad ? "" : " ");
292 }
293
294 sub BackupInfoRead
295 {
296     my($bpc, $host) = @_;
297
298     return $bpc->{storage}->BackupInfoRead($host);
299 }
300
301 sub BackupInfoWrite
302 {
303     my($bpc, $host, @Backups) = @_;
304
305     return $bpc->{storage}->BackupInfoWrite($host, @Backups);
306 }
307
308 sub RestoreInfoRead
309 {
310     my($bpc, $host) = @_;
311
312     return $bpc->{storage}->RestoreInfoRead($host);
313 }
314
315 sub RestoreInfoWrite
316 {
317     my($bpc, $host, @Restores) = @_;
318
319     return $bpc->{storage}->RestoreInfoWrite($host, @Restores);
320 }
321
322 sub ArchiveInfoRead
323 {
324     my($bpc, $host) = @_;
325
326     return $bpc->{storage}->ArchiveInfoRead($host);
327 }
328
329 sub ArchiveInfoWrite
330 {
331     my($bpc, $host, @Archives) = @_;
332
333     return $bpc->{storage}->ArchiveInfoWrite($host, @Archives);
334 }
335
336 sub ConfigDataRead
337 {
338     my($bpc, $host) = @_;
339
340     return $bpc->{storage}->ConfigDataRead($host);
341 }
342
343 sub ConfigDataWrite
344 {
345     my($bpc, $host, $conf) = @_;
346
347     return $bpc->{storage}->ConfigDataWrite($host, $conf);
348 }
349
350 sub ConfigRead
351 {
352     my($bpc, $host) = @_;
353     my($ret);
354
355     #
356     # Read main config file
357     #
358     my($mesg, $config) = $bpc->{storage}->ConfigDataRead();
359     return $mesg if ( defined($mesg) );
360
361     $bpc->{Conf} = $config;
362
363     #
364     # Read host config file
365     #
366     if ( $host ne "" ) {
367         ($mesg, $config) = $bpc->{storage}->ConfigDataRead($host);
368         return $mesg if ( defined($mesg) );
369         $bpc->{Conf} = { %{$bpc->{Conf}}, %$config };
370     }
371
372     #
373     # Load optional perl modules
374     #
375     if ( defined($bpc->{Conf}{PerlModuleLoad}) ) {
376         #
377         # Load any user-specified perl modules.  This is for
378         # optional user-defined extensions.
379         #
380         $bpc->{Conf}{PerlModuleLoad} = [$bpc->{Conf}{PerlModuleLoad}]
381                     if ( ref($bpc->{Conf}{PerlModuleLoad}) ne "ARRAY" );
382         foreach my $module ( @{$bpc->{Conf}{PerlModuleLoad}} ) {
383             eval("use $module;");
384         }
385     }
386
387     #
388     # Load language file
389     #
390     return "No language setting" if ( !defined($bpc->{Conf}{Language}) );
391     my $langFile = "$bpc->{InstallDir}/lib/BackupPC/Lang/$bpc->{Conf}{Language}.pm";
392     if ( !defined($ret = do $langFile) && ($! || $@) ) {
393         $mesg = "Couldn't open language file $langFile: $!" if ( $! );
394         $mesg = "Couldn't execute language file $langFile: $@" if ( $@ );
395         $mesg =~ s/[\n\r]+//;
396         return $mesg;
397     }
398     $bpc->{Lang} = \%Lang;
399     return;
400 }
401
402 #
403 # Return the mtime of the config file
404 #
405 sub ConfigMTime
406 {
407     my($bpc) = @_;
408
409     return $bpc->{storage}->ConfigMTime();
410 }
411
412 #
413 # Returns information from the host file in $bpc->{TopDir}/conf/hosts.
414 # With no argument a ref to a hash of hosts is returned.  Each
415 # hash contains fields as specified in the hosts file.  With an
416 # argument a ref to a single hash is returned with information
417 # for just that host.
418 #
419 sub HostInfoRead
420 {
421     my($bpc, $host) = @_;
422
423     return $bpc->{storage}->HostInfoRead($host);
424 }
425
426 sub HostInfoWrite
427 {
428     my($bpc, $host) = @_;
429
430     return $bpc->{storage}->HostInfoWrite($host);
431 }
432
433 #
434 # Return the mtime of the hosts file
435 #
436 sub HostsMTime
437 {
438     my($bpc) = @_;
439
440     return $bpc->{storage}->HostsMTime();
441 }
442
443 #
444 # Read a directory and return the entries in sorted inode order.
445 # This relies on the IO::Dirent module being installed.  If not,
446 # the inode data is empty and the default directory order is
447 # returned.
448 #
449 # The returned data is a list of hashes with entries {name, type, inode, nlink}.
450 # The returned data includes "." and "..".
451 #
452 # $need is a hash of file attributes we need: type, inode, or nlink.
453 # If set, these parameters are added to the returned hash.
454 #
455 # To support browsing pre-3.0.0 backups where the charset encoding
456 # is typically iso-8859-1, the charsetLegacy option can be set in
457 # $need to convert the path from utf8 and convert the names to utf8.
458 #
459 # If IO::Dirent is successful if will get type and inode for free.
460 # Otherwise, a stat is done on each file, which is more expensive.
461 #
462 sub dirRead
463 {
464     my($bpc, $path, $need) = @_;
465     my(@entries, $addInode);
466
467     from_to($path, "utf8", $need->{charsetLegacy})
468                         if ( $need->{charsetLegacy} ne "" );
469     return if ( !opendir(my $fh, $path) );
470     if ( $IODirentOk ) {
471         @entries = sort({ $a->{inode} <=> $b->{inode} } readdirent($fh));
472         map { $_->{type} = 0 + $_->{type} } @entries;   # make type numeric
473     } else {
474         @entries = map { { name => $_} } readdir($fh);
475     }
476     closedir($fh);
477     if ( defined($need) ) {
478         for ( my $i = 0 ; $i < @entries ; $i++ ) {
479             next if ( (!$need->{inode} || defined($entries[$i]{inode}))
480                    && (!$need->{type}  || defined($entries[$i]{type}))
481                    && (!$need->{nlink} || defined($entries[$i]{nlink})) );
482             my @s = stat("$path/$entries[$i]{name}");
483             $entries[$i]{nlink} = $s[3] if ( $need->{nlink} );
484             if ( $need->{inode} && !defined($entries[$i]{inode}) ) {
485                 $addInode = 1;
486                 $entries[$i]{inode} = $s[1];
487             }
488             if ( $need->{type} && !defined($entries[$i]{type}) ) {
489                 my $mode = S_IFMT($s[2]);
490                 $entries[$i]{type} = BPC_DT_FIFO if ( S_ISFIFO($mode) );
491                 $entries[$i]{type} = BPC_DT_CHR  if ( S_ISCHR($mode) );
492                 $entries[$i]{type} = BPC_DT_DIR  if ( S_ISDIR($mode) );
493                 $entries[$i]{type} = BPC_DT_BLK  if ( S_ISBLK($mode) );
494                 $entries[$i]{type} = BPC_DT_REG  if ( S_ISREG($mode) );
495                 $entries[$i]{type} = BPC_DT_LNK  if ( S_ISLNK($mode) );
496                 $entries[$i]{type} = BPC_DT_SOCK if ( S_ISSOCK($mode) );
497             }
498         }
499     }
500     #
501     # Sort the entries if inodes were added (the IO::Dirent case already
502     # sorted above)
503     #
504     @entries = sort({ $a->{inode} <=> $b->{inode} } @entries) if ( $addInode );
505     #
506     # for browing pre-3.0.0 backups, map iso-8859-1 to utf8 if requested
507     #
508     if ( $need->{charsetLegacy} ne "" ) {
509         for ( my $i = 0 ; $i < @entries ; $i++ ) {
510             from_to($entries[$i]{name}, $need->{charsetLegacy}, "utf8");
511         }
512     }
513     return \@entries;
514 }
515
516 #
517 # Same as dirRead, but only returns the names (which will be sorted in
518 # inode order if IO::Dirent is installed)
519 #
520 sub dirReadNames
521 {
522     my($bpc, $path, $need) = @_;
523
524     my $entries = $bpc->dirRead($path, $need);
525     return if ( !defined($entries) );
526     my @names = map { $_->{name} } @$entries;
527     return \@names;
528 }
529
530 sub find
531 {
532     my($bpc, $param, $dir, $dontDoCwd) = @_;
533
534     return if ( !chdir($dir) );
535     my $entries = $bpc->dirRead(".", {inode => 1, type => 1});
536     #print Dumper($entries);
537     foreach my $f ( @$entries ) {
538         next if ( $f->{name} eq ".." || $f->{name} eq "." && $dontDoCwd );
539         $param->{wanted}($f->{name}, "$dir/$f->{name}");
540         next if ( $f->{type} != BPC_DT_DIR || $f->{name} eq "." );
541         chdir($f->{name});
542         $bpc->find($param, "$dir/$f->{name}", 1);
543         return if ( !chdir("..") );
544     }
545 }
546
547 #
548 # Stripped down from File::Path.  In particular we don't print
549 # many warnings and we try three times to delete each directory
550 # and file -- for some reason the original File::Path rmtree
551 # didn't always completely remove a directory tree on a NetApp.
552 #
553 # Warning: this routine changes the cwd.
554 #
555 sub RmTreeQuiet
556 {
557     my($bpc, $pwd, $roots) = @_;
558     my(@files, $root);
559
560     if ( defined($roots) && length($roots) ) {
561       $roots = [$roots] unless ref $roots;
562     } else {
563       print(STDERR "RmTreeQuiet: No root path(s) specified\n");
564     }
565     chdir($pwd);
566     foreach $root (@{$roots}) {
567         $root = $1 if ( $root =~ m{(.*?)/*$} );
568         #
569         # Try first to simply unlink the file: this avoids an
570         # extra stat for every file.  If it fails (which it
571         # will for directories), check if it is a directory and
572         # then recurse.
573         #
574         if ( !unlink($root) ) {
575             if ( -d $root ) {
576                 my $d = $bpc->dirReadNames($root);
577                 if ( !defined($d) ) {
578                     print(STDERR "Can't read $pwd/$root: $!\n");
579                 } else {
580                     @files = grep $_ !~ /^\.{1,2}$/, @$d;
581                     $bpc->RmTreeQuiet("$pwd/$root", \@files);
582                     chdir($pwd);
583                     rmdir($root) || rmdir($root);
584                 }
585             } else {
586                 unlink($root) || unlink($root);
587             }
588         }
589     }
590 }
591
592 #
593 # Move a directory or file away for later deletion
594 #
595 sub RmTreeDefer
596 {
597     my($bpc, $trashDir, $file) = @_;
598     my($i, $f);
599
600     return if ( !-e $file );
601     mkpath($trashDir, 0, 0777) if ( !-d $trashDir );
602     for ( $i = 0 ; $i < 1000 ; $i++ ) {
603         $f = sprintf("%s/%d_%d_%d", $trashDir, time, $$, $i);
604         next if ( -e $f );
605         return if ( rename($file, $f) );
606     }
607     # shouldn't get here, but might if you tried to call this
608     # across file systems.... just remove the tree right now.
609     if ( $file =~ /(.*)\/([^\/]*)/ ) {
610         my($d) = $1;
611         my($f) = $2;
612         my($cwd) = Cwd::fastcwd();
613         $cwd = $1 if ( $cwd =~ /(.*)/ );
614         $bpc->RmTreeQuiet($d, $f);
615         chdir($cwd) if ( $cwd );
616     }
617 }
618
619 #
620 # Empty the trash directory.  Returns 0 if it did nothing, 1 if it
621 # did something, -1 if it failed to remove all the files.
622 #
623 sub RmTreeTrashEmpty
624 {
625     my($bpc, $trashDir) = @_;
626     my(@files);
627     my($cwd) = Cwd::fastcwd();
628
629     $cwd = $1 if ( $cwd =~ /(.*)/ );
630     return if ( !-d $trashDir );
631     my $d = $bpc->dirReadNames($trashDir) or carp "Can't read $trashDir: $!";
632     @files = grep $_ !~ /^\.{1,2}$/, @$d;
633     return 0 if ( !@files );
634     $bpc->RmTreeQuiet($trashDir, \@files);
635     foreach my $f ( @files ) {
636         return -1 if ( -e $f );
637     }
638     chdir($cwd) if ( $cwd );
639     return 1;
640 }
641
642 #
643 # Open a connection to the server.  Returns an error string on failure.
644 # Returns undef on success.
645 #
646 sub ServerConnect
647 {
648     my($bpc, $host, $port, $justConnect) = @_;
649     local(*FH);
650
651     return if ( defined($bpc->{ServerFD}) );
652     #
653     # First try the unix-domain socket
654     #
655     my $sockFile = "$bpc->{LogDir}/BackupPC.sock";
656     socket(*FH, PF_UNIX, SOCK_STREAM, 0)     || return "unix socket: $!";
657     if ( !connect(*FH, sockaddr_un($sockFile)) ) {
658         my $err = "unix connect: $!";
659         close(*FH);
660         if ( $port > 0 ) {
661             my $proto = getprotobyname('tcp');
662             my $iaddr = inet_aton($host)     || return "unknown host $host";
663             my $paddr = sockaddr_in($port, $iaddr);
664
665             socket(*FH, PF_INET, SOCK_STREAM, $proto)
666                                              || return "inet socket: $!";
667             connect(*FH, $paddr)             || return "inet connect: $!";
668         } else {
669             return $err;
670         }
671     }
672     my($oldFH) = select(*FH); $| = 1; select($oldFH);
673     $bpc->{ServerFD} = *FH;
674     return if ( $justConnect );
675     #
676     # Read the seed that we need for our MD5 message digest.  See
677     # ServerMesg below.
678     #
679     sysread($bpc->{ServerFD}, $bpc->{ServerSeed}, 1024);
680     $bpc->{ServerMesgCnt} = 0;
681     return;
682 }
683
684 #
685 # Check that the server connection is still ok
686 #
687 sub ServerOK
688 {
689     my($bpc) = @_;
690
691     return 0 if ( !defined($bpc->{ServerFD}) );
692     vec(my $FDread, fileno($bpc->{ServerFD}), 1) = 1;
693     my $ein = $FDread;
694     return 0 if ( select(my $rout = $FDread, undef, $ein, 0.0) < 0 );
695     return 1 if ( !vec($rout, fileno($bpc->{ServerFD}), 1) );
696 }
697
698 #
699 # Disconnect from the server
700 #
701 sub ServerDisconnect
702 {
703     my($bpc) = @_;
704     return if ( !defined($bpc->{ServerFD}) );
705     close($bpc->{ServerFD});
706     delete($bpc->{ServerFD});
707 }
708
709 #
710 # Sends a message to the server and returns with the reply.
711 #
712 # To avoid possible attacks via the TCP socket interface, every client
713 # message is protected by an MD5 digest. The MD5 digest includes four
714 # items:
715 #   - a seed that is sent to us when we first connect
716 #   - a sequence number that increments for each message
717 #   - a shared secret that is stored in $Conf{ServerMesgSecret}
718 #   - the message itself.
719 # The message is sent in plain text preceded by the MD5 digest. A
720 # snooper can see the plain-text seed sent by BackupPC and plain-text
721 # message, but cannot construct a valid MD5 digest since the secret in
722 # $Conf{ServerMesgSecret} is unknown. A replay attack is not possible
723 # since the seed changes on a per-connection and per-message basis.
724 #
725 sub ServerMesg
726 {
727     my($bpc, $mesg) = @_;
728     return if ( !defined(my $fh = $bpc->{ServerFD}) );
729     $mesg =~ s/\n/\\n/g;
730     $mesg =~ s/\r/\\r/g;
731     my $md5 = Digest::MD5->new;
732     $mesg = encode_utf8($mesg);
733     $md5->add($bpc->{ServerSeed} . $bpc->{ServerMesgCnt}
734             . $bpc->{Conf}{ServerMesgSecret} . $mesg);
735     print($fh $md5->b64digest . " $mesg\n");
736     $bpc->{ServerMesgCnt}++;
737     return <$fh>;
738 }
739
740 #
741 # Do initialization for child processes
742 #
743 sub ChildInit
744 {
745     my($bpc) = @_;
746     close(STDERR);
747     open(STDERR, ">&STDOUT");
748     select(STDERR); $| = 1;
749     select(STDOUT); $| = 1;
750     $ENV{PATH} = $bpc->{Conf}{MyPath};
751 }
752
753 #
754 # Compute the MD5 digest of a file.  For efficiency we don't
755 # use the whole file for big files:
756 #   - for files <= 256K we use the file size and the whole file.
757 #   - for files <= 1M we use the file size, the first 128K and
758 #     the last 128K.
759 #   - for files > 1M, we use the file size, the first 128K and
760 #     the 8th 128K (ie: the 128K up to 1MB).
761 # See the documentation for a discussion of the tradeoffs in
762 # how much data we use and how many collisions we get.
763 #
764 # Returns the MD5 digest (a hex string) and the file size.
765 #
766 sub File2MD5
767 {
768     my($bpc, $md5, $name) = @_;
769     my($data, $fileSize);
770     local(*N);
771
772     $fileSize = (stat($name))[7];
773     return ("", -1) if ( !-f _ );
774     $name = $1 if ( $name =~ /(.*)/ );
775     return ("", 0) if ( $fileSize == 0 );
776     return ("", -1) if ( !open(N, $name) );
777     binmode(N);
778     $md5->reset();
779     $md5->add($fileSize);
780     if ( $fileSize > 262144 ) {
781         #
782         # read the first and last 131072 bytes of the file,
783         # up to 1MB.
784         #
785         my $seekPosn = ($fileSize > 1048576 ? 1048576 : $fileSize) - 131072;
786         $md5->add($data) if ( sysread(N, $data, 131072) );
787         $md5->add($data) if ( sysseek(N, $seekPosn, 0)
788                                 && sysread(N, $data, 131072) );
789     } else {
790         #
791         # read the whole file
792         #
793         $md5->add($data) if ( sysread(N, $data, $fileSize) );
794     }
795     close(N);
796     return ($md5->hexdigest, $fileSize);
797 }
798
799 #
800 # Compute the MD5 digest of a buffer (string).  For efficiency we don't
801 # use the whole string for big strings:
802 #   - for files <= 256K we use the file size and the whole file.
803 #   - for files <= 1M we use the file size, the first 128K and
804 #     the last 128K.
805 #   - for files > 1M, we use the file size, the first 128K and
806 #     the 8th 128K (ie: the 128K up to 1MB).
807 # See the documentation for a discussion of the tradeoffs in
808 # how much data we use and how many collisions we get.
809 #
810 # Returns the MD5 digest (a hex string).
811 #
812 sub Buffer2MD5
813 {
814     my($bpc, $md5, $fileSize, $dataRef) = @_;
815
816     $md5->reset();
817     $md5->add($fileSize);
818     if ( $fileSize > 262144 ) {
819         #
820         # add the first and last 131072 bytes of the string,
821         # up to 1MB.
822         #
823         my $seekPosn = ($fileSize > 1048576 ? 1048576 : $fileSize) - 131072;
824         $md5->add(substr($$dataRef, 0, 131072));
825         $md5->add(substr($$dataRef, $seekPosn, 131072));
826     } else {
827         #
828         # add the whole string
829         #
830         $md5->add($$dataRef);
831     }
832     return $md5->hexdigest;
833 }
834
835 #
836 # Given an MD5 digest $d and a compress flag, return the full
837 # path in the pool.
838 #
839 sub MD52Path
840 {
841     my($bpc, $d, $compress, $poolDir) = @_;
842
843     return if ( $d !~ m{(.)(.)(.)(.*)} );
844     $poolDir = ($compress ? $bpc->{CPoolDir} : $bpc->{PoolDir})
845                     if ( !defined($poolDir) );
846     return "$poolDir/$1/$2/$3/$1$2$3$4";
847 }
848
849 #
850 # For each file, check if the file exists in $bpc->{TopDir}/pool.
851 # If so, remove the file and make a hardlink to the file in
852 # the pool.  Otherwise, if the newFile flag is set, make a
853 # hardlink in the pool to the new file.
854 #
855 # Returns 0 if a link should be made to a new file (ie: when the file
856 #    is a new file but the newFile flag is 0).
857 # Returns 1 if a link to an existing file is made,
858 # Returns 2 if a link to a new file is made (only if $newFile is set)
859 # Returns negative on error.
860 #
861 sub MakeFileLink
862 {
863     my($bpc, $name, $d, $newFile, $compress) = @_;
864     my($i, $rawFile);
865
866     return -1 if ( !-f $name );
867     for ( $i = -1 ; ; $i++ ) {
868         return -2 if ( !defined($rawFile = $bpc->MD52Path($d, $compress)) );
869         $rawFile .= "_$i" if ( $i >= 0 );
870         if ( -f $rawFile ) {
871             if ( (stat(_))[3] < $bpc->{Conf}{HardLinkMax}
872                     && !compare($name, $rawFile) ) {
873                 unlink($name);
874                 return -3 if ( !link($rawFile, $name) );
875                 return 1;
876             }
877         } elsif ( $newFile && -f $name && (stat($name))[3] == 1 ) {
878             my($newDir);
879             ($newDir = $rawFile) =~ s{(.*)/.*}{$1};
880             mkpath($newDir, 0, 0777) if ( !-d $newDir );
881             return -4 if ( !link($name, $rawFile) );
882             return 2;
883         } else {
884             return 0;
885         }
886     }
887 }
888
889 #
890 # Tests if we can create a hardlink from a file in directory
891 # $newDir to a file in directory $targetDir.  A temporary
892 # file in $targetDir is created and an attempt to create a
893 # hardlink of the same name in $newDir is made.  The temporary
894 # files are removed.
895 #
896 # Like link(), returns true on success and false on failure.
897 #
898 sub HardlinkTest
899 {
900     my($bpc, $targetDir, $newDir) = @_;
901
902     my($targetFile, $newFile, $fd);
903     for ( my $i = 0 ; ; $i++ ) {
904         $targetFile = "$targetDir/.TestFileLink.$$.$i";
905         $newFile    = "$newDir/.TestFileLink.$$.$i";
906         last if ( !-e $targetFile && !-e $newFile );
907     }
908     return 0 if ( !open($fd, ">", $targetFile) );
909     close($fd);
910     my $ret = link($targetFile, $newFile);
911     unlink($targetFile);
912     unlink($newFile);
913     return $ret;
914 }
915
916 sub CheckHostAlive
917 {
918     my($bpc, $host) = @_;
919     my($s, $pingCmd, $ret);
920
921     #
922     # Return success if the ping cmd is undefined or empty.
923     #
924     if ( $bpc->{Conf}{PingCmd} eq "" ) {
925         print(STDERR "CheckHostAlive: return ok because \$Conf{PingCmd}"
926                    . " is empty\n") if ( $bpc->{verbose} );
927         return 0;
928     }
929
930     my $args = {
931         pingPath => $bpc->{Conf}{PingPath},
932         host     => $host,
933     };
934     $pingCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{PingCmd}, $args);
935
936     #
937     # Do a first ping in case the PC needs to wakeup
938     #
939     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
940     if ( $? ) {
941         print(STDERR "CheckHostAlive: first ping failed ($?, $!)\n")
942                         if ( $bpc->{verbose} );
943         return -1;
944     }
945
946     #
947     # Do a second ping and get the round-trip time in msec
948     #
949     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
950     if ( $? ) {
951         print(STDERR "CheckHostAlive: second ping failed ($?, $!)\n")
952                         if ( $bpc->{verbose} );
953         return -1;
954     }
955     if ( $s =~ /rtt\s*min\/avg\/max\/mdev\s*=\s*[\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+\s*(ms|usec)/i ) {
956         $ret = $1;
957         $ret /= 1000 if ( lc($2) eq "usec" );
958     } elsif ( $s =~ /time=([\d.]+)\s*(ms|usec)/i ) {
959         $ret = $1;
960         $ret /= 1000 if ( lc($2) eq "usec" );
961     } else {
962         print(STDERR "CheckHostAlive: can't extract round-trip time"
963                    . " (not fatal)\n") if ( $bpc->{verbose} );
964         $ret = 0;
965     }
966     print(STDERR "CheckHostAlive: returning $ret\n") if ( $bpc->{verbose} );
967     return $ret;
968 }
969
970 sub CheckFileSystemUsage
971 {
972     my($bpc) = @_;
973     my($topDir) = $bpc->{TopDir};
974     my($s, $dfCmd);
975
976     return 0 if ( $bpc->{Conf}{DfCmd} eq "" );
977     my $args = {
978         dfPath   => $bpc->{Conf}{DfPath},
979         topDir   => $bpc->{TopDir},
980     };
981     $dfCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{DfCmd}, $args);
982     $s = $bpc->cmdSystemOrEval($dfCmd, undef, $args);
983     return 0 if ( $? || $s !~ /(\d+)%/s );
984     return $1;
985 }
986
987 #
988 # Given an IP address, return the host name and user name via
989 # NetBios.
990 #
991 sub NetBiosInfoGet
992 {
993     my($bpc, $host) = @_;
994     my($netBiosHostName, $netBiosUserName);
995     my($s, $nmbCmd);
996
997     #
998     # Skip NetBios check if NmbLookupCmd is emtpy
999     #
1000     if ( $bpc->{Conf}{NmbLookupCmd} eq "" ) {
1001         print(STDERR "NetBiosInfoGet: return $host because \$Conf{NmbLookupCmd}"
1002                    . " is empty\n") if ( $bpc->{verbose} );
1003         return ($host, undef);
1004     }
1005
1006     my $args = {
1007         nmbLookupPath => $bpc->{Conf}{NmbLookupPath},
1008         host          => $host,
1009     };
1010     $nmbCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{NmbLookupCmd}, $args);
1011     foreach ( split(/[\n\r]+/, $bpc->cmdSystemOrEval($nmbCmd, undef, $args)) ) {
1012         next if ( !/^\s*([\w\s-]+?)\s*<(\w{2})\> - .*<ACTIVE>/i );
1013         $netBiosHostName ||= $1 if ( $2 eq "00" );  # host is first 00
1014         $netBiosUserName   = $1 if ( $2 eq "03" );  # user is last 03
1015     }
1016     if ( !defined($netBiosHostName) ) {
1017         print(STDERR "NetBiosInfoGet: failed: can't parse return string\n")
1018                         if ( $bpc->{verbose} );
1019         return;
1020     }
1021     $netBiosHostName = lc($netBiosHostName);
1022     $netBiosUserName = lc($netBiosUserName);
1023     print(STDERR "NetBiosInfoGet: success, returning host $netBiosHostName,"
1024                . " user $netBiosUserName\n") if ( $bpc->{verbose} );
1025     return ($netBiosHostName, $netBiosUserName);
1026 }
1027
1028 #
1029 # Given a NetBios name lookup the IP address via NetBios.
1030 # In the case of a host returning multiple interfaces we
1031 # return the first IP address that matches the subnet mask.
1032 # If none match the subnet mask (or nmblookup doesn't print
1033 # the subnet mask) then just the first IP address is returned.
1034 #
1035 sub NetBiosHostIPFind
1036 {
1037     my($bpc, $host) = @_;
1038     my($netBiosHostName, $netBiosUserName);
1039     my($s, $nmbCmd, $subnet, $ipAddr, $firstIpAddr);
1040
1041     #
1042     # Skip NetBios lookup if NmbLookupFindHostCmd is emtpy
1043     #
1044     if ( $bpc->{Conf}{NmbLookupFindHostCmd} eq "" ) {
1045         print(STDERR "NetBiosHostIPFind: return $host because"
1046             . " \$Conf{NmbLookupFindHostCmd} is empty\n")
1047                 if ( $bpc->{verbose} );
1048         return $host;
1049     }
1050
1051     my $args = {
1052         nmbLookupPath => $bpc->{Conf}{NmbLookupPath},
1053         host          => $host,
1054     };
1055     $nmbCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{NmbLookupFindHostCmd}, $args);
1056     foreach my $resp ( split(/[\n\r]+/, $bpc->cmdSystemOrEval($nmbCmd, undef,
1057                                                               $args) ) ) {
1058         if ( $resp =~ /querying\s+\Q$host\E\s+on\s+(\d+\.\d+\.\d+\.\d+)/i ) {
1059             $subnet = $1;
1060             $subnet = $1 if ( $subnet =~ /^(.*?)(\.255)+$/ );
1061         } elsif ( $resp =~ /^\s*(\d+\.\d+\.\d+\.\d+)\s+\Q$host/ ) {
1062             my $ip = $1;
1063             $firstIpAddr = $ip if ( !defined($firstIpAddr) );
1064             $ipAddr      = $ip if ( !defined($ipAddr) && $ip =~ /^\Q$subnet/ );
1065         }
1066     }
1067     $ipAddr = $firstIpAddr if ( !defined($ipAddr) );
1068     if ( defined($ipAddr) ) {
1069         print(STDERR "NetBiosHostIPFind: found IP address $ipAddr for"
1070                    . " host $host\n") if ( $bpc->{verbose} );
1071         return $ipAddr;
1072     } else {
1073         print(STDERR "NetBiosHostIPFind: couldn't find IP address for"
1074                    . " host $host\n") if ( $bpc->{verbose} );
1075         return;
1076     }
1077 }
1078
1079 sub fileNameEltMangle
1080 {
1081     my($bpc, $name) = @_;
1082
1083     return "" if ( $name eq "" );
1084     $name =~ s{([%/\n\r])}{sprintf("%%%02x", ord($1))}eg;
1085     return "f$name";
1086 }
1087
1088 #
1089 # We store files with every name preceded by "f".  This
1090 # avoids possible name conflicts with other information
1091 # we store in the same directories (eg: attribute info).
1092 # The process of turning a normal path into one with each
1093 # node prefixed with "f" is called mangling.
1094 #
1095 sub fileNameMangle
1096 {
1097     my($bpc, $name) = @_;
1098
1099     $name =~ s{/([^/]+)}{"/" . $bpc->fileNameEltMangle($1)}eg;
1100     $name =~ s{^([^/]+)}{$bpc->fileNameEltMangle($1)}eg;
1101     return $name;
1102 }
1103
1104 #
1105 # This undoes FileNameMangle
1106 #
1107 sub fileNameUnmangle
1108 {
1109     my($bpc, $name) = @_;
1110
1111     $name =~ s{/f}{/}g;
1112     $name =~ s{^f}{};
1113     $name =~ s{%(..)}{chr(hex($1))}eg;
1114     return $name;
1115 }
1116
1117 #
1118 # Escape shell meta-characters with backslashes.
1119 # This should be applied to each argument seperately, not an
1120 # entire shell command.
1121 #
1122 sub shellEscape
1123 {
1124     my($bpc, $cmd) = @_;
1125
1126     $cmd =~ s/([][;&()<>{}|^\n\r\t *\$\\'"`?])/\\$1/g;
1127     return $cmd;
1128 }
1129
1130 #
1131 # For printing exec commands (which don't use a shell) so they look like
1132 # a valid shell command this function should be called with the exec
1133 # args.  The shell command string is returned.
1134 #
1135 sub execCmd2ShellCmd
1136 {
1137     my($bpc, @args) = @_;
1138     my $str;
1139
1140     foreach my $a ( @args ) {
1141         $str .= " " if ( $str ne "" );
1142         $str .= $bpc->shellEscape($a);
1143     }
1144     return $str;
1145 }
1146
1147 #
1148 # Do a URI-style escape to protect/encode special characters
1149 #
1150 sub uriEsc
1151 {
1152     my($bpc, $s) = @_;
1153     $s =~ s{([^\w.\/-])}{sprintf("%%%02X", ord($1));}eg;
1154     return $s;
1155 }
1156
1157 #
1158 # Do a URI-style unescape to restore special characters
1159 #
1160 sub uriUnesc
1161 {
1162     my($bpc, $s) = @_;
1163     $s =~ s{%(..)}{chr(hex($1))}eg;
1164     return $s;
1165 }
1166
1167 #
1168 # Do variable substitution prior to execution of a command.
1169 #
1170 sub cmdVarSubstitute
1171 {
1172     my($bpc, $template, $vars) = @_;
1173     my(@cmd);
1174
1175     #
1176     # Return without any substitution if the first entry starts with "&",
1177     # indicating this is perl code.
1178     #
1179     if ( (ref($template) eq "ARRAY" ? $template->[0] : $template) =~ /^\&/ ) {
1180         return $template;
1181     }
1182     if ( ref($template) ne "ARRAY" ) {
1183         #
1184         # Split at white space, except if escaped by \
1185         #
1186         $template = [split(/(?<!\\)\s+/, $template)];
1187         #
1188         # Remove the \ that escaped white space.
1189         #
1190         foreach ( @$template ) {
1191             s{\\(\s)}{$1}g;
1192         }
1193     }
1194     #
1195     # Merge variables into @tarClientCmd
1196     #
1197     foreach my $arg ( @$template ) {
1198         #
1199         # Replace scalar variables first
1200         #
1201         $arg =~ s{\$(\w+)(\+?)}{
1202             exists($vars->{$1}) && ref($vars->{$1}) ne "ARRAY"
1203                 ? ($2 eq "+" ? $bpc->shellEscape($vars->{$1}) : $vars->{$1})
1204                 : "\$$1$2"
1205         }eg;
1206         #
1207         # Now replicate any array arguments; this just works for just one
1208         # array var in each argument.
1209         #
1210         if ( $arg =~ m{(.*)\$(\w+)(\+?)(.*)} && ref($vars->{$2}) eq "ARRAY" ) {
1211             my $pre  = $1;
1212             my $var  = $2;
1213             my $esc  = $3;
1214             my $post = $4;
1215             foreach my $v ( @{$vars->{$var}} ) {
1216                 $v = $bpc->shellEscape($v) if ( $esc eq "+" );
1217                 push(@cmd, "$pre$v$post");
1218             }
1219         } else {
1220             push(@cmd, $arg);
1221         }
1222     }
1223     return \@cmd;
1224 }
1225
1226 #
1227 # Exec or eval a command.  $cmd is either a string on an array ref.
1228 #
1229 # @args are optional arguments for the eval() case; they are not used
1230 # for exec().
1231 #
1232 sub cmdExecOrEval
1233 {
1234     my($bpc, $cmd, @args) = @_;
1235     
1236     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
1237         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
1238         print(STDERR "cmdExecOrEval: about to eval perl code $cmd\n")
1239                         if ( $bpc->{verbose} );
1240         eval($cmd);
1241         print(STDERR "Perl code fragment for exec shouldn't return!!\n");
1242         exit(1);
1243     } else {
1244         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
1245         print(STDERR "cmdExecOrEval: about to exec ",
1246               $bpc->execCmd2ShellCmd(@$cmd), "\n")
1247                         if ( $bpc->{verbose} );
1248         alarm(0);
1249         $cmd = [map { m/(.*)/ } @$cmd];         # untaint
1250         #
1251         # force list-form of exec(), ie: no shell even for 1 arg
1252         #
1253         exec { $cmd->[0] } @$cmd;
1254         print(STDERR "Exec failed for @$cmd\n");
1255         exit(1);
1256     }
1257 }
1258
1259 #
1260 # System or eval a command.  $cmd is either a string on an array ref.
1261 # $stdoutCB is a callback for output generated by the command.  If it
1262 # is undef then output is returned.  If it is a code ref then the function
1263 # is called with each piece of output as an argument.  If it is a scalar
1264 # ref the output is appended to this variable.
1265 #
1266 # @args are optional arguments for the eval() case; they are not used
1267 # for system().
1268 #
1269 # Also, $? should be set when the CHILD pipe is closed.
1270 #
1271 sub cmdSystemOrEvalLong
1272 {
1273     my($bpc, $cmd, $stdoutCB, $ignoreStderr, $pidHandlerCB, @args) = @_;
1274     my($pid, $out, $allOut);
1275     local(*CHILD);
1276     
1277     $? = 0;
1278     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
1279         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
1280         print(STDERR "cmdSystemOrEval: about to eval perl code $cmd\n")
1281                         if ( $bpc->{verbose} );
1282         $out = eval($cmd);
1283         $$stdoutCB .= $out if ( ref($stdoutCB) eq 'SCALAR' );
1284         &$stdoutCB($out)   if ( ref($stdoutCB) eq 'CODE' );
1285         print(STDERR "cmdSystemOrEval: finished: got output $out\n")
1286                         if ( $bpc->{verbose} );
1287         return $out        if ( !defined($stdoutCB) );
1288         return;
1289     } else {
1290         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
1291         print(STDERR "cmdSystemOrEval: about to system ",
1292               $bpc->execCmd2ShellCmd(@$cmd), "\n")
1293                         if ( $bpc->{verbose} );
1294         if ( !defined($pid = open(CHILD, "-|")) ) {
1295             my $err = "Can't fork to run @$cmd\n";
1296             $? = 1;
1297             $$stdoutCB .= $err if ( ref($stdoutCB) eq 'SCALAR' );
1298             &$stdoutCB($err)   if ( ref($stdoutCB) eq 'CODE' );
1299             return $err        if ( !defined($stdoutCB) );
1300             return;
1301         }
1302         binmode(CHILD);
1303         if ( !$pid ) {
1304             #
1305             # This is the child
1306             #
1307             close(STDERR);
1308             if ( $ignoreStderr ) {
1309                 open(STDERR, ">", "/dev/null");
1310             } else {
1311                 open(STDERR, ">&STDOUT");
1312             }
1313             alarm(0);
1314             $cmd = [map { m/(.*)/ } @$cmd];             # untaint
1315             #
1316             # force list-form of exec(), ie: no shell even for 1 arg
1317             #
1318             exec { $cmd->[0] } @$cmd;
1319             print(STDERR "Exec of @$cmd failed\n");
1320             exit(1);
1321         }
1322
1323         #
1324         # Notify caller of child's pid
1325         #
1326         &$pidHandlerCB($pid) if ( ref($pidHandlerCB) eq "CODE" );
1327
1328         #
1329         # The parent gathers the output from the child
1330         #
1331         while ( <CHILD> ) {
1332             $$stdoutCB .= $_ if ( ref($stdoutCB) eq 'SCALAR' );
1333             &$stdoutCB($_)   if ( ref($stdoutCB) eq 'CODE' );
1334             $out .= $_       if ( !defined($stdoutCB) );
1335             $allOut .= $_    if ( $bpc->{verbose} );
1336         }
1337         $? = 0;
1338         close(CHILD);
1339     }
1340     print(STDERR "cmdSystemOrEval: finished: got output $allOut\n")
1341                         if ( $bpc->{verbose} );
1342     return $out;
1343 }
1344
1345 #
1346 # The shorter version that sets $ignoreStderr = 0, ie: merges stdout
1347 # and stderr together.
1348 #
1349 sub cmdSystemOrEval
1350 {
1351     my($bpc, $cmd, $stdoutCB, @args) = @_;
1352
1353     return $bpc->cmdSystemOrEvalLong($cmd, $stdoutCB, 0, undef, @args);
1354 }
1355
1356 #
1357 # Promotes $conf->{BackupFilesOnly}, $conf->{BackupFilesExclude}
1358 # to hashes and $conf->{$shareName} to an array.
1359 #
1360 sub backupFileConfFix
1361 {
1362     my($bpc, $conf, $shareName) = @_;
1363
1364     $conf->{$shareName} = [ $conf->{$shareName} ]
1365                     if ( ref($conf->{$shareName}) ne "ARRAY" );
1366     foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
1367         next if ( !defined($conf->{$param}) );
1368         if ( ref($conf->{$param}) eq "HASH" ) {
1369             #
1370             # A "*" entry means wildcard - it is the default for
1371             # all shares.  Replicate the "*" entry for all shares,
1372             # but still allow override of specific entries.
1373             #
1374             next if ( !defined($conf->{$param}{"*"}) );
1375             $conf->{$param} = {
1376                                     map({ $_ => $conf->{$param}{"*"} }
1377                                             @{$conf->{$shareName}}),
1378                                     %{$conf->{$param}}
1379                               };
1380         } else {
1381             $conf->{$param} = [ $conf->{$param} ]
1382                                     if ( ref($conf->{$param}) ne "ARRAY" );
1383             $conf->{$param} = { map { $_ => $conf->{$param} }
1384                                     @{$conf->{$shareName}} };
1385         }
1386     }
1387 }
1388
1389 #
1390 # This is sort() compare function, used below.
1391 #
1392 # New client LOG names are LOG.MMYYYY.  Old style names are
1393 # LOG, LOG.0, LOG.1 etc.  Sort them so new names are
1394 # first, and newest to oldest.
1395 #
1396 sub compareLOGName
1397 {
1398     my $na = $1 if ( $a =~ /LOG\.(\d+)(\.z)?$/ );
1399     my $nb = $1 if ( $b =~ /LOG\.(\d+)(\.z)?$/ );
1400
1401     $na = -1 if ( !defined($na) );
1402     $nb = -1 if ( !defined($nb) );
1403
1404     if ( length($na) >= 5 && length($nb) >= 5 ) {
1405         #
1406         # Both new style: format is MMYYYY.  Bigger dates are
1407         # more recent.
1408         #
1409         my $ma = $2 * 12 + $1 if ( $na =~ /(\d+)(\d{4})/ );
1410         my $mb = $2 * 12 + $1 if ( $nb =~ /(\d+)(\d{4})/ );
1411         return $mb - $ma;
1412     } elsif ( length($na) >= 5 && length($nb) < 5 ) {
1413         return -1;
1414     } elsif ( length($na) < 5 && length($nb) >= 5 ) {
1415         return 1;
1416     } else {
1417         #
1418         # Both old style.  Smaller numbers are more recent.
1419         #
1420         return $na - $nb;
1421     }
1422 }
1423
1424 #
1425 # Returns list of paths to a clients's (or main) LOG files,
1426 # most recent first.
1427 #
1428 sub sortedPCLogFiles
1429 {
1430     my($bpc, $host) = @_;
1431
1432     my(@files, $dir);
1433
1434     if ( $host ne "" ) {
1435         $dir = "$bpc->{TopDir}/pc/$host";
1436     } else {
1437         $dir = "$bpc->{LogDir}";
1438     }
1439     if ( opendir(DIR, $dir) ) {
1440         foreach my $file ( readdir(DIR) ) {
1441             next if ( !-f "$dir/$file" );
1442             next if ( $file ne "LOG" && $file !~ /^LOG\.\d/ );
1443             push(@files, "$dir/$file");
1444         }
1445         closedir(DIR);
1446     }
1447     return sort(compareLOGName @files);
1448 }
1449
1450 1;