# Craig Barratt <cbarratt@users.sourceforge.net>
#
# COPYRIGHT
-# Copyright (C) 2001-2003 Craig Barratt
+# Copyright (C) 2001-2007 Craig Barratt
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
#
#========================================================================
#
-# Version 2.1.0_CVS, released 8 Feb 2004.
+# Version 3.1.0, released 25 Nov 2007.
#
# See http://backuppc.sourceforge.net.
#
use strict;
use vars qw(%Conf %Lang);
-use Fcntl qw/:flock/;
+use BackupPC::Storage;
+use Fcntl ':mode';
use Carp;
-use DirHandle ();
use File::Path;
use File::Compare;
use Socket;
use Cwd;
use Digest::MD5;
+use Config;
+use Encode qw/from_to encode_utf8/;
+
+use vars qw( $IODirentOk );
+use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+require Exporter;
+require DynaLoader;
+
+@ISA = qw(Exporter DynaLoader);
+@EXPORT_OK = qw( BPC_DT_UNKNOWN
+ BPC_DT_FIFO
+ BPC_DT_CHR
+ BPC_DT_DIR
+ BPC_DT_BLK
+ BPC_DT_REG
+ BPC_DT_LNK
+ BPC_DT_SOCK
+ );
+@EXPORT = qw( );
+%EXPORT_TAGS = ('BPC_DT_ALL' => [@EXPORT, @EXPORT_OK]);
+
+BEGIN {
+ eval "use IO::Dirent qw( readdirent DT_DIR );";
+ if ( !$@ && opendir(my $fh, ".") ) {
+ #
+ # Make sure the IO::Dirent really works - some installs
+ # on certain file systems don't return a valid type.
+ #
+ my $dt_dir = eval("DT_DIR");
+ foreach my $e ( readdirent($fh) ) {
+ if ( $e->{name} eq "." && $e->{type} == $dt_dir ) {
+ $IODirentOk = 1;
+ last;
+ }
+ }
+ closedir($fh);
+ }
+};
+
+#
+# The need to match the constants in IO::Dirent
+#
+use constant BPC_DT_UNKNOWN => 0;
+use constant BPC_DT_FIFO => 1; ## named pipe (fifo)
+use constant BPC_DT_CHR => 2; ## character special
+use constant BPC_DT_DIR => 4; ## directory
+use constant BPC_DT_BLK => 6; ## block special
+use constant BPC_DT_REG => 8; ## regular
+use constant BPC_DT_LNK => 10; ## symbolic link
+use constant BPC_DT_SOCK => 12; ## socket
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 "" ? '/tera0/backup/BackupPC/conf' : $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.1.0',
}, $class;
- $bpc->{BinDir} .= "/bin";
- $bpc->{LibDir} .= "/lib";
+
+ $bpc->{storage} = BackupPC::Storage->new($paths);
+
#
# Clean up %ENV and setup other variables.
#
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("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;
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
sub adminJob
{
- return " admin ";
+ my($bpc, $num) = @_;
+ return " admin " if ( !$num );
+ return " admin$num ";
+}
+
+sub isAdminJob
+{
+ my($bpc, $str) = @_;
+ return $str =~ /^ admin/;
}
sub trashJob
return $bpc->{verbose};
}
+sub sigName2num
+{
+ my($bpc, $sig) = @_;
+
+ if ( !defined($bpc->{SigName2Num}) ) {
+ my $i = 0;
+ foreach my $name ( split(' ', $Config{sig_name}) ) {
+ $bpc->{SigName2Num}{$name} = $i;
+ $i++;
+ }
+ }
+ return $bpc->{SigName2Num}{$sig};
+}
+
#
# Generate an ISO 8601 format timeStamp (but without the "T").
# See http://www.w3.org/TR/NOTE-datetime and
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 ( <BK_INFO> ) {
- 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 ( <RESTORE_INFO> ) {
- 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 ( <ARCHIVE_INFO> ) {
- 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
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 ( $@ );
return $mesg;
}
$bpc->{Lang} = \%Lang;
+
+ #
+ # Make sure IncrLevels is defined
+ #
+ $bpc->{Conf}{IncrLevels} = [1] if ( !defined($bpc->{Conf}{IncrLevels}) );
+
return;
}
sub ConfigMTime
{
my($bpc) = @_;
- return (stat("$bpc->{TopDir}/conf/config.pl"))[9];
+
+ return $bpc->{storage}->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 {};
+ return $bpc->{storage}->HostInfoRead($host);
+}
+
+sub HostInfoWrite
+{
+ my($bpc, $host) = @_;
+
+ return $bpc->{storage}->HostInfoWrite($host);
+}
+
+#
+# Return the mtime of the hosts file
+#
+sub HostsMTime
+{
+ my($bpc) = @_;
+
+ return $bpc->{storage}->HostsMTime();
+}
+
+#
+# Read a directory and return the entries in sorted inode order.
+# This relies on the IO::Dirent module being installed. If not,
+# the inode data is empty and the default directory order is
+# returned.
+#
+# The returned data is a list of hashes with entries {name, type, inode, nlink}.
+# The returned data includes "." and "..".
+#
+# $need is a hash of file attributes we need: type, inode, or nlink.
+# If set, these parameters are added to the returned hash.
+#
+# To support browsing pre-3.0.0 backups where the charset encoding
+# is typically iso-8859-1, the charsetLegacy option can be set in
+# $need to convert the path from utf8 and convert the names to utf8.
+#
+# If IO::Dirent is successful if will get type and inode for free.
+# Otherwise, a stat is done on each file, which is more expensive.
+#
+sub dirRead
+{
+ my($bpc, $path, $need) = @_;
+ my(@entries, $addInode);
+
+ from_to($path, "utf8", $need->{charsetLegacy})
+ if ( $need->{charsetLegacy} ne "" );
+ return if ( !opendir(my $fh, $path) );
+ if ( $IODirentOk ) {
+ @entries = sort({ $a->{inode} <=> $b->{inode} } readdirent($fh));
+ map { $_->{type} = 0 + $_->{type} } @entries; # make type numeric
+ } else {
+ @entries = map { { name => $_} } readdir($fh);
}
- binmode(HOST_INFO);
- while ( <HOST_INFO> ) {
- 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(/(?<!\\)\s+/, $1);
- #
- # Remove any \
- #
- foreach ( @fld ) {
- s{\\(\s)}{$1}g;
- }
- if ( @hdr ) {
- if ( defined($host) ) {
- next if ( lc($fld[0]) ne $host );
- @{$hosts{lc($fld[0])}}{@hdr} = @fld;
- close(HOST_INFO);
- return \%hosts;
- } else {
- @{$hosts{lc($fld[0])}}{@hdr} = @fld;
+ closedir($fh);
+ if ( defined($need) ) {
+ for ( my $i = 0 ; $i < @entries ; $i++ ) {
+ next if ( (!$need->{inode} || defined($entries[$i]{inode}))
+ && (!$need->{type} || defined($entries[$i]{type}))
+ && (!$need->{nlink} || defined($entries[$i]{nlink})) );
+ my @s = stat("$path/$entries[$i]{name}");
+ $entries[$i]{nlink} = $s[3] if ( $need->{nlink} );
+ if ( $need->{inode} && !defined($entries[$i]{inode}) ) {
+ $addInode = 1;
+ $entries[$i]{inode} = $s[1];
+ }
+ if ( $need->{type} && !defined($entries[$i]{type}) ) {
+ my $mode = S_IFMT($s[2]);
+ $entries[$i]{type} = BPC_DT_FIFO if ( S_ISFIFO($mode) );
+ $entries[$i]{type} = BPC_DT_CHR if ( S_ISCHR($mode) );
+ $entries[$i]{type} = BPC_DT_DIR if ( S_ISDIR($mode) );
+ $entries[$i]{type} = BPC_DT_BLK if ( S_ISBLK($mode) );
+ $entries[$i]{type} = BPC_DT_REG if ( S_ISREG($mode) );
+ $entries[$i]{type} = BPC_DT_LNK if ( S_ISLNK($mode) );
+ $entries[$i]{type} = BPC_DT_SOCK if ( S_ISSOCK($mode) );
}
- } else {
- @hdr = @fld;
}
}
- close(HOST_INFO);
- return \%hosts;
+ #
+ # Sort the entries if inodes were added (the IO::Dirent case already
+ # sorted above)
+ #
+ @entries = sort({ $a->{inode} <=> $b->{inode} } @entries) if ( $addInode );
+ #
+ # for browing pre-3.0.0 backups, map iso-8859-1 to utf8 if requested
+ #
+ if ( $need->{charsetLegacy} ne "" ) {
+ for ( my $i = 0 ; $i < @entries ; $i++ ) {
+ from_to($entries[$i]{name}, $need->{charsetLegacy}, "utf8");
+ }
+ }
+ return \@entries;
}
#
-# Return the mtime of the hosts file
+# Same as dirRead, but only returns the names (which will be sorted in
+# inode order if IO::Dirent is installed)
#
-sub HostsMTime
+sub dirReadNames
{
- my($bpc) = @_;
- return (stat("$bpc->{TopDir}/conf/hosts"))[9];
+ my($bpc, $path, $need) = @_;
+
+ my $entries = $bpc->dirRead($path, $need);
+ return if ( !defined($entries) );
+ my @names = map { $_->{name} } @$entries;
+ return \@names;
+}
+
+sub find
+{
+ my($bpc, $param, $dir, $dontDoCwd) = @_;
+
+ return if ( !chdir($dir) );
+ my $entries = $bpc->dirRead(".", {inode => 1, type => 1});
+ #print Dumper($entries);
+ foreach my $f ( @$entries ) {
+ next if ( $f->{name} eq ".." || $f->{name} eq "." && $dontDoCwd );
+ $param->{wanted}($f->{name}, "$dir/$f->{name}");
+ next if ( $f->{type} != BPC_DT_DIR || $f->{name} eq "." );
+ chdir($f->{name});
+ $bpc->find($param, "$dir/$f->{name}", 1);
+ return if ( !chdir("..") );
+ }
}
#
# Stripped down from File::Path. In particular we don't print
# many warnings and we try three times to delete each directory
# and file -- for some reason the original File::Path rmtree
-# didn't always completely remove a directory tree on the NetApp.
+# didn't always completely remove a directory tree on a NetApp.
#
# Warning: this routine changes the cwd.
#
if ( defined($roots) && length($roots) ) {
$roots = [$roots] unless ref $roots;
} else {
- print "RmTreeQuiet: No root path(s) specified\n";
+ print(STDERR "RmTreeQuiet: No root path(s) specified\n");
}
chdir($pwd);
foreach $root (@{$roots}) {
#
if ( !unlink($root) ) {
if ( -d $root ) {
- my $d = DirHandle->new($root)
- or print "Can't read $pwd/$root: $!";
- @files = $d->read;
- $d->close;
- @files = grep $_!~/^\.{1,2}$/, @files;
- $bpc->RmTreeQuiet("$pwd/$root", \@files);
- chdir($pwd);
- rmdir($root) || rmdir($root);
+ my $d = $bpc->dirReadNames($root);
+ if ( !defined($d) ) {
+ print(STDERR "Can't read $pwd/$root: $!\n");
+ } else {
+ @files = grep $_ !~ /^\.{1,2}$/, @$d;
+ $bpc->RmTreeQuiet("$pwd/$root", \@files);
+ chdir($pwd);
+ rmdir($root) || rmdir($root);
+ }
} else {
unlink($root) || unlink($root);
}
$cwd = $1 if ( $cwd =~ /(.*)/ );
return if ( !-d $trashDir );
- my $d = DirHandle->new($trashDir) or carp "Can't read $trashDir: $!";
- @files = $d->read;
- $d->close;
- @files = grep $_!~/^\.{1,2}$/, @files;
+ my $d = $bpc->dirReadNames($trashDir) or carp "Can't read $trashDir: $!";
+ @files = grep $_ !~ /^\.{1,2}$/, @$d;
return 0 if ( !@files );
$bpc->RmTreeQuiet($trashDir, \@files);
foreach my $f ( @files ) {
#
# 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: $!";
{
my($bpc, $mesg) = @_;
return if ( !defined(my $fh = $bpc->{ServerFD}) );
+ $mesg =~ s/\n/\\n/g;
+ $mesg =~ s/\r/\\r/g;
my $md5 = Digest::MD5->new;
+ $mesg = encode_utf8($mesg);
$md5->add($bpc->{ServerSeed} . $bpc->{ServerMesgCnt}
. $bpc->{Conf}{ServerMesgSecret} . $mesg);
print($fh $md5->b64digest . " $mesg\n");
}
}
+#
+# Tests if we can create a hardlink from a file in directory
+# $newDir to a file in directory $targetDir. A temporary
+# file in $targetDir is created and an attempt to create a
+# hardlink of the same name in $newDir is made. The temporary
+# files are removed.
+#
+# Like link(), returns true on success and false on failure.
+#
+sub HardlinkTest
+{
+ my($bpc, $targetDir, $newDir) = @_;
+
+ my($targetFile, $newFile, $fd);
+ for ( my $i = 0 ; ; $i++ ) {
+ $targetFile = "$targetDir/.TestFileLink.$$.$i";
+ $newFile = "$newDir/.TestFileLink.$$.$i";
+ last if ( !-e $targetFile && !-e $newFile );
+ }
+ return 0 if ( !open($fd, ">", $targetFile) );
+ close($fd);
+ my $ret = link($targetFile, $newFile);
+ unlink($targetFile);
+ unlink($newFile);
+ return $ret;
+}
+
sub CheckHostAlive
{
my($bpc, $host) = @_;
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} );
#
# 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")
# 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
#
# force list-form of exec(), ie: no shell even for 1 arg
#
exec { $cmd->[0] } @$cmd;
- print("Exec of @$cmd failed\n");
+ 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
#
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;