- fixed configure.pl and makeDist.
[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.0alpha, released 23 Jan 2006.
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 qw/:flock/;
45 use Carp;
46 use DirHandle ();
47 use File::Path;
48 use File::Compare;
49 use Socket;
50 use Cwd;
51 use Digest::MD5;
52 use Config;
53
54 sub new
55 {
56     my $class = shift;
57     my($topDir, $installDir, $confDir, $noUserCheck) = @_;
58
59     #
60     # Whether to use filesystem hierarchy standard for file layout.
61     # If set, text config files are below /etc/BackupPC.
62     #
63     my $useFHS = 0;
64     my $paths;
65
66     #
67     # Set defaults for $topDir and $installDir.
68     #
69     $topDir     = '/tera0/backup/BackupPC' if ( $topDir eq "" );
70     $installDir = '/usr/local/BackupPC'    if ( $installDir eq "" );
71
72     #
73     # Pick some initial defaults.  For FHS the only critical
74     # path is the ConfDir, since we get everything else out
75     # of the main config file.
76     #
77     if ( $useFHS ) {
78         $paths = {
79             useFHS  => $useFHS,
80             TopDir  => $topDir,
81             BinDir  => "$installDir/bin",
82             LibDir  => "$installDir/lib",
83             ConfDir => $confDir eq "" ? '/etc/BackupPC' : $confDir,
84             LogDir  => '/var/log/BackupPC',
85         };
86     } else {
87         $paths = {
88             useFHS  => $useFHS,
89             TopDir  => $topDir,
90             BinDir  => "$installDir/bin",
91             LibDir  => "$installDir/lib",
92             ConfDir => $confDir eq "" ? "$topDir/conf" : $confDir,
93             LogDir  => "$topDir/log",
94         };
95     }
96
97     my $bpc = bless {
98         %$paths,
99         Version => '3.0.0alpha',
100     }, $class;
101
102     $bpc->{storage} = BackupPC::Storage->new($paths);
103
104     #
105     # Clean up %ENV and setup other variables.
106     #
107     delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
108     $bpc->{PoolDir}  = "$bpc->{TopDir}/pool";
109     $bpc->{CPoolDir} = "$bpc->{TopDir}/cpool";
110     if ( defined(my $error = $bpc->ConfigRead()) ) {
111         print(STDERR $error, "\n");
112         return;
113     }
114
115     #
116     # Update the paths based on the config file
117     #
118     foreach my $dir ( qw(TopDir BinDir LibDir ConfDir LogDir) ) {
119         next if ( !defined($bpc->{Conf}{$dir}) );
120         $paths->{$dir} = $bpc->{$dir} = $bpc->{Conf}{$dir};
121     }
122     $bpc->{storage}->setPaths($paths);
123
124     #
125     # Verify we are running as the correct user
126     #
127     if ( !$noUserCheck
128             && $bpc->{Conf}{BackupPCUserVerify}
129             && $> != (my $uid = (getpwnam($bpc->{Conf}{BackupPCUser}))[2]) ) {
130         print(STDERR "Wrong user: my userid is $>, instead of $uid"
131             . " ($bpc->{Conf}{BackupPCUser})\n");
132         return;
133     }
134     return $bpc;
135 }
136
137 sub TopDir
138 {
139     my($bpc) = @_;
140     return $bpc->{TopDir};
141 }
142
143 sub BinDir
144 {
145     my($bpc) = @_;
146     return $bpc->{BinDir};
147 }
148
149 sub LogDir
150 {
151     my($bpc) = @_;
152     return $bpc->{LogDir};
153 }
154
155 sub ConfDir
156 {
157     my($bpc) = @_;
158     return $bpc->{ConfDir};
159 }
160
161 sub LibDir
162 {
163     my($bpc) = @_;
164     return $bpc->{LibDir};
165 }
166
167 sub useFHS
168 {
169     my($bpc) = @_;
170     return $bpc->{useFHS};
171 }
172
173 sub Version
174 {
175     my($bpc) = @_;
176     return $bpc->{Version};
177 }
178
179 sub Conf
180 {
181     my($bpc) = @_;
182     return %{$bpc->{Conf}};
183 }
184
185 sub Lang
186 {
187     my($bpc) = @_;
188     return $bpc->{Lang};
189 }
190
191 sub adminJob
192 {
193     my($bpc, $num) = @_;
194     return " admin " if ( !$num );
195     return " admin$num ";
196 }
197
198 sub isAdminJob
199 {
200     my($bpc, $str) = @_;
201     return $str =~ /^ admin/;
202 }
203
204 sub trashJob
205 {
206     return " trashClean ";
207 }
208
209 sub ConfValue
210 {
211     my($bpc, $param) = @_;
212
213     return $bpc->{Conf}{$param};
214 }
215
216 sub verbose
217 {
218     my($bpc, $param) = @_;
219
220     $bpc->{verbose} = $param if ( defined($param) );
221     return $bpc->{verbose};
222 }
223
224 sub sigName2num
225 {
226     my($bpc, $sig) = @_;
227
228     if ( !defined($bpc->{SigName2Num}) ) {
229         my $i = 0;
230         foreach my $name ( split(' ', $Config{sig_name}) ) {
231             $bpc->{SigName2Num}{$name} = $i;
232             $i++;
233         }
234     }
235     return $bpc->{SigName2Num}{$sig};
236 }
237
238 #
239 # Generate an ISO 8601 format timeStamp (but without the "T").
240 # See http://www.w3.org/TR/NOTE-datetime and
241 # http://www.cl.cam.ac.uk/~mgk25/iso-time.html
242 #
243 sub timeStamp
244 {
245     my($bpc, $t, $noPad) = @_;
246     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
247               = localtime($t || time);
248     return sprintf("%04d-%02d-%02d %02d:%02d:%02d",
249                     $year + 1900, $mon + 1, $mday, $hour, $min, $sec)
250              . ($noPad ? "" : " ");
251 }
252
253 sub BackupInfoRead
254 {
255     my($bpc, $host) = @_;
256
257     return $bpc->{storage}->BackupInfoRead($host);
258 }
259
260 sub BackupInfoWrite
261 {
262     my($bpc, $host, @Backups) = @_;
263
264     return $bpc->{storage}->BackupInfoWrite($host, @Backups);
265 }
266
267 sub RestoreInfoRead
268 {
269     my($bpc, $host) = @_;
270
271     return $bpc->{storage}->RestoreInfoRead($host);
272 }
273
274 sub RestoreInfoWrite
275 {
276     my($bpc, $host, @Restores) = @_;
277
278     return $bpc->{storage}->RestoreInfoWrite($host, @Restores);
279 }
280
281 sub ArchiveInfoRead
282 {
283     my($bpc, $host) = @_;
284
285     return $bpc->{storage}->ArchiveInfoRead($host);
286 }
287
288 sub ArchiveInfoWrite
289 {
290     my($bpc, $host, @Archives) = @_;
291
292     return $bpc->{storage}->ArchiveInfoWrite($host, @Archives);
293 }
294
295 sub ConfigDataRead
296 {
297     my($bpc, $host) = @_;
298
299     return $bpc->{storage}->ConfigDataRead($host);
300 }
301
302 sub ConfigDataWrite
303 {
304     my($bpc, $host, $conf) = @_;
305
306     return $bpc->{storage}->ConfigDataWrite($host, $conf);
307 }
308
309 sub ConfigRead
310 {
311     my($bpc, $host) = @_;
312     my($ret);
313
314     #
315     # Read main config file
316     #
317     my($mesg, $config) = $bpc->{storage}->ConfigDataRead();
318     return $mesg if ( defined($mesg) );
319
320     $bpc->{Conf} = $config;
321
322     #
323     # Read host config file
324     #
325     if ( $host ne "" ) {
326         ($mesg, $config) = $bpc->{storage}->ConfigDataRead($host);
327         return $mesg if ( defined($mesg) );
328         $bpc->{Conf} = { %{$bpc->{Conf}}, %$config };
329     }
330
331     #
332     # Load optional perl modules
333     #
334     if ( defined($bpc->{Conf}{PerlModuleLoad}) ) {
335         #
336         # Load any user-specified perl modules.  This is for
337         # optional user-defined extensions.
338         #
339         $bpc->{Conf}{PerlModuleLoad} = [$bpc->{Conf}{PerlModuleLoad}]
340                     if ( ref($bpc->{Conf}{PerlModuleLoad}) ne "ARRAY" );
341         foreach my $module ( @{$bpc->{Conf}{PerlModuleLoad}} ) {
342             eval("use $module;");
343         }
344     }
345
346     #
347     # Load language file
348     #
349     return "No language setting" if ( !defined($bpc->{Conf}{Language}) );
350     my $langFile = "$bpc->{LibDir}/BackupPC/Lang/$bpc->{Conf}{Language}.pm";
351     if ( !defined($ret = do $langFile) && ($! || $@) ) {
352         $mesg = "Couldn't open language file $langFile: $!" if ( $! );
353         $mesg = "Couldn't execute language file $langFile: $@" if ( $@ );
354         $mesg =~ s/[\n\r]+//;
355         return $mesg;
356     }
357     $bpc->{Lang} = \%Lang;
358     return;
359 }
360
361 #
362 # Return the mtime of the config file
363 #
364 sub ConfigMTime
365 {
366     my($bpc) = @_;
367
368     return $bpc->{storage}->ConfigMTime();
369 }
370
371 #
372 # Returns information from the host file in $bpc->{TopDir}/conf/hosts.
373 # With no argument a ref to a hash of hosts is returned.  Each
374 # hash contains fields as specified in the hosts file.  With an
375 # argument a ref to a single hash is returned with information
376 # for just that host.
377 #
378 sub HostInfoRead
379 {
380     my($bpc, $host) = @_;
381
382     return $bpc->{storage}->HostInfoRead($host);
383 }
384
385 sub HostInfoWrite
386 {
387     my($bpc, $host) = @_;
388
389     return $bpc->{storage}->HostInfoWrite($host);
390 }
391
392 #
393 # Return the mtime of the hosts file
394 #
395 sub HostsMTime
396 {
397     my($bpc) = @_;
398
399     return $bpc->{storage}->HostsMTime();
400 }
401
402 #
403 # Stripped down from File::Path.  In particular we don't print
404 # many warnings and we try three times to delete each directory
405 # and file -- for some reason the original File::Path rmtree
406 # didn't always completely remove a directory tree on the NetApp.
407 #
408 # Warning: this routine changes the cwd.
409 #
410 sub RmTreeQuiet
411 {
412     my($bpc, $pwd, $roots) = @_;
413     my(@files, $root);
414
415     if ( defined($roots) && length($roots) ) {
416       $roots = [$roots] unless ref $roots;
417     } else {
418       print(STDERR "RmTreeQuiet: No root path(s) specified\n");
419     }
420     chdir($pwd);
421     foreach $root (@{$roots}) {
422         $root = $1 if ( $root =~ m{(.*?)/*$} );
423         #
424         # Try first to simply unlink the file: this avoids an
425         # extra stat for every file.  If it fails (which it
426         # will for directories), check if it is a directory and
427         # then recurse.
428         #
429         if ( !unlink($root) ) {
430             if ( -d $root ) {
431                 my $d = DirHandle->new($root);
432                 if ( !defined($d) ) {
433                     print(STDERR "Can't read $pwd/$root: $!\n");
434                 } else {
435                     @files = $d->read;
436                     $d->close;
437                     @files = grep $_!~/^\.{1,2}$/, @files;
438                     $bpc->RmTreeQuiet("$pwd/$root", \@files);
439                     chdir($pwd);
440                     rmdir($root) || rmdir($root);
441                 }
442             } else {
443                 unlink($root) || unlink($root);
444             }
445         }
446     }
447 }
448
449 #
450 # Move a directory or file away for later deletion
451 #
452 sub RmTreeDefer
453 {
454     my($bpc, $trashDir, $file) = @_;
455     my($i, $f);
456
457     return if ( !-e $file );
458     mkpath($trashDir, 0, 0777) if ( !-d $trashDir );
459     for ( $i = 0 ; $i < 1000 ; $i++ ) {
460         $f = sprintf("%s/%d_%d_%d", $trashDir, time, $$, $i);
461         next if ( -e $f );
462         return if ( rename($file, $f) );
463     }
464     # shouldn't get here, but might if you tried to call this
465     # across file systems.... just remove the tree right now.
466     if ( $file =~ /(.*)\/([^\/]*)/ ) {
467         my($d) = $1;
468         my($f) = $2;
469         my($cwd) = Cwd::fastcwd();
470         $cwd = $1 if ( $cwd =~ /(.*)/ );
471         $bpc->RmTreeQuiet($d, $f);
472         chdir($cwd) if ( $cwd );
473     }
474 }
475
476 #
477 # Empty the trash directory.  Returns 0 if it did nothing, 1 if it
478 # did something, -1 if it failed to remove all the files.
479 #
480 sub RmTreeTrashEmpty
481 {
482     my($bpc, $trashDir) = @_;
483     my(@files);
484     my($cwd) = Cwd::fastcwd();
485
486     $cwd = $1 if ( $cwd =~ /(.*)/ );
487     return if ( !-d $trashDir );
488     my $d = DirHandle->new($trashDir) or carp "Can't read $trashDir: $!";
489     @files = $d->read;
490     $d->close;
491     @files = grep $_!~/^\.{1,2}$/, @files;
492     return 0 if ( !@files );
493     $bpc->RmTreeQuiet($trashDir, \@files);
494     foreach my $f ( @files ) {
495         return -1 if ( -e $f );
496     }
497     chdir($cwd) if ( $cwd );
498     return 1;
499 }
500
501 #
502 # Open a connection to the server.  Returns an error string on failure.
503 # Returns undef on success.
504 #
505 sub ServerConnect
506 {
507     my($bpc, $host, $port, $justConnect) = @_;
508     local(*FH);
509
510     return if ( defined($bpc->{ServerFD}) );
511     #
512     # First try the unix-domain socket
513     #
514     my $sockFile = "$bpc->{LogDir}/BackupPC.sock";
515     socket(*FH, PF_UNIX, SOCK_STREAM, 0)     || return "unix socket: $!";
516     if ( !connect(*FH, sockaddr_un($sockFile)) ) {
517         my $err = "unix connect: $!";
518         close(*FH);
519         if ( $port > 0 ) {
520             my $proto = getprotobyname('tcp');
521             my $iaddr = inet_aton($host)     || return "unknown host $host";
522             my $paddr = sockaddr_in($port, $iaddr);
523
524             socket(*FH, PF_INET, SOCK_STREAM, $proto)
525                                              || return "inet socket: $!";
526             connect(*FH, $paddr)             || return "inet connect: $!";
527         } else {
528             return $err;
529         }
530     }
531     my($oldFH) = select(*FH); $| = 1; select($oldFH);
532     $bpc->{ServerFD} = *FH;
533     return if ( $justConnect );
534     #
535     # Read the seed that we need for our MD5 message digest.  See
536     # ServerMesg below.
537     #
538     sysread($bpc->{ServerFD}, $bpc->{ServerSeed}, 1024);
539     $bpc->{ServerMesgCnt} = 0;
540     return;
541 }
542
543 #
544 # Check that the server connection is still ok
545 #
546 sub ServerOK
547 {
548     my($bpc) = @_;
549
550     return 0 if ( !defined($bpc->{ServerFD}) );
551     vec(my $FDread, fileno($bpc->{ServerFD}), 1) = 1;
552     my $ein = $FDread;
553     return 0 if ( select(my $rout = $FDread, undef, $ein, 0.0) < 0 );
554     return 1 if ( !vec($rout, fileno($bpc->{ServerFD}), 1) );
555 }
556
557 #
558 # Disconnect from the server
559 #
560 sub ServerDisconnect
561 {
562     my($bpc) = @_;
563     return if ( !defined($bpc->{ServerFD}) );
564     close($bpc->{ServerFD});
565     delete($bpc->{ServerFD});
566 }
567
568 #
569 # Sends a message to the server and returns with the reply.
570 #
571 # To avoid possible attacks via the TCP socket interface, every client
572 # message is protected by an MD5 digest. The MD5 digest includes four
573 # items:
574 #   - a seed that is sent to us when we first connect
575 #   - a sequence number that increments for each message
576 #   - a shared secret that is stored in $Conf{ServerMesgSecret}
577 #   - the message itself.
578 # The message is sent in plain text preceded by the MD5 digest. A
579 # snooper can see the plain-text seed sent by BackupPC and plain-text
580 # message, but cannot construct a valid MD5 digest since the secret in
581 # $Conf{ServerMesgSecret} is unknown. A replay attack is not possible
582 # since the seed changes on a per-connection and per-message basis.
583 #
584 sub ServerMesg
585 {
586     my($bpc, $mesg) = @_;
587     return if ( !defined(my $fh = $bpc->{ServerFD}) );
588     my $md5 = Digest::MD5->new;
589     $md5->add($bpc->{ServerSeed} . $bpc->{ServerMesgCnt}
590             . $bpc->{Conf}{ServerMesgSecret} . $mesg);
591     print($fh $md5->b64digest . " $mesg\n");
592     $bpc->{ServerMesgCnt}++;
593     return <$fh>;
594 }
595
596 #
597 # Do initialization for child processes
598 #
599 sub ChildInit
600 {
601     my($bpc) = @_;
602     close(STDERR);
603     open(STDERR, ">&STDOUT");
604     select(STDERR); $| = 1;
605     select(STDOUT); $| = 1;
606     $ENV{PATH} = $bpc->{Conf}{MyPath};
607 }
608
609 #
610 # Compute the MD5 digest of a file.  For efficiency we don't
611 # use the whole file for big files:
612 #   - for files <= 256K we use the file size and the whole file.
613 #   - for files <= 1M we use the file size, the first 128K and
614 #     the last 128K.
615 #   - for files > 1M, we use the file size, the first 128K and
616 #     the 8th 128K (ie: the 128K up to 1MB).
617 # See the documentation for a discussion of the tradeoffs in
618 # how much data we use and how many collisions we get.
619 #
620 # Returns the MD5 digest (a hex string) and the file size.
621 #
622 sub File2MD5
623 {
624     my($bpc, $md5, $name) = @_;
625     my($data, $fileSize);
626     local(*N);
627
628     $fileSize = (stat($name))[7];
629     return ("", -1) if ( !-f _ );
630     $name = $1 if ( $name =~ /(.*)/ );
631     return ("", 0) if ( $fileSize == 0 );
632     return ("", -1) if ( !open(N, $name) );
633     binmode(N);
634     $md5->reset();
635     $md5->add($fileSize);
636     if ( $fileSize > 262144 ) {
637         #
638         # read the first and last 131072 bytes of the file,
639         # up to 1MB.
640         #
641         my $seekPosn = ($fileSize > 1048576 ? 1048576 : $fileSize) - 131072;
642         $md5->add($data) if ( sysread(N, $data, 131072) );
643         $md5->add($data) if ( sysseek(N, $seekPosn, 0)
644                                 && sysread(N, $data, 131072) );
645     } else {
646         #
647         # read the whole file
648         #
649         $md5->add($data) if ( sysread(N, $data, $fileSize) );
650     }
651     close(N);
652     return ($md5->hexdigest, $fileSize);
653 }
654
655 #
656 # Compute the MD5 digest of a buffer (string).  For efficiency we don't
657 # use the whole string for big strings:
658 #   - for files <= 256K we use the file size and the whole file.
659 #   - for files <= 1M we use the file size, the first 128K and
660 #     the last 128K.
661 #   - for files > 1M, we use the file size, the first 128K and
662 #     the 8th 128K (ie: the 128K up to 1MB).
663 # See the documentation for a discussion of the tradeoffs in
664 # how much data we use and how many collisions we get.
665 #
666 # Returns the MD5 digest (a hex string).
667 #
668 sub Buffer2MD5
669 {
670     my($bpc, $md5, $fileSize, $dataRef) = @_;
671
672     $md5->reset();
673     $md5->add($fileSize);
674     if ( $fileSize > 262144 ) {
675         #
676         # add the first and last 131072 bytes of the string,
677         # up to 1MB.
678         #
679         my $seekPosn = ($fileSize > 1048576 ? 1048576 : $fileSize) - 131072;
680         $md5->add(substr($$dataRef, 0, 131072));
681         $md5->add(substr($$dataRef, $seekPosn, 131072));
682     } else {
683         #
684         # add the whole string
685         #
686         $md5->add($$dataRef);
687     }
688     return $md5->hexdigest;
689 }
690
691 #
692 # Given an MD5 digest $d and a compress flag, return the full
693 # path in the pool.
694 #
695 sub MD52Path
696 {
697     my($bpc, $d, $compress, $poolDir) = @_;
698
699     return if ( $d !~ m{(.)(.)(.)(.*)} );
700     $poolDir = ($compress ? $bpc->{CPoolDir} : $bpc->{PoolDir})
701                     if ( !defined($poolDir) );
702     return "$poolDir/$1/$2/$3/$1$2$3$4";
703 }
704
705 #
706 # For each file, check if the file exists in $bpc->{TopDir}/pool.
707 # If so, remove the file and make a hardlink to the file in
708 # the pool.  Otherwise, if the newFile flag is set, make a
709 # hardlink in the pool to the new file.
710 #
711 # Returns 0 if a link should be made to a new file (ie: when the file
712 #    is a new file but the newFile flag is 0).
713 # Returns 1 if a link to an existing file is made,
714 # Returns 2 if a link to a new file is made (only if $newFile is set)
715 # Returns negative on error.
716 #
717 sub MakeFileLink
718 {
719     my($bpc, $name, $d, $newFile, $compress) = @_;
720     my($i, $rawFile);
721
722     return -1 if ( !-f $name );
723     for ( $i = -1 ; ; $i++ ) {
724         return -2 if ( !defined($rawFile = $bpc->MD52Path($d, $compress)) );
725         $rawFile .= "_$i" if ( $i >= 0 );
726         if ( -f $rawFile ) {
727             if ( (stat(_))[3] < $bpc->{Conf}{HardLinkMax}
728                     && !compare($name, $rawFile) ) {
729                 unlink($name);
730                 return -3 if ( !link($rawFile, $name) );
731                 return 1;
732             }
733         } elsif ( $newFile && -f $name && (stat($name))[3] == 1 ) {
734             my($newDir);
735             ($newDir = $rawFile) =~ s{(.*)/.*}{$1};
736             mkpath($newDir, 0, 0777) if ( !-d $newDir );
737             return -4 if ( !link($name, $rawFile) );
738             return 2;
739         } else {
740             return 0;
741         }
742     }
743 }
744
745 sub CheckHostAlive
746 {
747     my($bpc, $host) = @_;
748     my($s, $pingCmd, $ret);
749
750     #
751     # Return success if the ping cmd is undefined or empty.
752     #
753     if ( $bpc->{Conf}{PingCmd} eq "" ) {
754         print(STDERR "CheckHostAlive: return ok because \$Conf{PingCmd}"
755                    . " is empty\n") if ( $bpc->{verbose} );
756         return 0;
757     }
758
759     my $args = {
760         pingPath => $bpc->{Conf}{PingPath},
761         host     => $host,
762     };
763     $pingCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{PingCmd}, $args);
764
765     #
766     # Do a first ping in case the PC needs to wakeup
767     #
768     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
769     if ( $? ) {
770         print(STDERR "CheckHostAlive: first ping failed ($?, $!)\n")
771                         if ( $bpc->{verbose} );
772         return -1;
773     }
774
775     #
776     # Do a second ping and get the round-trip time in msec
777     #
778     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
779     if ( $? ) {
780         print(STDERR "CheckHostAlive: second ping failed ($?, $!)\n")
781                         if ( $bpc->{verbose} );
782         return -1;
783     }
784     if ( $s =~ /rtt\s*min\/avg\/max\/mdev\s*=\s*[\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+\s*(ms|usec)/i ) {
785         $ret = $1;
786         $ret /= 1000 if ( lc($2) eq "usec" );
787     } elsif ( $s =~ /time=([\d.]+)\s*(ms|usec)/i ) {
788         $ret = $1;
789         $ret /= 1000 if ( lc($2) eq "usec" );
790     } else {
791         print(STDERR "CheckHostAlive: can't extract round-trip time"
792                    . " (not fatal)\n") if ( $bpc->{verbose} );
793         $ret = 0;
794     }
795     print(STDERR "CheckHostAlive: returning $ret\n") if ( $bpc->{verbose} );
796     return $ret;
797 }
798
799 sub CheckFileSystemUsage
800 {
801     my($bpc) = @_;
802     my($topDir) = $bpc->{TopDir};
803     my($s, $dfCmd);
804
805     return 0 if ( $bpc->{Conf}{DfCmd} eq "" );
806     my $args = {
807         dfPath   => $bpc->{Conf}{DfPath},
808         topDir   => $bpc->{TopDir},
809     };
810     $dfCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{DfCmd}, $args);
811     $s = $bpc->cmdSystemOrEval($dfCmd, undef, $args);
812     return 0 if ( $? || $s !~ /(\d+)%/s );
813     return $1;
814 }
815
816 #
817 # Given an IP address, return the host name and user name via
818 # NetBios.
819 #
820 sub NetBiosInfoGet
821 {
822     my($bpc, $host) = @_;
823     my($netBiosHostName, $netBiosUserName);
824     my($s, $nmbCmd);
825
826     #
827     # Skip NetBios check if NmbLookupCmd is emtpy
828     #
829     if ( $bpc->{Conf}{NmbLookupCmd} eq "" ) {
830         print(STDERR "NetBiosInfoGet: return $host because \$Conf{NmbLookupCmd}"
831                    . " is empty\n") if ( $bpc->{verbose} );
832         return ($host, undef);
833     }
834
835     my $args = {
836         nmbLookupPath => $bpc->{Conf}{NmbLookupPath},
837         host          => $host,
838     };
839     $nmbCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{NmbLookupCmd}, $args);
840     foreach ( split(/[\n\r]+/, $bpc->cmdSystemOrEval($nmbCmd, undef, $args)) ) {
841         next if ( !/^\s*([\w\s-]+?)\s*<(\w{2})\> - .*<ACTIVE>/i );
842         $netBiosHostName ||= $1 if ( $2 eq "00" );  # host is first 00
843         $netBiosUserName   = $1 if ( $2 eq "03" );  # user is last 03
844     }
845     if ( !defined($netBiosHostName) ) {
846         print(STDERR "NetBiosInfoGet: failed: can't parse return string\n")
847                         if ( $bpc->{verbose} );
848         return;
849     }
850     $netBiosHostName = lc($netBiosHostName);
851     $netBiosUserName = lc($netBiosUserName);
852     print(STDERR "NetBiosInfoGet: success, returning host $netBiosHostName,"
853                . " user $netBiosUserName\n") if ( $bpc->{verbose} );
854     return ($netBiosHostName, $netBiosUserName);
855 }
856
857 #
858 # Given a NetBios name lookup the IP address via NetBios.
859 # In the case of a host returning multiple interfaces we
860 # return the first IP address that matches the subnet mask.
861 # If none match the subnet mask (or nmblookup doesn't print
862 # the subnet mask) then just the first IP address is returned.
863 #
864 sub NetBiosHostIPFind
865 {
866     my($bpc, $host) = @_;
867     my($netBiosHostName, $netBiosUserName);
868     my($s, $nmbCmd, $subnet, $ipAddr, $firstIpAddr);
869
870     #
871     # Skip NetBios lookup if NmbLookupFindHostCmd is emtpy
872     #
873     if ( $bpc->{Conf}{NmbLookupFindHostCmd} eq "" ) {
874         print(STDERR "NetBiosHostIPFind: return $host because"
875             . " \$Conf{NmbLookupFindHostCmd} is empty\n")
876                 if ( $bpc->{verbose} );
877         return $host;
878     }
879
880     my $args = {
881         nmbLookupPath => $bpc->{Conf}{NmbLookupPath},
882         host          => $host,
883     };
884     $nmbCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{NmbLookupFindHostCmd}, $args);
885     foreach my $resp ( split(/[\n\r]+/, $bpc->cmdSystemOrEval($nmbCmd, undef,
886                                                               $args) ) ) {
887         if ( $resp =~ /querying\s+\Q$host\E\s+on\s+(\d+\.\d+\.\d+\.\d+)/i ) {
888             $subnet = $1;
889             $subnet = $1 if ( $subnet =~ /^(.*?)(\.255)+$/ );
890         } elsif ( $resp =~ /^\s*(\d+\.\d+\.\d+\.\d+)\s+\Q$host/ ) {
891             my $ip = $1;
892             $firstIpAddr = $ip if ( !defined($firstIpAddr) );
893             $ipAddr      = $ip if ( !defined($ipAddr) && $ip =~ /^\Q$subnet/ );
894         }
895     }
896     $ipAddr = $firstIpAddr if ( !defined($ipAddr) );
897     if ( defined($ipAddr) ) {
898         print(STDERR "NetBiosHostIPFind: found IP address $ipAddr for"
899                    . " host $host\n") if ( $bpc->{verbose} );
900         return $ipAddr;
901     } else {
902         print(STDERR "NetBiosHostIPFind: couldn't find IP address for"
903                    . " host $host\n") if ( $bpc->{verbose} );
904         return;
905     }
906 }
907
908 sub fileNameEltMangle
909 {
910     my($bpc, $name) = @_;
911
912     return "" if ( $name eq "" );
913     $name =~ s{([%/\n\r])}{sprintf("%%%02x", ord($1))}eg;
914     return "f$name";
915 }
916
917 #
918 # We store files with every name preceded by "f".  This
919 # avoids possible name conflicts with other information
920 # we store in the same directories (eg: attribute info).
921 # The process of turning a normal path into one with each
922 # node prefixed with "f" is called mangling.
923 #
924 sub fileNameMangle
925 {
926     my($bpc, $name) = @_;
927
928     $name =~ s{/([^/]+)}{"/" . $bpc->fileNameEltMangle($1)}eg;
929     $name =~ s{^([^/]+)}{$bpc->fileNameEltMangle($1)}eg;
930     return $name;
931 }
932
933 #
934 # This undoes FileNameMangle
935 #
936 sub fileNameUnmangle
937 {
938     my($bpc, $name) = @_;
939
940     $name =~ s{/f}{/}g;
941     $name =~ s{^f}{};
942     $name =~ s{%(..)}{chr(hex($1))}eg;
943     return $name;
944 }
945
946 #
947 # Escape shell meta-characters with backslashes.
948 # This should be applied to each argument seperately, not an
949 # entire shell command.
950 #
951 sub shellEscape
952 {
953     my($bpc, $cmd) = @_;
954
955     $cmd =~ s/([][;&()<>{}|^\n\r\t *\$\\'"`?])/\\$1/g;
956     return $cmd;
957 }
958
959 #
960 # For printing exec commands (which don't use a shell) so they look like
961 # a valid shell command this function should be called with the exec
962 # args.  The shell command string is returned.
963 #
964 sub execCmd2ShellCmd
965 {
966     my($bpc, @args) = @_;
967     my $str;
968
969     foreach my $a ( @args ) {
970         $str .= " " if ( $str ne "" );
971         $str .= $bpc->shellEscape($a);
972     }
973     return $str;
974 }
975
976 #
977 # Do a URI-style escape to protect/encode special characters
978 #
979 sub uriEsc
980 {
981     my($bpc, $s) = @_;
982     $s =~ s{([^\w.\/-])}{sprintf("%%%02X", ord($1));}eg;
983     return $s;
984 }
985
986 #
987 # Do a URI-style unescape to restore special characters
988 #
989 sub uriUnesc
990 {
991     my($bpc, $s) = @_;
992     $s =~ s{%(..)}{chr(hex($1))}eg;
993     return $s;
994 }
995
996 #
997 # Do variable substitution prior to execution of a command.
998 #
999 sub cmdVarSubstitute
1000 {
1001     my($bpc, $template, $vars) = @_;
1002     my(@cmd);
1003
1004     #
1005     # Return without any substitution if the first entry starts with "&",
1006     # indicating this is perl code.
1007     #
1008     if ( (ref($template) eq "ARRAY" ? $template->[0] : $template) =~ /^\&/ ) {
1009         return $template;
1010     }
1011     if ( ref($template) ne "ARRAY" ) {
1012         #
1013         # Split at white space, except if escaped by \
1014         #
1015         $template = [split(/(?<!\\)\s+/, $template)];
1016         #
1017         # Remove the \ that escaped white space.
1018         #
1019         foreach ( @$template ) {
1020             s{\\(\s)}{$1}g;
1021         }
1022     }
1023     #
1024     # Merge variables into @tarClientCmd
1025     #
1026     foreach my $arg ( @$template ) {
1027         #
1028         # Replace scalar variables first
1029         #
1030         $arg =~ s{\$(\w+)(\+?)}{
1031             exists($vars->{$1}) && ref($vars->{$1}) ne "ARRAY"
1032                 ? ($2 eq "+" ? $bpc->shellEscape($vars->{$1}) : $vars->{$1})
1033                 : "\$$1$2"
1034         }eg;
1035         #
1036         # Now replicate any array arguments; this just works for just one
1037         # array var in each argument.
1038         #
1039         if ( $arg =~ m{(.*)\$(\w+)(\+?)(.*)} && ref($vars->{$2}) eq "ARRAY" ) {
1040             my $pre  = $1;
1041             my $var  = $2;
1042             my $esc  = $3;
1043             my $post = $4;
1044             foreach my $v ( @{$vars->{$var}} ) {
1045                 $v = $bpc->shellEscape($v) if ( $esc eq "+" );
1046                 push(@cmd, "$pre$v$post");
1047             }
1048         } else {
1049             push(@cmd, $arg);
1050         }
1051     }
1052     return \@cmd;
1053 }
1054
1055 #
1056 # Exec or eval a command.  $cmd is either a string on an array ref.
1057 #
1058 # @args are optional arguments for the eval() case; they are not used
1059 # for exec().
1060 #
1061 sub cmdExecOrEval
1062 {
1063     my($bpc, $cmd, @args) = @_;
1064     
1065     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
1066         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
1067         print(STDERR "cmdExecOrEval: about to eval perl code $cmd\n")
1068                         if ( $bpc->{verbose} );
1069         eval($cmd);
1070         print(STDERR "Perl code fragment for exec shouldn't return!!\n");
1071         exit(1);
1072     } else {
1073         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
1074         print(STDERR "cmdExecOrEval: about to exec ",
1075               $bpc->execCmd2ShellCmd(@$cmd), "\n")
1076                         if ( $bpc->{verbose} );
1077         alarm(0);
1078         $cmd = [map { m/(.*)/ } @$cmd];         # untaint
1079         #
1080         # force list-form of exec(), ie: no shell even for 1 arg
1081         #
1082         exec { $cmd->[0] } @$cmd;
1083         print(STDERR "Exec failed for @$cmd\n");
1084         exit(1);
1085     }
1086 }
1087
1088 #
1089 # System or eval a command.  $cmd is either a string on an array ref.
1090 # $stdoutCB is a callback for output generated by the command.  If it
1091 # is undef then output is returned.  If it is a code ref then the function
1092 # is called with each piece of output as an argument.  If it is a scalar
1093 # ref the output is appended to this variable.
1094 #
1095 # @args are optional arguments for the eval() case; they are not used
1096 # for system().
1097 #
1098 # Also, $? should be set when the CHILD pipe is closed.
1099 #
1100 sub cmdSystemOrEvalLong
1101 {
1102     my($bpc, $cmd, $stdoutCB, $ignoreStderr, $pidHandlerCB, @args) = @_;
1103     my($pid, $out, $allOut);
1104     local(*CHILD);
1105     
1106     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
1107         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
1108         print(STDERR "cmdSystemOrEval: about to eval perl code $cmd\n")
1109                         if ( $bpc->{verbose} );
1110         $out = eval($cmd);
1111         $$stdoutCB .= $out if ( ref($stdoutCB) eq 'SCALAR' );
1112         &$stdoutCB($out)   if ( ref($stdoutCB) eq 'CODE' );
1113         print(STDERR "cmdSystemOrEval: finished: got output $out\n")
1114                         if ( $bpc->{verbose} );
1115         return $out        if ( !defined($stdoutCB) );
1116         return;
1117     } else {
1118         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
1119         print(STDERR "cmdSystemOrEval: about to system ",
1120               $bpc->execCmd2ShellCmd(@$cmd), "\n")
1121                         if ( $bpc->{verbose} );
1122         if ( !defined($pid = open(CHILD, "-|")) ) {
1123             my $err = "Can't fork to run @$cmd\n";
1124             $? = 1;
1125             $$stdoutCB .= $err if ( ref($stdoutCB) eq 'SCALAR' );
1126             &$stdoutCB($err)   if ( ref($stdoutCB) eq 'CODE' );
1127             return $err        if ( !defined($stdoutCB) );
1128             return;
1129         }
1130         binmode(CHILD);
1131         if ( !$pid ) {
1132             #
1133             # This is the child
1134             #
1135             close(STDERR);
1136             if ( $ignoreStderr ) {
1137                 open(STDERR, ">", "/dev/null");
1138             } else {
1139                 open(STDERR, ">&STDOUT");
1140             }
1141             alarm(0);
1142             $cmd = [map { m/(.*)/ } @$cmd];             # untaint
1143             #
1144             # force list-form of exec(), ie: no shell even for 1 arg
1145             #
1146             exec { $cmd->[0] } @$cmd;
1147             print(STDERR "Exec of @$cmd failed\n");
1148             exit(1);
1149         }
1150
1151         #
1152         # Notify caller of child's pid
1153         #
1154         &$pidHandlerCB($pid) if ( ref($pidHandlerCB) eq "CODE" );
1155
1156         #
1157         # The parent gathers the output from the child
1158         #
1159         while ( <CHILD> ) {
1160             $$stdoutCB .= $_ if ( ref($stdoutCB) eq 'SCALAR' );
1161             &$stdoutCB($_)   if ( ref($stdoutCB) eq 'CODE' );
1162             $out .= $_       if ( !defined($stdoutCB) );
1163             $allOut .= $_    if ( $bpc->{verbose} );
1164         }
1165         $? = 0;
1166         close(CHILD);
1167     }
1168     print(STDERR "cmdSystemOrEval: finished: got output $allOut\n")
1169                         if ( $bpc->{verbose} );
1170     return $out;
1171 }
1172
1173 #
1174 # The shorter version that sets $ignoreStderr = 0, ie: merges stdout
1175 # and stderr together.
1176 #
1177 sub cmdSystemOrEval
1178 {
1179     my($bpc, $cmd, $stdoutCB, @args) = @_;
1180
1181     return $bpc->cmdSystemOrEvalLong($cmd, $stdoutCB, 0, undef, @args);
1182 }
1183
1184 #
1185 # Promotes $conf->{BackupFilesOnly}, $conf->{BackupFilesExclude}
1186 # to hashes and $conf->{$shareName} to an array.
1187 #
1188 sub backupFileConfFix
1189 {
1190     my($bpc, $conf, $shareName) = @_;
1191
1192     $conf->{$shareName} = [ $conf->{$shareName} ]
1193                     if ( ref($conf->{$shareName}) ne "ARRAY" );
1194     foreach my $param qw(BackupFilesOnly BackupFilesExclude) {
1195         next if ( !defined($conf->{$param}) );
1196         if ( ref($conf->{$param}) eq "HASH" ) {
1197             #
1198             # A "*" entry means wildcard - it is the default for
1199             # all shares.  Replicate the "*" entry for all shares,
1200             # but still allow override of specific entries.
1201             #
1202             next if ( !defined($conf->{$param}{"*"}) );
1203             $conf->{$param} = {
1204                                     map({ $_ => $conf->{$param}{"*"} }
1205                                             @{$conf->{$shareName}}),
1206                                     %{$conf->{$param}}
1207                               };
1208         } else {
1209             $conf->{$param} = [ $conf->{$param} ]
1210                                     if ( ref($conf->{$param}) ne "ARRAY" );
1211             $conf->{$param} = { map { $_ => $conf->{$param} }
1212                                     @{$conf->{$shareName}} };
1213         }
1214     }
1215 }
1216
1217 1;