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