X-Git-Url: http://git.rot13.org/?p=BackupPC.git;a=blobdiff_plain;f=lib%2FBackupPC%2FLib.pm;h=0c4b69b075f9c21174c21852f3397afe9a33f9ef;hp=7b2973c1336f8fabc8bf48e6f65b15f937a54569;hb=c2b072c9ad558447fb73fedf0cad170214b7d80e;hpb=3ae3b7557db4829ebfc6f580ceac30376717db6a diff --git a/lib/BackupPC/Lib.pm b/lib/BackupPC/Lib.pm index 7b2973c..0c4b69b 100644 --- a/lib/BackupPC/Lib.pm +++ b/lib/BackupPC/Lib.pm @@ -29,7 +29,7 @@ # #======================================================================== # -# Version 2.1.0_CVS, released 13 Mar 2004. +# Version 3.0.0, released 28 Jan 2007. # # See http://backuppc.sourceforge.net. # @@ -40,6 +40,7 @@ package BackupPC::Lib; use strict; use vars qw(%Conf %Lang); +use BackupPC::Storage; use Fcntl qw/:flock/; use Carp; use DirHandle (); @@ -53,30 +54,51 @@ use Config; sub new { my $class = shift; - my($topDir, $installDir, $noUserCheck) = @_; + my($topDir, $installDir, $confDir, $noUserCheck) = @_; + + # + # Whether to use filesystem hierarchy standard for file layout. + # If set, text config files are below /etc/BackupPC. + # + my $useFHS = 0; + my $paths; + + # + # Set defaults for $topDir and $installDir. + # + $topDir = '/tera0/backup/BackupPC' if ( $topDir eq "" ); + $installDir = '/usr/local/BackupPC' if ( $installDir eq "" ); + + # + # Pick some initial defaults. For FHS the only critical + # path is the ConfDir, since we get everything else out + # of the main config file. + # + if ( $useFHS ) { + $paths = { + useFHS => $useFHS, + TopDir => $topDir, + InstallDir => $installDir, + ConfDir => $confDir eq "" ? '/etc/BackupPC' : $confDir, + LogDir => '/var/log/BackupPC', + }; + } else { + $paths = { + useFHS => $useFHS, + TopDir => $topDir, + InstallDir => $installDir, + ConfDir => $confDir eq "" ? "$topDir/conf" : $confDir, + LogDir => "$topDir/log", + }; + } my $bpc = bless { - TopDir => $topDir || '/data/BackupPC', - BinDir => $installDir || '/usr/local/BackupPC', - LibDir => $installDir || '/usr/local/BackupPC', - Version => '2.1.0_CVS', - BackupFields => [qw( - num type startTime endTime - nFiles size nFilesExist sizeExist nFilesNew sizeNew - xferErrs xferBadFile xferBadShare tarErrs - compress sizeExistComp sizeNewComp - noFill fillFromNum mangle xferMethod level - )], - RestoreFields => [qw( - num startTime endTime result errorMsg nFiles size - tarCreateErrs xferErrs - )], - ArchiveFields => [qw( - num startTime endTime result errorMsg - )], + %$paths, + Version => '3.0.0', }, $class; - $bpc->{BinDir} .= "/bin"; - $bpc->{LibDir} .= "/lib"; + + $bpc->{storage} = BackupPC::Storage->new($paths); + # # Clean up %ENV and setup other variables. # @@ -87,14 +109,25 @@ sub new print(STDERR $error, "\n"); return; } + + # + # Update the paths based on the config file + # + foreach my $dir ( qw(TopDir ConfDir InstallDir LogDir) ) { + next if ( $bpc->{Conf}{$dir} eq "" ); + $paths->{$dir} = $bpc->{$dir} = $bpc->{Conf}{$dir}; + } + $bpc->{storage}->setPaths($paths); + # # Verify we are running as the correct user # if ( !$noUserCheck && $bpc->{Conf}{BackupPCUserVerify} && $> != (my $uid = (getpwnam($bpc->{Conf}{BackupPCUser}))[2]) ) { - print(STDERR "Wrong user: my userid is $>, instead of $uid" + print(STDERR "$0: Wrong user: my userid is $>, instead of $uid" . " ($bpc->{Conf}{BackupPCUser})\n"); + print(STDERR "Please su $bpc->{Conf}{BackupPCUser} first\n"); return; } return $bpc; @@ -109,7 +142,37 @@ sub TopDir sub BinDir { my($bpc) = @_; - return $bpc->{BinDir}; + return "$bpc->{InstallDir}/bin"; +} + +sub LogDir +{ + my($bpc) = @_; + return $bpc->{LogDir}; +} + +sub ConfDir +{ + my($bpc) = @_; + return $bpc->{ConfDir}; +} + +sub LibDir +{ + my($bpc) = @_; + return "$bpc->{InstallDir}/lib"; +} + +sub InstallDir +{ + my($bpc) = @_; + return $bpc->{InstallDir}; +} + +sub useFHS +{ + my($bpc) = @_; + return $bpc->{useFHS}; } sub Version @@ -132,7 +195,15 @@ sub Lang sub adminJob { - return " admin "; + my($bpc, $num) = @_; + return " admin " if ( !$num ); + return " admin$num "; +} + +sub isAdminJob +{ + my($bpc, $str) = @_; + return $str =~ /^ admin/; } sub trashJob @@ -187,159 +258,84 @@ sub timeStamp sub BackupInfoRead { my($bpc, $host) = @_; - local(*BK_INFO, *LOCK); - my(@Backups); - - flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK"); - if ( open(BK_INFO, "$bpc->{TopDir}/pc/$host/backups") ) { - binmode(BK_INFO); - while ( ) { - s/[\n\r]+//; - next if ( !/^(\d+\t(incr|full|partial)[\d\t]*$)/ ); - $_ = $1; - @{$Backups[@Backups]}{@{$bpc->{BackupFields}}} = split(/\t/); - } - close(BK_INFO); - } - close(LOCK); - return @Backups; + + return $bpc->{storage}->BackupInfoRead($host); } sub BackupInfoWrite { my($bpc, $host, @Backups) = @_; - local(*BK_INFO, *LOCK); - my($i); - - flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK"); - unlink("$bpc->{TopDir}/pc/$host/backups.old") - if ( -f "$bpc->{TopDir}/pc/$host/backups.old" ); - rename("$bpc->{TopDir}/pc/$host/backups", - "$bpc->{TopDir}/pc/$host/backups.old") - if ( -f "$bpc->{TopDir}/pc/$host/backups" ); - if ( open(BK_INFO, ">$bpc->{TopDir}/pc/$host/backups") ) { - binmode(BK_INFO); - for ( $i = 0 ; $i < @Backups ; $i++ ) { - my %b = %{$Backups[$i]}; - printf(BK_INFO "%s\n", join("\t", @b{@{$bpc->{BackupFields}}})); - } - close(BK_INFO); - } - close(LOCK); + + return $bpc->{storage}->BackupInfoWrite($host, @Backups); } sub RestoreInfoRead { my($bpc, $host) = @_; - local(*RESTORE_INFO, *LOCK); - my(@Restores); - - flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK"); - if ( open(RESTORE_INFO, "$bpc->{TopDir}/pc/$host/restores") ) { - binmode(RESTORE_INFO); - while ( ) { - s/[\n\r]+//; - next if ( !/^(\d+.*)/ ); - $_ = $1; - @{$Restores[@Restores]}{@{$bpc->{RestoreFields}}} = split(/\t/); - } - close(RESTORE_INFO); - } - close(LOCK); - return @Restores; + + return $bpc->{storage}->RestoreInfoRead($host); } sub RestoreInfoWrite { my($bpc, $host, @Restores) = @_; - local(*RESTORE_INFO, *LOCK); - my($i); - - flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK"); - unlink("$bpc->{TopDir}/pc/$host/restores.old") - if ( -f "$bpc->{TopDir}/pc/$host/restores.old" ); - rename("$bpc->{TopDir}/pc/$host/restores", - "$bpc->{TopDir}/pc/$host/restores.old") - if ( -f "$bpc->{TopDir}/pc/$host/restores" ); - if ( open(RESTORE_INFO, ">$bpc->{TopDir}/pc/$host/restores") ) { - binmode(RESTORE_INFO); - for ( $i = 0 ; $i < @Restores ; $i++ ) { - my %b = %{$Restores[$i]}; - printf(RESTORE_INFO "%s\n", - join("\t", @b{@{$bpc->{RestoreFields}}})); - } - close(RESTORE_INFO); - } - close(LOCK); + + return $bpc->{storage}->RestoreInfoWrite($host, @Restores); } sub ArchiveInfoRead { my($bpc, $host) = @_; - local(*ARCHIVE_INFO, *LOCK); - my(@Archives); - - flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK"); - if ( open(ARCHIVE_INFO, "$bpc->{TopDir}/pc/$host/archives") ) { - binmode(ARCHIVE_INFO); - while ( ) { - s/[\n\r]+//; - next if ( !/^(\d+.*)/ ); - $_ = $1; - @{$Archives[@Archives]}{@{$bpc->{ArchiveFields}}} = split(/\t/); - } - close(ARCHIVE_INFO); - } - close(LOCK); - return @Archives; + + return $bpc->{storage}->ArchiveInfoRead($host); } sub ArchiveInfoWrite { my($bpc, $host, @Archives) = @_; - local(*ARCHIVE_INFO, *LOCK); - my($i); - - flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK"); - unlink("$bpc->{TopDir}/pc/$host/archives.old") - if ( -f "$bpc->{TopDir}/pc/$host/archives.old" ); - rename("$bpc->{TopDir}/pc/$host/archives", - "$bpc->{TopDir}/pc/$host/archives.old") - if ( -f "$bpc->{TopDir}/pc/$host/archives" ); - if ( open(ARCHIVE_INFO, ">$bpc->{TopDir}/pc/$host/archives") ) { - binmode(ARCHIVE_INFO); - for ( $i = 0 ; $i < @Archives ; $i++ ) { - my %b = %{$Archives[$i]}; - printf(ARCHIVE_INFO "%s\n", - join("\t", @b{@{$bpc->{ArchiveFields}}})); - } - close(ARCHIVE_INFO); - } - close(LOCK); + + return $bpc->{storage}->ArchiveInfoWrite($host, @Archives); +} + +sub ConfigDataRead +{ + my($bpc, $host) = @_; + + return $bpc->{storage}->ConfigDataRead($host); +} + +sub ConfigDataWrite +{ + my($bpc, $host, $conf) = @_; + + return $bpc->{storage}->ConfigDataWrite($host, $conf); } sub ConfigRead { my($bpc, $host) = @_; - my($ret, $mesg, $config, @configs); - - $bpc->{Conf} = (); - push(@configs, "$bpc->{TopDir}/conf/config.pl"); - push(@configs, "$bpc->{TopDir}/conf/$host.pl") - if ( $host ne "config" && -f "$bpc->{TopDir}/conf/$host.pl" ); - push(@configs, "$bpc->{TopDir}/pc/$host/config.pl") - if ( defined($host) && -f "$bpc->{TopDir}/pc/$host/config.pl" ); - foreach $config ( @configs ) { - %Conf = (); - if ( !defined($ret = do $config) && ($! || $@) ) { - $mesg = "Couldn't open $config: $!" if ( $! ); - $mesg = "Couldn't execute $config: $@" if ( $@ ); - $mesg =~ s/[\n\r]+//; - return $mesg; - } - %{$bpc->{Conf}} = ( %{$bpc->{Conf} || {}}, %Conf ); + my($ret); + + # + # Read main config file + # + my($mesg, $config) = $bpc->{storage}->ConfigDataRead(); + return $mesg if ( defined($mesg) ); + + $bpc->{Conf} = $config; + + # + # Read host config file + # + if ( $host ne "" ) { + ($mesg, $config) = $bpc->{storage}->ConfigDataRead($host); + return $mesg if ( defined($mesg) ); + $bpc->{Conf} = { %{$bpc->{Conf}}, %$config }; } - return if ( !defined($bpc->{Conf}{Language}) ); + + # + # Load optional perl modules + # if ( defined($bpc->{Conf}{PerlModuleLoad}) ) { # # Load any user-specified perl modules. This is for @@ -351,7 +347,12 @@ sub ConfigRead eval("use $module;"); } } - my $langFile = "$bpc->{LibDir}/BackupPC/Lang/$bpc->{Conf}{Language}.pm"; + + # + # Load language file + # + return "No language setting" if ( !defined($bpc->{Conf}{Language}) ); + my $langFile = "$bpc->{InstallDir}/lib/BackupPC/Lang/$bpc->{Conf}{Language}.pm"; if ( !defined($ret = do $langFile) && ($! || $@) ) { $mesg = "Couldn't open language file $langFile: $!" if ( $! ); $mesg = "Couldn't execute language file $langFile: $@" if ( $@ ); @@ -368,7 +369,8 @@ sub ConfigRead sub ConfigMTime { my($bpc) = @_; - return (stat("$bpc->{TopDir}/conf/config.pl"))[9]; + + return $bpc->{storage}->ConfigMTime(); } # @@ -381,47 +383,15 @@ sub ConfigMTime sub HostInfoRead { my($bpc, $host) = @_; - my(%hosts, @hdr, @fld); - local(*HOST_INFO); - if ( !open(HOST_INFO, "$bpc->{TopDir}/conf/hosts") ) { - print(STDERR $bpc->timeStamp, - "Can't open $bpc->{TopDir}/conf/hosts\n"); - return {}; - } - binmode(HOST_INFO); - while ( ) { - s/[\n\r]+//; - s/#.*//; - s/\s+$//; - next if ( /^\s*$/ || !/^([\w\.\\-]+\s+.*)/ ); - # - # Split on white space, except if preceded by \ - # using zero-width negative look-behind assertion - # (always wanted to use one of those). - # - @fld = split(/(?{storage}->HostInfoRead($host); +} + +sub HostInfoWrite +{ + my($bpc, $host) = @_; + + return $bpc->{storage}->HostInfoWrite($host); } # @@ -430,7 +400,8 @@ sub HostInfoRead sub HostsMTime { my($bpc) = @_; - return (stat("$bpc->{TopDir}/conf/hosts"))[9]; + + return $bpc->{storage}->HostsMTime(); } # @@ -545,7 +516,7 @@ sub ServerConnect # # First try the unix-domain socket # - my $sockFile = "$bpc->{TopDir}/log/BackupPC.sock"; + my $sockFile = "$bpc->{LogDir}/BackupPC.sock"; socket(*FH, PF_UNIX, SOCK_STREAM, 0) || return "unix socket: $!"; if ( !connect(*FH, sockaddr_un($sockFile)) ) { my $err = "unix connect: $!"; @@ -815,10 +786,12 @@ sub CheckHostAlive if ( $bpc->{verbose} ); return -1; } - if ( $s =~ /time=([\d\.]+)\s*ms/i ) { + if ( $s =~ /rtt\s*min\/avg\/max\/mdev\s*=\s*[\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+\s*(ms|usec)/i ) { + $ret = $1; + $ret /= 1000 if ( lc($2) eq "usec" ); + } elsif ( $s =~ /time=([\d.]+)\s*(ms|usec)/i ) { $ret = $1; - } elsif ( $s =~ /time=([\d\.]+)\s*usec/i ) { - $ret = $1/1000; + $ret /= 1000 if ( lc($2) eq "usec" ); } else { print(STDERR "CheckHostAlive: can't extract round-trip time" . " (not fatal)\n") if ( $bpc->{verbose} ); @@ -1129,12 +1102,13 @@ sub cmdExecOrEval # # Also, $? should be set when the CHILD pipe is closed. # -sub cmdSystemOrEval +sub cmdSystemOrEvalLong { - my($bpc, $cmd, $stdoutCB, @args) = @_; + my($bpc, $cmd, $stdoutCB, $ignoreStderr, $pidHandlerCB, @args) = @_; my($pid, $out, $allOut); local(*CHILD); + $? = 0; if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) { $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" ); print(STDERR "cmdSystemOrEval: about to eval perl code $cmd\n") @@ -1165,7 +1139,11 @@ sub cmdSystemOrEval # This is the child # close(STDERR); - open(STDERR, ">&STDOUT"); + if ( $ignoreStderr ) { + open(STDERR, ">", "/dev/null"); + } else { + open(STDERR, ">&STDOUT"); + } alarm(0); $cmd = [map { m/(.*)/ } @$cmd]; # untaint # @@ -1175,6 +1153,12 @@ sub cmdSystemOrEval print(STDERR "Exec of @$cmd failed\n"); exit(1); } + + # + # Notify caller of child's pid + # + &$pidHandlerCB($pid) if ( ref($pidHandlerCB) eq "CODE" ); + # # The parent gathers the output from the child # @@ -1192,4 +1176,109 @@ sub cmdSystemOrEval return $out; } +# +# The shorter version that sets $ignoreStderr = 0, ie: merges stdout +# and stderr together. +# +sub cmdSystemOrEval +{ + my($bpc, $cmd, $stdoutCB, @args) = @_; + + return $bpc->cmdSystemOrEvalLong($cmd, $stdoutCB, 0, undef, @args); +} + +# +# Promotes $conf->{BackupFilesOnly}, $conf->{BackupFilesExclude} +# to hashes and $conf->{$shareName} to an array. +# +sub backupFileConfFix +{ + my($bpc, $conf, $shareName) = @_; + + $conf->{$shareName} = [ $conf->{$shareName} ] + if ( ref($conf->{$shareName}) ne "ARRAY" ); + foreach my $param qw(BackupFilesOnly BackupFilesExclude) { + next if ( !defined($conf->{$param}) ); + if ( ref($conf->{$param}) eq "HASH" ) { + # + # A "*" entry means wildcard - it is the default for + # all shares. Replicate the "*" entry for all shares, + # but still allow override of specific entries. + # + next if ( !defined($conf->{$param}{"*"}) ); + $conf->{$param} = { + map({ $_ => $conf->{$param}{"*"} } + @{$conf->{$shareName}}), + %{$conf->{$param}} + }; + } else { + $conf->{$param} = [ $conf->{$param} ] + if ( ref($conf->{$param}) ne "ARRAY" ); + $conf->{$param} = { map { $_ => $conf->{$param} } + @{$conf->{$shareName}} }; + } + } +} + +# +# This is sort() compare function, used below. +# +# New client LOG names are LOG.MMYYYY. Old style names are +# LOG, LOG.0, LOG.1 etc. Sort them so new names are +# first, and newest to oldest. +# +sub compareLOGName +{ + my $na = $1 if ( $a =~ /LOG\.(\d+)(\.z)?$/ ); + my $nb = $1 if ( $b =~ /LOG\.(\d+)(\.z)?$/ ); + + $na = -1 if ( !defined($na) ); + $nb = -1 if ( !defined($nb) ); + + if ( length($na) >= 5 && length($nb) >= 5 ) { + # + # Both new style: format is MMYYYY. Bigger dates are + # more recent. + # + my $ma = $2 * 12 + $1 if ( $na =~ /(\d+)(\d{4})/ ); + my $mb = $2 * 12 + $1 if ( $nb =~ /(\d+)(\d{4})/ ); + return $mb - $ma; + } elsif ( length($na) >= 5 && length($nb) < 5 ) { + return -1; + } elsif ( length($na) < 5 && length($nb) >= 5 ) { + return 1; + } else { + # + # Both old style. Smaller numbers are more recent. + # + return $na - $nb; + } +} + +# +# Returns list of paths to a clients's (or main) LOG files, +# most recent first. +# +sub sortedPCLogFiles +{ + my($bpc, $host) = @_; + + my(@files, $dir); + + if ( $host ne "" ) { + $dir = "$bpc->{TopDir}/pc/$host"; + } else { + $dir = "$bpc->{LogDir}"; + } + if ( opendir(DIR, $dir) ) { + foreach my $file ( readdir(DIR) ) { + next if ( !-f "$dir/$file" ); + next if ( $file ne "LOG" && $file !~ /^LOG\.\d/ ); + push(@files, "$dir/$file"); + } + closedir(DIR); + } + return sort(compareLOGName @files); +} + 1;