* Added Simplified Chinese CGI translation from Youlin Feng.
[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.0.0, released 28 Jan 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;
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 "" ? '/etc/BackupPC' : $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.0.0',
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 sub CheckHostAlive
890 {
891     my($bpc, $host) = @_;
892     my($s, $pingCmd, $ret);
893
894     #
895     # Return success if the ping cmd is undefined or empty.
896     #
897     if ( $bpc->{Conf}{PingCmd} eq "" ) {
898         print(STDERR "CheckHostAlive: return ok because \$Conf{PingCmd}"
899                    . " is empty\n") if ( $bpc->{verbose} );
900         return 0;
901     }
902
903     my $args = {
904         pingPath => $bpc->{Conf}{PingPath},
905         host     => $host,
906     };
907     $pingCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{PingCmd}, $args);
908
909     #
910     # Do a first ping in case the PC needs to wakeup
911     #
912     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
913     if ( $? ) {
914         print(STDERR "CheckHostAlive: first ping failed ($?, $!)\n")
915                         if ( $bpc->{verbose} );
916         return -1;
917     }
918
919     #
920     # Do a second ping and get the round-trip time in msec
921     #
922     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
923     if ( $? ) {
924         print(STDERR "CheckHostAlive: second ping failed ($?, $!)\n")
925                         if ( $bpc->{verbose} );
926         return -1;
927     }
928     if ( $s =~ /rtt\s*min\/avg\/max\/mdev\s*=\s*[\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+\s*(ms|usec)/i ) {
929         $ret = $1;
930         $ret /= 1000 if ( lc($2) eq "usec" );
931     } elsif ( $s =~ /time=([\d.]+)\s*(ms|usec)/i ) {
932         $ret = $1;
933         $ret /= 1000 if ( lc($2) eq "usec" );
934     } else {
935         print(STDERR "CheckHostAlive: can't extract round-trip time"
936                    . " (not fatal)\n") if ( $bpc->{verbose} );
937         $ret = 0;
938     }
939     print(STDERR "CheckHostAlive: returning $ret\n") if ( $bpc->{verbose} );
940     return $ret;
941 }
942
943 sub CheckFileSystemUsage
944 {
945     my($bpc) = @_;
946     my($topDir) = $bpc->{TopDir};
947     my($s, $dfCmd);
948
949     return 0 if ( $bpc->{Conf}{DfCmd} eq "" );
950     my $args = {
951         dfPath   => $bpc->{Conf}{DfPath},
952         topDir   => $bpc->{TopDir},
953     };
954     $dfCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{DfCmd}, $args);
955     $s = $bpc->cmdSystemOrEval($dfCmd, undef, $args);
956     return 0 if ( $? || $s !~ /(\d+)%/s );
957     return $1;
958 }
959
960 #
961 # Given an IP address, return the host name and user name via
962 # NetBios.
963 #
964 sub NetBiosInfoGet
965 {
966     my($bpc, $host) = @_;
967     my($netBiosHostName, $netBiosUserName);
968     my($s, $nmbCmd);
969
970     #
971     # Skip NetBios check if NmbLookupCmd is emtpy
972     #
973     if ( $bpc->{Conf}{NmbLookupCmd} eq "" ) {
974         print(STDERR "NetBiosInfoGet: return $host because \$Conf{NmbLookupCmd}"
975                    . " is empty\n") if ( $bpc->{verbose} );
976         return ($host, undef);
977     }
978
979     my $args = {
980         nmbLookupPath => $bpc->{Conf}{NmbLookupPath},
981         host          => $host,
982     };
983     $nmbCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{NmbLookupCmd}, $args);
984     foreach ( split(/[\n\r]+/, $bpc->cmdSystemOrEval($nmbCmd, undef, $args)) ) {
985         next if ( !/^\s*([\w\s-]+?)\s*<(\w{2})\> - .*<ACTIVE>/i );
986         $netBiosHostName ||= $1 if ( $2 eq "00" );  # host is first 00
987         $netBiosUserName   = $1 if ( $2 eq "03" );  # user is last 03
988     }
989     if ( !defined($netBiosHostName) ) {
990         print(STDERR "NetBiosInfoGet: failed: can't parse return string\n")
991                         if ( $bpc->{verbose} );
992         return;
993     }
994     $netBiosHostName = lc($netBiosHostName);
995     $netBiosUserName = lc($netBiosUserName);
996     print(STDERR "NetBiosInfoGet: success, returning host $netBiosHostName,"
997                . " user $netBiosUserName\n") if ( $bpc->{verbose} );
998     return ($netBiosHostName, $netBiosUserName);
999 }
1000
1001 #
1002 # Given a NetBios name lookup the IP address via NetBios.
1003 # In the case of a host returning multiple interfaces we
1004 # return the first IP address that matches the subnet mask.
1005 # If none match the subnet mask (or nmblookup doesn't print
1006 # the subnet mask) then just the first IP address is returned.
1007 #
1008 sub NetBiosHostIPFind
1009 {
1010     my($bpc, $host) = @_;
1011     my($netBiosHostName, $netBiosUserName);
1012     my($s, $nmbCmd, $subnet, $ipAddr, $firstIpAddr);
1013
1014     #
1015     # Skip NetBios lookup if NmbLookupFindHostCmd is emtpy
1016     #
1017     if ( $bpc->{Conf}{NmbLookupFindHostCmd} eq "" ) {
1018         print(STDERR "NetBiosHostIPFind: return $host because"
1019             . " \$Conf{NmbLookupFindHostCmd} is empty\n")
1020                 if ( $bpc->{verbose} );
1021         return $host;
1022     }
1023
1024     my $args = {
1025         nmbLookupPath => $bpc->{Conf}{NmbLookupPath},
1026         host          => $host,
1027     };
1028     $nmbCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{NmbLookupFindHostCmd}, $args);
1029     foreach my $resp ( split(/[\n\r]+/, $bpc->cmdSystemOrEval($nmbCmd, undef,
1030                                                               $args) ) ) {
1031         if ( $resp =~ /querying\s+\Q$host\E\s+on\s+(\d+\.\d+\.\d+\.\d+)/i ) {
1032             $subnet = $1;
1033             $subnet = $1 if ( $subnet =~ /^(.*?)(\.255)+$/ );
1034         } elsif ( $resp =~ /^\s*(\d+\.\d+\.\d+\.\d+)\s+\Q$host/ ) {
1035             my $ip = $1;
1036             $firstIpAddr = $ip if ( !defined($firstIpAddr) );
1037             $ipAddr      = $ip if ( !defined($ipAddr) && $ip =~ /^\Q$subnet/ );
1038         }
1039     }
1040     $ipAddr = $firstIpAddr if ( !defined($ipAddr) );
1041     if ( defined($ipAddr) ) {
1042         print(STDERR "NetBiosHostIPFind: found IP address $ipAddr for"
1043                    . " host $host\n") if ( $bpc->{verbose} );
1044         return $ipAddr;
1045     } else {
1046         print(STDERR "NetBiosHostIPFind: couldn't find IP address for"
1047                    . " host $host\n") if ( $bpc->{verbose} );
1048         return;
1049     }
1050 }
1051
1052 sub fileNameEltMangle
1053 {
1054     my($bpc, $name) = @_;
1055
1056     return "" if ( $name eq "" );
1057     $name =~ s{([%/\n\r])}{sprintf("%%%02x", ord($1))}eg;
1058     return "f$name";
1059 }
1060
1061 #
1062 # We store files with every name preceded by "f".  This
1063 # avoids possible name conflicts with other information
1064 # we store in the same directories (eg: attribute info).
1065 # The process of turning a normal path into one with each
1066 # node prefixed with "f" is called mangling.
1067 #
1068 sub fileNameMangle
1069 {
1070     my($bpc, $name) = @_;
1071
1072     $name =~ s{/([^/]+)}{"/" . $bpc->fileNameEltMangle($1)}eg;
1073     $name =~ s{^([^/]+)}{$bpc->fileNameEltMangle($1)}eg;
1074     return $name;
1075 }
1076
1077 #
1078 # This undoes FileNameMangle
1079 #
1080 sub fileNameUnmangle
1081 {
1082     my($bpc, $name) = @_;
1083
1084     $name =~ s{/f}{/}g;
1085     $name =~ s{^f}{};
1086     $name =~ s{%(..)}{chr(hex($1))}eg;
1087     return $name;
1088 }
1089
1090 #
1091 # Escape shell meta-characters with backslashes.
1092 # This should be applied to each argument seperately, not an
1093 # entire shell command.
1094 #
1095 sub shellEscape
1096 {
1097     my($bpc, $cmd) = @_;
1098
1099     $cmd =~ s/([][;&()<>{}|^\n\r\t *\$\\'"`?])/\\$1/g;
1100     return $cmd;
1101 }
1102
1103 #
1104 # For printing exec commands (which don't use a shell) so they look like
1105 # a valid shell command this function should be called with the exec
1106 # args.  The shell command string is returned.
1107 #
1108 sub execCmd2ShellCmd
1109 {
1110     my($bpc, @args) = @_;
1111     my $str;
1112
1113     foreach my $a ( @args ) {
1114         $str .= " " if ( $str ne "" );
1115         $str .= $bpc->shellEscape($a);
1116     }
1117     return $str;
1118 }
1119
1120 #
1121 # Do a URI-style escape to protect/encode special characters
1122 #
1123 sub uriEsc
1124 {
1125     my($bpc, $s) = @_;
1126     $s =~ s{([^\w.\/-])}{sprintf("%%%02X", ord($1));}eg;
1127     return $s;
1128 }
1129
1130 #
1131 # Do a URI-style unescape to restore special characters
1132 #
1133 sub uriUnesc
1134 {
1135     my($bpc, $s) = @_;
1136     $s =~ s{%(..)}{chr(hex($1))}eg;
1137     return $s;
1138 }
1139
1140 #
1141 # Do variable substitution prior to execution of a command.
1142 #
1143 sub cmdVarSubstitute
1144 {
1145     my($bpc, $template, $vars) = @_;
1146     my(@cmd);
1147
1148     #
1149     # Return without any substitution if the first entry starts with "&",
1150     # indicating this is perl code.
1151     #
1152     if ( (ref($template) eq "ARRAY" ? $template->[0] : $template) =~ /^\&/ ) {
1153         return $template;
1154     }
1155     if ( ref($template) ne "ARRAY" ) {
1156         #
1157         # Split at white space, except if escaped by \
1158         #
1159         $template = [split(/(?<!\\)\s+/, $template)];
1160         #
1161         # Remove the \ that escaped white space.
1162         #
1163         foreach ( @$template ) {
1164             s{\\(\s)}{$1}g;
1165         }
1166     }
1167     #
1168     # Merge variables into @tarClientCmd
1169     #
1170     foreach my $arg ( @$template ) {
1171         #
1172         # Replace scalar variables first
1173         #
1174         $arg =~ s{\$(\w+)(\+?)}{
1175             exists($vars->{$1}) && ref($vars->{$1}) ne "ARRAY"
1176                 ? ($2 eq "+" ? $bpc->shellEscape($vars->{$1}) : $vars->{$1})
1177                 : "\$$1$2"
1178         }eg;
1179         #
1180         # Now replicate any array arguments; this just works for just one
1181         # array var in each argument.
1182         #
1183         if ( $arg =~ m{(.*)\$(\w+)(\+?)(.*)} && ref($vars->{$2}) eq "ARRAY" ) {
1184             my $pre  = $1;
1185             my $var  = $2;
1186             my $esc  = $3;
1187             my $post = $4;
1188             foreach my $v ( @{$vars->{$var}} ) {
1189                 $v = $bpc->shellEscape($v) if ( $esc eq "+" );
1190                 push(@cmd, "$pre$v$post");
1191             }
1192         } else {
1193             push(@cmd, $arg);
1194         }
1195     }
1196     return \@cmd;
1197 }
1198
1199 #
1200 # Exec or eval a command.  $cmd is either a string on an array ref.
1201 #
1202 # @args are optional arguments for the eval() case; they are not used
1203 # for exec().
1204 #
1205 sub cmdExecOrEval
1206 {
1207     my($bpc, $cmd, @args) = @_;
1208     
1209     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
1210         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
1211         print(STDERR "cmdExecOrEval: about to eval perl code $cmd\n")
1212                         if ( $bpc->{verbose} );
1213         eval($cmd);
1214         print(STDERR "Perl code fragment for exec shouldn't return!!\n");
1215         exit(1);
1216     } else {
1217         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
1218         print(STDERR "cmdExecOrEval: about to exec ",
1219               $bpc->execCmd2ShellCmd(@$cmd), "\n")
1220                         if ( $bpc->{verbose} );
1221         alarm(0);
1222         $cmd = [map { m/(.*)/ } @$cmd];         # untaint
1223         #
1224         # force list-form of exec(), ie: no shell even for 1 arg
1225         #
1226         exec { $cmd->[0] } @$cmd;
1227         print(STDERR "Exec failed for @$cmd\n");
1228         exit(1);
1229     }
1230 }
1231
1232 #
1233 # System or eval a command.  $cmd is either a string on an array ref.
1234 # $stdoutCB is a callback for output generated by the command.  If it
1235 # is undef then output is returned.  If it is a code ref then the function
1236 # is called with each piece of output as an argument.  If it is a scalar
1237 # ref the output is appended to this variable.
1238 #
1239 # @args are optional arguments for the eval() case; they are not used
1240 # for system().
1241 #
1242 # Also, $? should be set when the CHILD pipe is closed.
1243 #
1244 sub cmdSystemOrEvalLong
1245 {
1246     my($bpc, $cmd, $stdoutCB, $ignoreStderr, $pidHandlerCB, @args) = @_;
1247     my($pid, $out, $allOut);
1248     local(*CHILD);
1249     
1250     $? = 0;
1251     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
1252         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
1253         print(STDERR "cmdSystemOrEval: about to eval perl code $cmd\n")
1254                         if ( $bpc->{verbose} );
1255         $out = eval($cmd);
1256         $$stdoutCB .= $out if ( ref($stdoutCB) eq 'SCALAR' );
1257         &$stdoutCB($out)   if ( ref($stdoutCB) eq 'CODE' );
1258         print(STDERR "cmdSystemOrEval: finished: got output $out\n")
1259                         if ( $bpc->{verbose} );
1260         return $out        if ( !defined($stdoutCB) );
1261         return;
1262     } else {
1263         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
1264         print(STDERR "cmdSystemOrEval: about to system ",
1265               $bpc->execCmd2ShellCmd(@$cmd), "\n")
1266                         if ( $bpc->{verbose} );
1267         if ( !defined($pid = open(CHILD, "-|")) ) {
1268             my $err = "Can't fork to run @$cmd\n";
1269             $? = 1;
1270             $$stdoutCB .= $err if ( ref($stdoutCB) eq 'SCALAR' );
1271             &$stdoutCB($err)   if ( ref($stdoutCB) eq 'CODE' );
1272             return $err        if ( !defined($stdoutCB) );
1273             return;
1274         }
1275         binmode(CHILD);
1276         if ( !$pid ) {
1277             #
1278             # This is the child
1279             #
1280             close(STDERR);
1281             if ( $ignoreStderr ) {
1282                 open(STDERR, ">", "/dev/null");
1283             } else {
1284                 open(STDERR, ">&STDOUT");
1285             }
1286             alarm(0);
1287             $cmd = [map { m/(.*)/ } @$cmd];             # untaint
1288             #
1289             # force list-form of exec(), ie: no shell even for 1 arg
1290             #
1291             exec { $cmd->[0] } @$cmd;
1292             print(STDERR "Exec of @$cmd failed\n");
1293             exit(1);
1294         }
1295
1296         #
1297         # Notify caller of child's pid
1298         #
1299         &$pidHandlerCB($pid) if ( ref($pidHandlerCB) eq "CODE" );
1300
1301         #
1302         # The parent gathers the output from the child
1303         #
1304         while ( <CHILD> ) {
1305             $$stdoutCB .= $_ if ( ref($stdoutCB) eq 'SCALAR' );
1306             &$stdoutCB($_)   if ( ref($stdoutCB) eq 'CODE' );
1307             $out .= $_       if ( !defined($stdoutCB) );
1308             $allOut .= $_    if ( $bpc->{verbose} );
1309         }
1310         $? = 0;
1311         close(CHILD);
1312     }
1313     print(STDERR "cmdSystemOrEval: finished: got output $allOut\n")
1314                         if ( $bpc->{verbose} );
1315     return $out;
1316 }
1317
1318 #
1319 # The shorter version that sets $ignoreStderr = 0, ie: merges stdout
1320 # and stderr together.
1321 #
1322 sub cmdSystemOrEval
1323 {
1324     my($bpc, $cmd, $stdoutCB, @args) = @_;
1325
1326     return $bpc->cmdSystemOrEvalLong($cmd, $stdoutCB, 0, undef, @args);
1327 }
1328
1329 #
1330 # Promotes $conf->{BackupFilesOnly}, $conf->{BackupFilesExclude}
1331 # to hashes and $conf->{$shareName} to an array.
1332 #
1333 sub backupFileConfFix
1334 {
1335     my($bpc, $conf, $shareName) = @_;
1336
1337     $conf->{$shareName} = [ $conf->{$shareName} ]
1338                     if ( ref($conf->{$shareName}) ne "ARRAY" );
1339     foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
1340         next if ( !defined($conf->{$param}) );
1341         if ( ref($conf->{$param}) eq "HASH" ) {
1342             #
1343             # A "*" entry means wildcard - it is the default for
1344             # all shares.  Replicate the "*" entry for all shares,
1345             # but still allow override of specific entries.
1346             #
1347             next if ( !defined($conf->{$param}{"*"}) );
1348             $conf->{$param} = {
1349                                     map({ $_ => $conf->{$param}{"*"} }
1350                                             @{$conf->{$shareName}}),
1351                                     %{$conf->{$param}}
1352                               };
1353         } else {
1354             $conf->{$param} = [ $conf->{$param} ]
1355                                     if ( ref($conf->{$param}) ne "ARRAY" );
1356             $conf->{$param} = { map { $_ => $conf->{$param} }
1357                                     @{$conf->{$shareName}} };
1358         }
1359     }
1360 }
1361
1362 #
1363 # This is sort() compare function, used below.
1364 #
1365 # New client LOG names are LOG.MMYYYY.  Old style names are
1366 # LOG, LOG.0, LOG.1 etc.  Sort them so new names are
1367 # first, and newest to oldest.
1368 #
1369 sub compareLOGName
1370 {
1371     my $na = $1 if ( $a =~ /LOG\.(\d+)(\.z)?$/ );
1372     my $nb = $1 if ( $b =~ /LOG\.(\d+)(\.z)?$/ );
1373
1374     $na = -1 if ( !defined($na) );
1375     $nb = -1 if ( !defined($nb) );
1376
1377     if ( length($na) >= 5 && length($nb) >= 5 ) {
1378         #
1379         # Both new style: format is MMYYYY.  Bigger dates are
1380         # more recent.
1381         #
1382         my $ma = $2 * 12 + $1 if ( $na =~ /(\d+)(\d{4})/ );
1383         my $mb = $2 * 12 + $1 if ( $nb =~ /(\d+)(\d{4})/ );
1384         return $mb - $ma;
1385     } elsif ( length($na) >= 5 && length($nb) < 5 ) {
1386         return -1;
1387     } elsif ( length($na) < 5 && length($nb) >= 5 ) {
1388         return 1;
1389     } else {
1390         #
1391         # Both old style.  Smaller numbers are more recent.
1392         #
1393         return $na - $nb;
1394     }
1395 }
1396
1397 #
1398 # Returns list of paths to a clients's (or main) LOG files,
1399 # most recent first.
1400 #
1401 sub sortedPCLogFiles
1402 {
1403     my($bpc, $host) = @_;
1404
1405     my(@files, $dir);
1406
1407     if ( $host ne "" ) {
1408         $dir = "$bpc->{TopDir}/pc/$host";
1409     } else {
1410         $dir = "$bpc->{LogDir}";
1411     }
1412     if ( opendir(DIR, $dir) ) {
1413         foreach my $file ( readdir(DIR) ) {
1414             next if ( !-f "$dir/$file" );
1415             next if ( $file ne "LOG" && $file !~ /^LOG\.\d/ );
1416             push(@files, "$dir/$file");
1417         }
1418         closedir(DIR);
1419     }
1420     return sort(compareLOGName @files);
1421 }
1422
1423 1;